From 416e025c46c6d778316f411616a2f76db01563b6 Mon Sep 17 00:00:00 2001 From: bb Date: Fri, 7 Aug 2026 13:06:47 -0500 Subject: [PATCH] fix(desktop): rebind the Files pane cwd when switching sessions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two defects left the Files pane showing the previous project's tree: - `applyStoredSessionPreviewRuntimeInfo` reset every composer atom EXCEPT cwd, and runs before the `session.resume` RPC. The sidebar row already knows the conversation's workspace (`cwd` is in the compact row projection), so mirror it on the same tick the selection changes. - `if (info.cwd)` was truthy-only, so a detached session reporting `cwd: ''` never cleared and the pane stayed pinned to the last project for the rest of the session — the "not always" in the report. Empty is now authoritative. Empty routes through ownership release rather than a persisted `''`: `setCurrentCwd` writes to localStorage and seeds `$currentCwd` on next boot, so blanking would also wipe the remembered workspace. Only `cwd` is consulted, never `git_repo_root` — the latter is documented null for non-git workspaces and not-yet-backfilled rows, so falling back to it reads as "no workspace" and blanks a pane that was correct. A session outside the loaded sidebar page (no row at all) releases ownership instead of blanking, for the same reason. Also claims ownership on the warm-cache path (its missing-RPC compat branch returns before `applyRuntimeInfo`) and for a center tile, whose Project "+" create left the right rail on the previous session's folder. Co-authored-by: xxxigm Co-authored-by: worlldz Co-authored-by: ZHJay --- .../hooks/use-session-actions/index.ts | 28 +++++++++- .../hooks/use-session-actions/utils.ts | 56 ++++++++++++++++++- 2 files changed, 78 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/app/session/hooks/use-session-actions/index.ts b/apps/desktop/src/app/session/hooks/use-session-actions/index.ts index 543074f0265f1..8bb8029caac31 100644 --- a/apps/desktop/src/app/session/hooks/use-session-actions/index.ts +++ b/apps/desktop/src/app/session/hooks/use-session-actions/index.ts @@ -54,6 +54,7 @@ import { setSessions, setSessionStartedAt, setTurnStartedAt, + setWorkspaceCwdOwner, setYoloActive } from '@/store/session' import { @@ -350,6 +351,11 @@ export function useSessionActions({ setCurrentCwd(workspaceTarget) } + // A fresh draft resolves its own workspace right here, so it owns it. The + // selected stored id is null for a draft, and so is the owner — they match, + // which keeps workspace surfaces live on a new chat instead of treating the + // draft as an un-re-homed switch (#71254). + setWorkspaceCwdOwner(null) setCurrentBranch('') // Never clear the composer here — ChatBar's per-thread draft swap owns it. setFreshDraftReady(true) @@ -514,13 +520,23 @@ export function useSessionActions({ upsertOptimisticSession(created, stored, null, null) } - // A tile lives in its OWN worktree — it must not publish its cwd/branch - // into the composer atoms the main pane renders from. + // A tile lives in its OWN worktree, so it must not run the full + // foreground composer publish. A CENTER tile is the focused surface, + // though, and the Files pane still keys off the global `$currentCwd` — + // so the right rail kept showing the previous session's tree when a + // Project "+" created a session while the main chat was occupied + // (#76696). Split/side tiles deliberately stay isolated. const runtimeInfo = applyRuntimeInfo(created.info, { foreground: false }) updateSessionState(created.session_id, state => (runtimeInfo ? { ...state, ...runtimeInfo } : state), stored) openSessionTile(stored, dir) patchSessionTile(stored, { runtimeId: created.session_id }) + + if (dir === 'center' && runtimeInfo?.cwd) { + setCurrentCwd(runtimeInfo.cwd) + setWorkspaceCwdOwner(stored) + } + revealTreePane(`session-tile:${stored}`) if (listed) { @@ -701,6 +717,12 @@ export function useSessionActions({ activeSessionIdRef.current = cachedRuntimeId syncSessionStateToView(cachedRuntimeId, cachedViewState) setCurrentCwd(cachedViewState.cwd) + // The warm cache IS this conversation's own workspace truth, so the + // switch is already re-homed here. This claim cannot wait for + // `session.activate`: its missing-RPC compat branch returns before + // `applyRuntimeInfo` runs, which would leave the workspace marked + // un-owned for the life of the session (#71254). + setWorkspaceCwdOwner(storedSessionId) setCurrentBranch(cachedViewState.branch) setSessionStartedAt(Date.now()) @@ -844,7 +866,7 @@ export function useSessionActions({ const stored = $sessions.get().find(session => sessionMatchesStoredId(session, storedSessionId)) ?? storedForProfile - applyStoredSessionPreviewRuntimeInfo(stored) + applyStoredSessionPreviewRuntimeInfo(stored, storedSessionId) if (stored) { applyStoredUsage(stored) diff --git a/apps/desktop/src/app/session/hooks/use-session-actions/utils.ts b/apps/desktop/src/app/session/hooks/use-session-actions/utils.ts index b6bc4ae6d5b2f..4648ca5ff1155 100644 --- a/apps/desktop/src/app/session/hooks/use-session-actions/utils.ts +++ b/apps/desktop/src/app/session/hooks/use-session-actions/utils.ts @@ -9,6 +9,8 @@ import { $activeGatewayProfile, $profiles, normalizeProfileKey } from '@/store/p import { $currentCwd, $sessions, + commitWorkspaceCwdForSelectedSession, + releaseWorkspaceCwdOwner, sessionMatchesStoredId, setCurrentBranch, setCurrentCwd, @@ -20,6 +22,7 @@ import { setCurrentServiceTier, setCurrentUsage, setSessions, + setWorkspaceCwdOwner, setYoloActive } from '@/store/session' @@ -959,7 +962,17 @@ function publishRuntimeToComposer(state: SessionRuntimeStatePatch): void { } if (state.cwd !== undefined) { - setCurrentCwd(state.cwd) + if (state.cwd) { + // The runtime named a real folder for the session in the main pane, so + // that conversation owns the path. + commitWorkspaceCwdForSelectedSession(state.cwd) + } else { + // A detached session: the path on screen is provably still the previous + // conversation's. Release rather than write `''` — `setCurrentCwd` + // persists, so blanking here would also wipe the remembered workspace + // that seeds `$currentCwd` on next boot. + releaseWorkspaceCwdOwner() + } } if (state.branch !== undefined) { @@ -1017,7 +1030,12 @@ export function applyRuntimeInfo( sessionState.provider = info.provider } - if (info.cwd) { + // Empty string is authoritative, not "no opinion": a detached/bare session + // reports `cwd: ''`, and the truthy-only test left `$currentCwd` — and so the + // Files pane — pinned to the PREVIOUS project for the rest of the session + // (#71254). Empty is routed through ownership release below rather than + // persisted, so the pane hides a path it no longer owns instead of blanking. + if (typeof info.cwd === 'string') { sessionState.cwd = info.cwd } @@ -1056,7 +1074,10 @@ export function applyRuntimeInfo( return sessionState } -export function applyStoredSessionPreviewRuntimeInfo(stored: { model?: null | string } | undefined) { +export function applyStoredSessionPreviewRuntimeInfo( + stored: { cwd?: null | string; model?: null | string } | undefined, + storedSessionId: null | string +) { setCurrentModel(stored?.model || '') setCurrentProvider('') setCurrentReasoningEffort('') @@ -1064,6 +1085,35 @@ export function applyStoredSessionPreviewRuntimeInfo(stored: { model?: null | st setCurrentFastMode(false) setYoloActive(false) setCurrentPersonality('') + + // Cold resume paints the transcript before `session.resume` returns, so + // without this the Files pane shows the PREVIOUS project's tree for the whole + // round-trip (#71254 / #76696). The sidebar row already knows this + // conversation's workspace — `cwd` is part of the compact row projection — so + // mirror it on the same tick the selection changes. + // + // Only `cwd` is consulted. `git_repo_root` is documented as null for non-git + // workspaces and not-yet-backfilled history rows, so falling back to it would + // read as "no workspace" for those sessions and blank a pane that was correct. + const storedCwd = stored?.cwd?.trim() || '' + + if (storedCwd) { + setCurrentCwd(storedCwd) + setWorkspaceCwdOwner(storedSessionId) + } else { + // Either a genuinely detached session, or a row outside the loaded sidebar + // page (`stored` is undefined) — neither says anything about the workspace, + // while `$currentCwd` still holds the previous conversation's folder. + // Release so workspace-derived surfaces stop trusting it; `applyRuntimeInfo` + // publishes the truth a moment later. The path is deliberately left in place + // — clearing it collapses the workspace/review panes and drops file-tree + // state on every switch. + releaseWorkspaceCwdOwner() + } + + // Same window, same reasoning: the branch is derived from the workspace, so + // carrying the previous conversation's label across a switch is never right. + setCurrentBranch('') } // A "session genuinely doesn't exist" failure (deleted, or an id from a wiped /