fix(desktop): dismiss file preview tabs when switching conversations (#39657) - #39712
fix(desktop): dismiss file preview tabs when switching conversations (#39657)#39712maxmilian wants to merge 1 commit into
Conversation
111797c to
c4759ce
Compare
c4759ce to
5d55be2
Compare
5d55be2 to
0535408
Compare
teknium1
left a comment
There was a problem hiding this comment.
Thanks for isolating the state half of #39657; current main does retain file tabs globally (apps/desktop/src/store/preview.ts:147-155) while usePreviewRouting only re-scopes the live target (apps/desktop/src/app/session/hooks/use-preview-routing.ts:58-68).
Problems
- The new routing call syncs from
currentPreviewSessionId(), but the hook chooses its visible session through a different precedence chain:selectedStoredSessionId || routedSessionId || activeSessionIdRef.current(use-preview-routing.ts:34-40;preview.ts:292-294). PasspreviewSessionIdinto the sync function and cover divergent route/runtime IDs; the current test changes both together. sessionIdis required by the newFilePreviewTab, but the persisted-tab validator still accepts legacy{ id, target }rows (preview.ts:197-205). Version/migrate the storage or reject legacy rows explicitly, with a decode regression test.
Suggested changes
- Scope sync to the hook's resolved session identity.
- Define and test the localStorage upgrade behavior.
Automated hermes-sweeper review.
| @@ -62,6 +63,8 @@ export function usePreviewRouting({ | |||
| return | |||
| } | |||
|
|
|||
There was a problem hiding this comment.
Pass previewSessionId into this sync operation. The hook resolves it as selectedStoredSessionId || routedSessionId || activeSessionIdRef.current, whereas the new helper reads only global selected/active atoms; a route/runtime mismatch can retain tabs for the wrong session. Add a regression test with divergent IDs.
| @@ -57,6 +57,7 @@ type SessionPreviewRegistry = Record<string, SessionPreviewRecord[]> | |||
|
|
|||
| export interface FilePreviewTab { | |||
| id: `file:${string}` | |||
| sessionId: string | |||
There was a problem hiding this comment.
This makes sessionId mandatory, but the persisted-tab decoder's isFilePreviewTab guard is unchanged and still accepts legacy { id, target } rows. Version/migrate the storage or reject rows without a string sessionId, and test the selected upgrade behavior.
0535408 to
c3fa19c
Compare
|
Thanks for the review — both problems were real. Addressed on the new head (also rebased onto latest 1. Sync now scopes to the routing identity, not a re-derived one. 2. Legacy/blank rows rejected on decode. Verification: |
…ousResearch#39657) File preview tabs (`$filePreviewTabs`) were a flat global atom with no session association, so opening an image/file preview in one conversation left the tab visible after switching to a conversation that does not contain that attachment. Unlike `$previewTarget` — which the routing effect re-scopes per session via the preview registry — file tabs were never reset on a session change. Tag each `FilePreviewTab` with the session it was opened under and add `syncFilePreviewTabsForSession(sessionId)`, which the session-routing effect calls to drop tabs that don't belong to the conversation being restored. The sync scopes tabs to the id the effect passes it — `previewSessionId`, the exact id it routes with — rather than re-deriving `currentPreviewSessionId()` internally, so the filter can't diverge from the routed session when the runtime session id lags a mid-switch route. Syncing with the tab's own session id is a no-op, so the effect can call it on every registry update without clobbering same-session tabs. `sessionId` is now required on `FilePreviewTab`, so the persisted-tab decoder rejects legacy `{ id, target }` rows (and blank session ids) that predate scoping instead of restoring unattributable tabs; the decode step is extracted as `decodeFilePreviewTabs` to be tested directly. Tests: store tests cover dropping other-session tabs, the same-session no-op, argument-scoped filtering, and rejecting legacy/blank rows on decode; a routing test proves the tab is scoped to the routed session even when the runtime session store diverges. All fail without the corresponding fix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
c3fa19c to
30e71d3
Compare
|
Closing this — the premise it was built on no longer holds on #72963 ("Unify the preview rail onto one tab list", merged 2026-07-27) rewrote the rail.
So this isn't upstream drift to rebase past. This PR argued that a file tab surviving a conversation switch was a bug; the rail's design has since answered that question the other way, deliberately. Rebasing it would mean re-litigating a design decision through a merge conflict, which isn't what a bugfix PR is for. Worth saying that #72963 also fixed something real in this area that I had read as a separate concern — the set-then-immediately-cleared pane flash caused by the tab list and the session-keyed registry disagreeing about which session id was current. Collapsing the two lists was a better cut than scoping one of them harder, which is what this PR did. The half of #39657 that remains genuinely open is the one I scoped out of this PR originally: the image preview tab / title bar overlapping the top-right window controls. I've left a note on the issue so it doesn't read as partly fixed. |
What does this PR do?
Fixes the cross-conversation persistence half of #39657: opening an image/file preview in one conversation left the preview tab visible after switching to a different conversation that does not contain that attachment.
Related Issue
Addresses #39657 (the persistence issue). The top-right overlap with the window controls reported in the same issue is a separate titlebar/layout concern and is intentionally left as a follow-up — see Scope below.
Root cause
File preview tabs live in
$filePreviewTabs(apps/desktop/src/store/preview.ts), a flat global atom whoseFilePreviewTabrecords carried no session association. The live preview ($previewTarget) is already re-scoped per session —usePreviewRouting's effect reads the per-session$sessionPreviewRegistryon every session change and resets the target. But$filePreviewTabswas never touched on a session change, so a file tab opened in session A stayed mounted when you switched to session B.Clicking an image/file attachment routes through
setCurrentSessionPreviewTarget(..., 'manual', ...)→tryOpenFilePreview→$filePreviewTabs, which is exactly the leaking path.Fix
FilePreviewTabwith thesessionIdit was opened under (threaded throughtryOpenFilePreview/openFilePreviewTargetfrom thesessionIdthe caller already passes).syncFilePreviewTabsForSession(sessionId): keeps only tabs belonging to the given session, and re-selects the live-preview tab if the active file tab was dropped. Re-syncing the current session is a no-op, so it's safe to call on every render.usePreviewRouting's session effect now calls it alongside the existing$previewTargetreset — clearing other-session tabs on a real session switch, without clobbering same-session tabs when the effect re-runs on registry updates.Net production change is ~8 lines of logic; no new dependencies, no behavior change for the same-session case.
How to Test
Automated coverage (
npm run test:ui, run fromapps/desktop):store/preview.test.ts— drops other-session tabs on switch; keeps same-session tabs on re-sync.app/session/hooks/use-preview-routing.test.tsx— file tab is dismissed when the routed session changes. This test fails without the effect-level call, so it pins the regression.All preview/routing tests green;
tsc -bandeslintclean. (Pre-existing unrelated failures inmodel-settings/toolset/skills/streaming/pane-shellsuites reproduce on a cleanmaincheckout too.)Scope — intentionally not changed
app/shell--titlebar-tools-right/--titlebar-tools-widthreservations), not a state bug. It needs a packaged Desktop build to repro and verify a CSS fix with before/after evidence, so it's deliberately out of scope here to keep this PR a small, fully unit-tested state fix. Happy to follow up with a dedicated layout PR.$previewTarget) behavior is unchanged — it was already correctly per-session.Type of Change