fix(desktop): stop leaked chat sessions from reopening as new tabs

Merged
AV
10d ago
+188-15

What & why

Clicking + → Chat could open a burst of extra chat tabs, each showing only "No earlier conversation to open, so this is a new one." Closing them could bring more back until they ran out. The "+" button is not the cause. It only triggers the refresh that shows sessions that leaked earlier.

How the leak happens:

  1. A chat whose host session stopped (for example after an update restarts the host service) recovers by creating a replacement session. The pane takes it over only once the agent has started, a few seconds later.
  2. Only the active tab is mounted. Switching away in that window unmounts AcpChatPane and drops the replacement, but the session stays live on the host under the pane's terminalId. Each later visit to that tab leaks one more.
  3. useAutoAdoptChatSessions counts a session as attached when a pane has the same terminalId, so the leaks stay hidden while the pane exists.
  4. Closing the pane marks only its own (dead) session closed. The next chat:sessions-changed event (a new chat from "+", or another chat being stopped) refetches listSessions, and auto-adopt opens every leaked session as a new tab. Each one was created by resuming a conversation that was never prompted, so its transcript is that notice.

What this changes:

  • At the source: a new useReplacementSession hook owns the pending replacement and closes it if the pane unmounts before it takes the replacement over. The next visit recovers again.
  • On close: closing a chat marks its terminalId closed, so auto-adopt skips it, and stops the live sessions on that terminal that no remaining pane holds (findStrayChatSessions). This also clears leaks that hosts already have. The chat's own session keeps its 10 s undo window, and Undo reopens the terminal too.

Why close the replacement instead of handing it to the pane on unmount: panes are not keyed, so one ChatPane instance serves every chat tab. At unmount, its latest-data ref can already hold another tab's data, and writing the replacement then could put it on the wrong pane.

How I tested it

I reproduced the bug in the dev app over CDP first, then ran the same steps with the fix. After each step I read the live sessions from the dev host's listSessions.

ScenarioBeforeAfter
Two chats; stop chat A's host session; open A and switch away within 800 ms, three times3 live sessions leaked on A's terminalnone
Then close A and click + → Chat3 extra tabs, each showing the noticeonly the new chat
Stop a session and stay on its tab—recovery still takes over the replacement (one live session per tab)
Put 2 extra sessions on a tab's terminal through the host, then close the tab—both stopped within 3 s; the next + → Chat opens no extra tabs; the tab's own session stops after the undo window
Close a chat, then click Undo—the same tab and session come back and are not stopped later

Automated, in apps/desktop:

  • bun test src/renderer/routes/_authenticated/_dashboard/v2-workspace: 909 pass, 0 fail
  • New: useReplacementSession.test.ts (it fails if the unmount cleanup is removed) and findStrayChatSessions.test.ts
  • bun run lint (repo) and bun run typecheck (desktop) pass

Not tested: a packaged build. No screenshots attached; the before/after results are in the table above.

Note: closing a chat now makes one listSessions call to find stray sessions. If that call fails, the close works as before and the failure is logged.

Checklist

  • PR title follows conventional commits (type(scope): subject)
  • bun run lint and bun run typecheck pass (CI fails on lint warnings too)
  • "Allow edits from maintainers" is checked on fork PRs (not a fork PR)
  • I have signed the CLA (the bot asks on your first PR)

Summary by CodeRabbit

  • Bug Fixes
    • Closed chat sessions are less likely to reappear automatically in a workspace.
    • Closing a chat better cleans up unused sessions while preserving sessions still in use by other panes.
    • Undoing a chat close restores both the chat session and its terminal.
    • Opening a chat session reopens its terminal when needed.
    • Replacement chat sessions are cleaned up more reliably when recovery is interrupted or the replacement session has ended.