Skip to content

clicking on notifs should navigate to pane#1344

Merged
Kitenite merged 3 commits into
mainfrom
kitenite/clicking-on-notifs-should-navigate-to-pane
Feb 9, 2026
Merged

clicking on notifs should navigate to pane#1344
Kitenite merged 3 commits into
mainfrom
kitenite/clicking-on-notifs-should-navigate-to-pane

Conversation

@Kitenite
Copy link
Copy Markdown
Collaborator

@Kitenite Kitenite commented Feb 9, 2026

Description

Related Issues

Type of Change

  • Bug fix
  • New feature
  • Documentation
  • Refactor
  • Other (please describe):

Testing

Screenshots (if applicable)

Additional Notes

Summary by CodeRabbit

  • New Features

    • Workspace navigation supports URL search parameters to activate and focus specific tabs and panes.
    • Desktop notifications manager delivers agent lifecycle notifications with suppression, replacement, sounds, and cleanup.
  • Refactor

    • Navigation now uses route-driven parameters for tab/pane activation instead of immediate state mutations.
  • Tests

    • Comprehensive tests added for the notification manager covering lifecycle, visibility, replacement, and interactions.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 9, 2026

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

Adds URL-search-based workspace navigation (tabId, paneId) consumed and cleared by the workspace route; updates agent hook to navigate with search params; introduces a new main-process NotificationManager with tests and integrates it into window lifecycle.

Changes

Cohort / File(s) Summary
Workspace navigation util
apps/desktop/src/renderer/routes/_authenticated/_dashboard/utils/workspace-navigation.ts
Added WorkspaceSearchParams and extended navigateToWorkspace to accept a search object; de-structured options to pass search separately to navigation.
Workspace route page
apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/page.tsx
Added validateSearch to parse tabId/paneId from URL search, consumes search on mount to activate/focus UI, then navigates to clear search.
Agent hook listener
apps/desktop/src/renderer/stores/tabs/useAgentHookListener.ts
Replaced multi-step FOCUS_TAB state mutations with a single navigateToWorkspace(..., { search: { tabId, paneId } }) navigation.
Notification manager & tests
apps/desktop/src/main/lib/notifications/notification-manager.ts, apps/desktop/src/main/lib/notifications/notification-manager.test.ts
Introduced NotificationManager, NativeNotification, and NotificationManagerDeps; implements creation, tracking, TTL sweep, suppression via visibility context, click/close wiring, sound playback, and comprehensive tests.
Main windows integration
apps/desktop/src/main/windows/main.ts
Replaced inline notification handling with NotificationManager usage, starts/disposes the manager, and delegates lifecycle events to it.

Sequence Diagram(s)

sequenceDiagram
    participant Agent as AgentLifecycleProducer
    participant NM as NotificationManager
    participant Native as NativeNotification (OS)
    participant Win as Window/Renderer

    Agent->>NM: handleAgentLifecycle(event)
    NM->>NM: shouldSuppressForVisiblePane? (getVisibilityContext)
    alt not suppressed
      NM->>Native: createNotification(title, body)
      Native-->>NM: notificationInstance
      NM->>Native: playSound()
      Native->>NM: on('click')
      Native->>NM: on('close')
      Native->>Win: click -> navigate/focus payload (via IPC)
    else suppressed
      NM-->>Agent: ignored (no notification)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • saddlepaddle

Poem

