fix(desktop): stop leaked chat sessions from reopening as new tabs
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:
- 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.
- Only the active tab is mounted. Switching away in that window unmounts
AcpChatPaneand drops the replacement, but the session stays live on the host under the pane'sterminalId. Each later visit to that tab leaks one more. useAutoAdoptChatSessionscounts a session as attached when a pane has the sameterminalId, so the leaks stay hidden while the pane exists.- Closing the pane marks only its own (dead) session closed. The next
chat:sessions-changedevent (a new chat from "+", or another chat being stopped) refetcheslistSessions, 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
useReplacementSessionhook 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
terminalIdclosed, 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.
| Scenario | Before | After |
|---|---|---|
| Two chats; stop chat A's host session; open A and switch away within 800 ms, three times | 3 live sessions leaked on A's terminal | none |
| Then close A and click + → Chat | 3 extra tabs, each showing the notice | only 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) andfindStrayChatSessions.test.ts bun run lint(repo) andbun 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 lintandbun run typecheckpass (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.