From fb9b346c6db0f1afe96fa1681037174c004c34b9 Mon Sep 17 00:00:00 2001 From: Julius Olsson Date: Thu, 14 May 2026 10:59:04 +0200 Subject: [PATCH] fix(workspace): Bury Pane targets the visible Dispatch row MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit requestBuryFocused was the last grid-only action surfaced by issue #94's audit. It read tab.focusedSessionId directly, so "Bury Pane" from Dispatch Mode buried whichever grid pane happened to be focused underneath the visible dispatch row instead of acting on the row the user could see. Resolves the target through commandTargetSessionIdForState — the same resolver every other "act on the visible thing" command already uses (close, copy-assistant, scroll-to-latest, switch-provider, reload, rewind, soft-reload-view). Also adds a WHY comment on buryFocused's `?? activeTab?.focusedSessionId` fallback to flag it as intentional defensive code rather than a load-bearing path — every real caller passes an explicit id, but the fallback exists so a future caller that forgets fails loudly instead of silently dispatching to the wrong session. After this commit, the action-layer half of #94 is closed: every "focused" action either routes through commandTargetSessionIdForState or is intentionally grid-only by design (split, navigate, resize, detach-from-grid, undo-close). Closes #94. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/workspace/hook/actions/pane.ts | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/renderer/src/workspace/hook/actions/pane.ts b/src/renderer/src/workspace/hook/actions/pane.ts index 96fa13fc5..4966e8085 100644 --- a/src/renderer/src/workspace/hook/actions/pane.ts +++ b/src/renderer/src/workspace/hook/actions/pane.ts @@ -31,6 +31,7 @@ import { flattenDispatchRows, selectVisibleDispatchRow, } from '@renderer/workspace/dispatch/dispatchSelectors' +import { commandTargetSessionIdForState } from '@renderer/workspace/hook/selectors/commandTargetSessionId' import type { PlacementTarget } from '@renderer/features/workspace/lib/newAgentPlacement' import type { @@ -867,12 +868,21 @@ export function usePaneActions( // Bury: remove the focused pane from the visible layout without // killing the underlying session. The session keeps running in // the background and remains eligible for revive. + // + // WHY commandTargetSessionIdForState instead of tab.focusedSessionId: + // tab.focusedSessionId has a "must be a leaf in tab.root" invariant — + // it's grid-only. In Dispatch Mode the user has a row selected, not + // a grid focus, and reading tab.focusedSessionId silently opens the + // bury prompt on whatever grid pane is focused underneath the + // visible dispatch row — exactly the bug class issue #94 tracks. + // Routing through commandTargetSessionIdForState makes Bury agree + // with every other "act on the visible thing" command (close, + // copy-assistant, scroll-to-latest, switch-provider, reload, rewind, + // soft-reload-view — all already use this resolver). const requestBuryFocused = useCallback(() => { - const tab = refs.stateRef.current.tabs.find( - t => t.id === refs.stateRef.current.activeTabId, - ) - if (!tab) return - openBuryPrompt(tab.focusedSessionId) + const sessionId = commandTargetSessionIdForState(refs.stateRef.current) + if (!sessionId) return + openBuryPrompt(sessionId) }, [openBuryPrompt, refs.stateRef]) const buryFocused = useCallback( @@ -886,6 +896,16 @@ export function usePaneActions( // Resolve the owning tab from the target session instead. const snapshot = refs.stateRef.current const activeTab = snapshot.tabs.find(t => t.id === snapshot.activeTabId) + // The `?? activeTab?.focusedSessionId` fallback is intentionally + // defensive belt-and-suspenders: every current caller passes an + // explicit `targetSessionId` (the bury-prompt modal in App.tsx + // owns the resolved id at confirm time; requestBuryFocused + // resolves it via commandTargetSessionIdForState before opening + // the prompt). The fallback exists so a future caller that + // forgets to pass an id doesn't no-op silently — but it MUST + // NOT become the primary path, because activeTab.focusedSessionId + // is grid-only and would re-introduce the Dispatch-misses-target + // bug from #94. const targetId = targetSessionId ?? activeTab?.focusedSessionId if (!targetId) return