🐇 I hopped along the route so bright,
Whispered tabId into the night,
Pane found focus, search swept clear,
Bells and chirps brought joy near,
A tiny rabbit applauds the light 🌸

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description contains only the default template with unfilled placeholders and no concrete implementation details, rationale, or testing information provided by the author. Fill in the description template with actual details: explain the macOS notification handler loss issue, describe the NotificationManager solution, link related issues, mark as 'Refactor', document testing steps, and add any relevant context.
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'clicking on notifs should navigate to pane' accurately reflects the main purpose of the PR, which adds notification click handling that navigates to the relevant pane.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch kitenite/clicking-on-notifs-should-navigate-to-pane

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/`$workspaceId/page.tsx:
- Around line 74-90: The effect that reads useTabsStore.getState() can return
early when state.tabs.find(...) yields undefined and leaves stale search params;
modify the effect in page.tsx so that if no tab is found for searchTabId you
call routeNavigate({ search: {}, replace: true }) before returning (ensuring
search params are cleared even if the tab hasn't loaded), and optionally
consider the alternative approach of subscribing to the tabs slice (instead of
using getState) so the effect re-runs when tabs are populated; target symbols:
useEffect, useTabsStore.getState, state.tabs.find, state.setActiveTab,
state.setFocusedPane, routeNavigate, searchTabId, searchPaneId.
🧹 Nitpick comments (1)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/utils/workspace-navigation.ts (1)

22-24: Also omit "search" from NavigateOptions for proper type narrowing.

NavigateOptions from TanStack Router has a search property that accepts broader types (true | TToSearch | ((prev: TFromSearch) => TToSearch)). The current intersection overrides this but doesn't prevent type confusion. Use Omit<NavigateOptions, "to" | "params" | "search"> & { search?: WorkspaceSearchParams } to explicitly narrow the search type and prevent callers from accidentally relying on unsupported search patterns.

Comment on lines +74 to +90
useEffect(() => {
if (!searchTabId) return;

const state = useTabsStore.getState();
const tab = state.tabs.find(
(t) => t.id === searchTabId && t.workspaceId === workspaceId,
);
if (!tab) return;

state.setActiveTab(workspaceId, searchTabId);

if (searchPaneId && state.panes[searchPaneId]) {
state.setFocusedPane(searchTabId, searchPaneId);
}

routeNavigate({ search: {}, replace: true });
}, [searchTabId, searchPaneId, workspaceId, routeNavigate]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

If the target tab isn't in the store yet, search params silently persist without retry.

When state.tabs.find(...) returns undefined at line 78-81, the effect returns early without clearing the search params. Since the effect's dependency array doesn't include any store state (intentionally — it reads imperatively), it won't re-run when tabs eventually load. This leaves stale ?tabId=...&paneId=... in the URL.

If this is an unlikely edge case (tabs are persisted and always available), this is fine as-is. But if there's any scenario where tabs load asynchronously after mount, consider either:

  1. Subscribing to the relevant store slice so the effect retries, or
  2. Clearing the search params even on the early-return path (accepting that the navigation might be a no-op).
🤖 Prompt for AI Agents
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/`$workspaceId/page.tsx
around lines 74 - 90, The effect that reads useTabsStore.getState() can return
early when state.tabs.find(...) yields undefined and leaves stale search params;
modify the effect in page.tsx so that if no tab is found for searchTabId you
call routeNavigate({ search: {}, replace: true }) before returning (ensuring
search params are cleared even if the tab hasn't loaded), and optionally
consider the alternative approach of subscribing to the tabs slice (instead of
using getState) so the effect re-runs when tabs are populated; target symbols:
useEffect, useTabsStore.getState, state.tabs.find, state.setActiveTab,
state.setFocusedPane, routeNavigate, searchTabId, searchPaneId.

Electron Notification objects get garbage collected on macOS while the
native notification is still visible, silently dropping click handlers.
Extract notification lifecycle into a testable NotificationManager that
retains references, replaces per-pane, and sweeps stale entries on a TTL.
@Kitenite Kitenite force-pushed the kitenite/clicking-on-notifs-should-navigate-to-pane branch from 596d670 to 6cdde6d Compare February 9, 2026 22:11
@Kitenite Kitenite merged commit adb4fb8 into main Feb 9, 2026
@Kitenite Kitenite deleted the kitenite/clicking-on-notifs-should-navigate-to-pane branch February 9, 2026 22:11
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 9, 2026

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ⚠️ Neon database branch
  • ⚠️ Electric Fly.io app

Thank you for your contribution! 🎉

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@apps/desktop/src/main/lib/notifications/notification-manager.ts`:
- Around line 103-109: The dispose() method currently clears the sweep timer and
the active map but does not close native OS notifications, which can leave click
handlers (e.g., the onNotificationClick closure in main.ts) pointing at a
destroyed BrowserWindow; update NotificationManager.dispose() to iterate over
this.active (or this.active.values()) and call close() on each native
Notification object before clearing the map, then clear the interval and null
the sweepTimer as before so lingering notifications cannot trigger
window.show()/focus() on a destroyed window.
🧹 Nitpick comments (2)
apps/desktop/src/main/lib/notifications/notification-manager.ts (1)

140-147: sweep() removes stale entries without closing the native notification.

Same pattern as dispose() — the notification object loses its strong reference but the OS notification may still be visible. Closing it explicitly is more defensive:

Proposed fix
 private sweep(): void {
   const now = Date.now();
   for (const [key, entry] of this.active) {
     if (now - entry.createdAt > NOTIFICATION_TTL_MS) {
+      entry.notification.close();
       this.active.delete(key);
     }
   }
 }
apps/desktop/src/main/lib/notifications/notification-manager.test.ts (1)

30-33: clickedIds typed as AgentLifecycleEvent[] but receives NotificationIds.

The onNotificationClick callback receives NotificationIds (which lacks eventType), but the test stores it as AgentLifecycleEvent via an as cast on line 51. The test assertion on line 157-159 happens to pass because it only checks the overlapping fields, but this masks a type mismatch. Use NotificationIds[] instead:

Proposed fix
+import type { NotificationIds } from "shared/notification-types";
+
 interface TestDeps extends NotificationManagerDeps {
   notifications: MockNotification[];
-  clickedIds: AgentLifecycleEvent[];
+  clickedIds: NotificationIds[];
 }

And on line 39/51:

-  const clickedIds: AgentLifecycleEvent[] = [];
+  const clickedIds: NotificationIds[] = [];
   ...
-  onNotificationClick: (ids) => clickedIds.push(ids as AgentLifecycleEvent),
+  onNotificationClick: (ids) => clickedIds.push(ids),

Comment on lines +103 to +109
dispose(): void {
if (this.sweepTimer) {
clearInterval(this.sweepTimer);
this.sweepTimer = null;
}
this.active.clear();
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

dispose() clears tracking but does not close the native notifications.

Active OS notifications will remain visible after the window closes, but their click handlers capture the now-destroyed BrowserWindow (via the onNotificationClick closure in main.ts). If a user clicks a lingering notification before GC collects it, calling window.show()/window.focus() on a destroyed window will throw.

Consider closing each notification before clearing the map:

Proposed fix
 dispose(): void {
   if (this.sweepTimer) {
     clearInterval(this.sweepTimer);
     this.sweepTimer = null;
   }
+  for (const entry of this.active.values()) {
+    entry.notification.close();
+  }
   this.active.clear();
 }
🤖 Prompt for AI Agents
In `@apps/desktop/src/main/lib/notifications/notification-manager.ts` around lines
103 - 109, The dispose() method currently clears the sweep timer and the active
map but does not close native OS notifications, which can leave click handlers
(e.g., the onNotificationClick closure in main.ts) pointing at a destroyed
BrowserWindow; update NotificationManager.dispose() to iterate over this.active
(or this.active.values()) and call close() on each native Notification object
before clearing the map, then clear the interval and null the sweepTimer as
before so lingering notifications cannot trigger window.show()/focus() on a
destroyed window.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant