From ce905d948321862f91cb727884fcb1cfb4c5e3d4 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 19 Aug 2026 14:33:23 +0200 Subject: [PATCH] fix(app): open new tabs in the focused pane Preserve ambient placement for reconciliation and background opens while routing user-directed tab creation through the focused visible pane. Defer terminal reconciliation during creates so a push cannot preempt the intended pane placement. --- ...t-terminal-shortcut-pane-placement.spec.ts | 33 +++++++ .../command-center/workspace-file-search.ts | 2 +- .../compact-explorer-sidebar-host.tsx | 8 +- packages/app/src/git/diff-pane.tsx | 12 ++- .../workspace/workspace-file-open-command.ts | 7 +- .../screens/workspace/workspace-screen.tsx | 94 +++++++++++++----- .../src/stores/workspace-layout-actions.ts | 24 +++-- .../src/stores/workspace-layout-store.test.ts | 99 +++++++++++++++++++ .../app/src/stores/workspace-layout-store.ts | 34 ++++++- .../workspace-subagents-integration.test.ts | 1 + .../workspace-tabs/agent-visibility.test.ts | 2 + .../src/workspace-tabs/agent-visibility.ts | 2 + 12 files changed, 267 insertions(+), 51 deletions(-) create mode 100644 packages/app/e2e/browser/direct-terminal-shortcut-pane-placement.spec.ts diff --git a/packages/app/e2e/browser/direct-terminal-shortcut-pane-placement.spec.ts b/packages/app/e2e/browser/direct-terminal-shortcut-pane-placement.spec.ts new file mode 100644 index 000000000..6735705e2 --- /dev/null +++ b/packages/app/e2e/browser/direct-terminal-shortcut-pane-placement.spec.ts @@ -0,0 +1,33 @@ +import { expect, test } from "../support/fixtures"; +import { gotoWorkspace, pressDirectNewTabShortcut } from "../support/helpers/launcher"; +import { seedWorkspace } from "../support/helpers/seed-client"; +import { openChangesPanel, waitForWorkspaceTabsVisible } from "../support/helpers/workspace-tabs"; + +test.describe("Direct terminal shortcut pane placement", () => { + test("opens a terminal in the focused pane", async ({ page }) => { + const workspace = await seedWorkspace({ repoPrefix: "direct-terminal-shortcut-pane-" }); + + try { + await gotoWorkspace(page, workspace.workspaceId); + await waitForWorkspaceTabsVisible(page); + + await openChangesPanel(page); + const focusedPane = page + .getByTestId("workspace-tabs-row") + .filter({ has: page.getByTestId("workspace-tab-working_diff") }) + .first(); + const primaryPane = page + .getByTestId("workspace-tabs-row") + .filter({ hasNot: page.getByTestId("workspace-tab-working_diff") }) + .first(); + await pressDirectNewTabShortcut(page, "t"); + + await expect(focusedPane.locator('[data-testid^="workspace-tab-terminal_"]')).toHaveCount(1, { + timeout: 30_000, + }); + await expect(primaryPane.locator('[data-testid^="workspace-tab-terminal_"]')).toHaveCount(0); + } finally { + await workspace.cleanup(); + } + }); +}); diff --git a/packages/app/src/command-center/workspace-file-search.ts b/packages/app/src/command-center/workspace-file-search.ts index 7a536159b..d705af869 100644 --- a/packages/app/src/command-center/workspace-file-search.ts +++ b/packages/app/src/command-center/workspace-file-search.ts @@ -132,7 +132,7 @@ export function useWorkspaceFileSearch(input: { enabled: boolean; query: string filePath: path, persistenceKey: buildWorkspaceTabPersistenceKey({ serverId, workspaceId }), showMobileAgent: usePanelStore.getState().showMobileAgent, - openWorkspaceTabFocused: useWorkspaceLayoutStore.getState().openTabFocused, + openWorkspaceTabInFocusedPane: useWorkspaceLayoutStore.getState().openTabInFocusedPane, focusWorkspaceTab: useWorkspaceLayoutStore.getState().focusTab, }); }, diff --git a/packages/app/src/components/compact-explorer-sidebar-host.tsx b/packages/app/src/components/compact-explorer-sidebar-host.tsx index f51f827fa..6c5db7774 100644 --- a/packages/app/src/components/compact-explorer-sidebar-host.tsx +++ b/packages/app/src/components/compact-explorer-sidebar-host.tsx @@ -101,7 +101,9 @@ export function CompactExplorerSidebarHost({ children, enabled }: CompactExplore const model = useActiveCompactExplorerSidebarModel(enabled); const openFileExplorerForCheckout = usePanelStore((state) => state.openFileExplorerForCheckout); const showMobileAgent = usePanelStore((state) => state.showMobileAgent); - const openWorkspaceTabFocused = useWorkspaceLayoutStore((state) => state.openTabFocused); + const openWorkspaceTabInFocusedPane = useWorkspaceLayoutStore( + (state) => state.openTabInFocusedPane, + ); const focusWorkspaceTab = useWorkspaceLayoutStore((state) => state.focusTab); const handleOpenExplorer = useCallback(() => { @@ -127,11 +129,11 @@ export function CompactExplorerSidebarHost({ children, enabled }: CompactExplore filePath, persistenceKey: model.persistenceKey, showMobileAgent, - openWorkspaceTabFocused, + openWorkspaceTabInFocusedPane, focusWorkspaceTab, }); }, - [focusWorkspaceTab, model, openWorkspaceTabFocused, showMobileAgent], + [focusWorkspaceTab, model, openWorkspaceTabInFocusedPane, showMobileAgent], ); return ( diff --git a/packages/app/src/git/diff-pane.tsx b/packages/app/src/git/diff-pane.tsx index bfaf570ff..b27f268c4 100644 --- a/packages/app/src/git/diff-pane.tsx +++ b/packages/app/src/git/diff-pane.tsx @@ -1019,7 +1019,9 @@ function useDiffTabNavigation({ cwd: string; isMobile: boolean; }) { - const openWorkspaceTabFocused = useWorkspaceLayoutStore((state) => state.openTabFocused); + const openWorkspaceTabInFocusedPane = useWorkspaceLayoutStore( + (state) => state.openTabInFocusedPane, + ); const closeWorkspaceTab = useWorkspaceLayoutStore((state) => state.closeTab); const persistenceKey = useMemo( () => buildWorkspaceTabPersistenceKey({ serverId, workspaceId: workspaceId ?? cwd }), @@ -1040,12 +1042,12 @@ function useDiffTabNavigation({ if (!persistenceKey || isMobile) { return; } - openWorkspaceTabFocused(persistenceKey, { + openWorkspaceTabInFocusedPane(persistenceKey, { kind: "working_diff", ...(path ? { focusPath: path, focusRequestId: Date.now() } : {}), }); }, - [isMobile, openWorkspaceTabFocused, persistenceKey], + [isMobile, openWorkspaceTabInFocusedPane, persistenceKey], ); const toggleChanges = useCallback(() => { if (!persistenceKey || isMobile) { @@ -1060,10 +1062,10 @@ function useDiffTabNavigation({ const openCommit = useCallback( (sha: string) => { if (persistenceKey) { - openWorkspaceTabFocused(persistenceKey, { kind: "commit_diff", sha }); + openWorkspaceTabInFocusedPane(persistenceKey, { kind: "commit_diff", sha }); } }, - [openWorkspaceTabFocused, persistenceKey], + [openWorkspaceTabInFocusedPane, persistenceKey], ); return { changesTabOpen, diff --git a/packages/app/src/screens/workspace/workspace-file-open-command.ts b/packages/app/src/screens/workspace/workspace-file-open-command.ts index e6af1348b..c3db103a6 100644 --- a/packages/app/src/screens/workspace/workspace-file-open-command.ts +++ b/packages/app/src/screens/workspace/workspace-file-open-command.ts @@ -8,7 +8,10 @@ interface OpenWorkspaceFileFromExplorerInput { filePath: string; persistenceKey: string | null; showMobileAgent: () => void; - openWorkspaceTabFocused: (workspaceKey: string, target: WorkspaceTabTarget) => string | null; + openWorkspaceTabInFocusedPane: ( + workspaceKey: string, + target: WorkspaceTabTarget, + ) => string | null; focusWorkspaceTab: (workspaceKey: string, tabId: string) => void; } @@ -21,7 +24,7 @@ export function openWorkspaceFileFromExplorer(input: OpenWorkspaceFileFromExplor if (!location) { return; } - const tabId = input.openWorkspaceTabFocused( + const tabId = input.openWorkspaceTabInFocusedPane( input.persistenceKey, createWorkspaceFileTabTarget(location), ); diff --git a/packages/app/src/screens/workspace/workspace-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx index a41e9aa16..169bcc0bb 100644 --- a/packages/app/src/screens/workspace/workspace-screen.tsx +++ b/packages/app/src/screens/workspace/workspace-screen.tsx @@ -1466,6 +1466,10 @@ interface WorkspaceTerminalTabActionsInput { target: WorkspaceTabTarget, options?: WorkspaceTabOpenOptions, ) => string | null; + openWorkspaceTabInFocusedPane: ( + workspaceKey: string, + target: WorkspaceTabTarget, + ) => string | null; labels: { workspacePathUnavailable: string; terminalQueued: string; @@ -1487,6 +1491,7 @@ interface WorkspaceTerminalTabActions { function useWorkspaceTerminalTabActions({ persistenceKey, openWorkspaceTabFocused, + openWorkspaceTabInFocusedPane, labels, toast, }: WorkspaceTerminalTabActionsInput): WorkspaceTerminalTabActions { @@ -1495,18 +1500,22 @@ function useWorkspaceTerminalTabActions({ if (!persistenceKey) { return; } - openWorkspaceTabFocused(persistenceKey, { kind: "terminal", terminalId }, { paneId }); + if (paneId) { + openWorkspaceTabFocused(persistenceKey, { kind: "terminal", terminalId }, { paneId }); + } else { + openWorkspaceTabInFocusedPane(persistenceKey, { kind: "terminal", terminalId }); + } }, - [openWorkspaceTabFocused, persistenceKey], + [openWorkspaceTabFocused, openWorkspaceTabInFocusedPane, persistenceKey], ); const handleScriptTerminalSelected = useCallback( (terminalId: string) => { if (!persistenceKey) { return; } - openWorkspaceTabFocused(persistenceKey, { kind: "terminal", terminalId }); + openWorkspaceTabInFocusedPane(persistenceKey, { kind: "terminal", terminalId }); }, - [openWorkspaceTabFocused, persistenceKey], + [openWorkspaceTabInFocusedPane, persistenceKey], ); const handleWorkspacePathUnavailable = useCallback(() => { toast.error(labels.workspacePathUnavailable); @@ -1608,9 +1617,15 @@ function WorkspaceScreenContent({ [normalizedServerId, normalizedWorkspaceId], ); const openWorkspaceTabFocused = useWorkspaceLayoutStore((state) => state.openTabFocused); + const openWorkspaceTabInFocusedPane = useWorkspaceLayoutStore( + (state) => state.openTabInFocusedPane, + ); const openWorkspaceChildTabFocused = useWorkspaceLayoutStore( (state) => state.openChildTabFocused, ); + const openWorkspaceChildTabInFocusedPane = useWorkspaceLayoutStore( + (state) => state.openChildTabInFocusedPane, + ); // File targets stay identity-stable so the same path reuses its tab. Keep navigation // requests separate so clicking an unchanged path:line can still recenter the pane. const [fileNavigationRevisionByTabId, setFileNavigationRevisionByTabId] = useState< @@ -1657,6 +1672,7 @@ function WorkspaceScreenContent({ } = useWorkspaceTerminalTabActions({ persistenceKey, openWorkspaceTabFocused, + openWorkspaceTabInFocusedPane, labels: { workspacePathUnavailable: t("workspace.header.toasts.workspacePathUnavailable"), terminalQueued: t("workspace.header.toasts.terminalQueued"), @@ -1951,6 +1967,7 @@ function WorkspaceScreenContent({ const openWorkspaceDraftTab = useCallback( function openWorkspaceDraftTab(input?: { + ambient?: boolean; draftId?: string; focus?: boolean; paneId?: string | null; @@ -1964,13 +1981,25 @@ function WorkspaceScreenContent({ draftId: trimNonEmpty(input?.draftId) ?? generateDraftId(), }); invariant(target?.kind === "draft", "Draft tab target must be valid"); - const placement = { paneId: input?.paneId }; - if (input?.focus === false) { - return openWorkspaceTabInBackground(persistenceKey, target, placement); + if (input?.ambient) { + return openWorkspaceTabFocused(persistenceKey, target); } - return openWorkspaceTabFocused(persistenceKey, target, placement); + if (input?.focus === false) { + return openWorkspaceTabInBackground(persistenceKey, target, { + paneId: input?.paneId, + }); + } + if (input?.paneId) { + return openWorkspaceTabFocused(persistenceKey, target, { paneId: input.paneId }); + } + return openWorkspaceTabInFocusedPane(persistenceKey, target); }, - [openWorkspaceTabFocused, openWorkspaceTabInBackground, persistenceKey], + [ + openWorkspaceTabFocused, + openWorkspaceTabInBackground, + openWorkspaceTabInFocusedPane, + persistenceKey, + ], ); useEffect(() => { @@ -2000,12 +2029,16 @@ function WorkspaceScreenContent({ terminalsHydrated: terminalsQuery.isSuccess, knownTerminalIds, standaloneTerminalIds, + hasActivePendingTerminalCreate: + createTerminalMutation.isPending || pendingTerminalCreateInput !== null, hasActivePendingDraftCreate: hasActivePendingDraftCreateInWorkspace, }), ); }, [ hasHydratedAgents, hasHydratedWorkspaceLayoutStore, + pendingTerminalCreateInput, + createTerminalMutation.isPending, isRouteFocused, normalizedServerId, normalizedWorkspaceId, @@ -2047,12 +2080,12 @@ function WorkspaceScreenContent({ if (!persistenceKey) { return; } - const tabId = openWorkspaceTabFocused(persistenceKey, { kind: "agent", agentId }); + const tabId = openWorkspaceTabInFocusedPane(persistenceKey, { kind: "agent", agentId }); if (tabId) { navigateToTabId(tabId); } }, - [navigateToTabId, openWorkspaceTabFocused, persistenceKey], + [navigateToTabId, openWorkspaceTabInFocusedPane, persistenceKey], ); const emptyWorkspaceSeedRef = useRef(null); @@ -2155,7 +2188,7 @@ function WorkspaceScreenContent({ return; } emptyWorkspaceSeedRef.current = workspaceKey; - openWorkspaceDraftTab(); + openWorkspaceDraftTab({ ambient: true }); }, [ normalizedServerId, normalizedWorkspaceId, @@ -2180,12 +2213,15 @@ function WorkspaceScreenContent({ if (!location) { return; } - const tabId = openWorkspaceTabFocused(persistenceKey, createWorkspaceFileTabTarget(location)); + const tabId = openWorkspaceTabInFocusedPane( + persistenceKey, + createWorkspaceFileTabTarget(location), + ); if (tabId) { navigateToTabId(tabId); } }, - [navigateToTabId, openWorkspaceTabFocused, persistenceKey], + [navigateToTabId, openWorkspaceTabInFocusedPane, persistenceKey], ); const handleOpenFileFromChat = useCallback( @@ -2202,8 +2238,8 @@ function WorkspaceScreenContent({ } const target = createWorkspaceFileTabTarget(normalizedLocation); const tabId = options?.parentTabId - ? openWorkspaceChildTabFocused(persistenceKey, target, options.parentTabId) - : openWorkspaceTabFocused(persistenceKey, target); + ? openWorkspaceChildTabInFocusedPane(persistenceKey, target, options.parentTabId) + : openWorkspaceTabInFocusedPane(persistenceKey, target); if (tabId) { requestFileNavigation(tabId); navigateToTabId(tabId); @@ -2212,8 +2248,8 @@ function WorkspaceScreenContent({ [ isMobile, navigateToTabId, - openWorkspaceChildTabFocused, - openWorkspaceTabFocused, + openWorkspaceChildTabInFocusedPane, + openWorkspaceTabInFocusedPane, persistenceKey, requestFileNavigation, showMobileAgent, @@ -2379,13 +2415,19 @@ function WorkspaceScreenContent({ return; } const { browserId } = createWorkspaceBrowser(); - openWorkspaceTabFocused( - persistenceKey, - { kind: "browser", browserId }, - { paneId: input?.paneId }, - ); + if (input?.paneId) { + openWorkspaceTabFocused( + persistenceKey, + { kind: "browser", browserId }, + { + paneId: input.paneId, + }, + ); + } else { + openWorkspaceTabInFocusedPane(persistenceKey, { kind: "browser", browserId }); + } }, - [openWorkspaceTabFocused, persistenceKey], + [openWorkspaceTabFocused, openWorkspaceTabInFocusedPane, persistenceKey], ); const handleOpenUrlInBrowserTab = useCallback( @@ -2746,14 +2788,14 @@ function WorkspaceScreenContent({ return; } if (isMobile || !supportsDesktopPaneSplits()) { - openWorkspaceTabFocused(persistenceKey, target); + openWorkspaceTabInFocusedPane(persistenceKey, target); return; } openWorkspaceTabInExplorerPaneFocused(persistenceKey, { target }); }, [ isMobile, normalizedWorkspaceId, - openWorkspaceTabFocused, + openWorkspaceTabInFocusedPane, openWorkspaceTabInExplorerPaneFocused, persistenceKey, ]); diff --git a/packages/app/src/stores/workspace-layout-actions.ts b/packages/app/src/stores/workspace-layout-actions.ts index 23d264d1c..6e7a32fc1 100644 --- a/packages/app/src/stores/workspace-layout-actions.ts +++ b/packages/app/src/stores/workspace-layout-actions.ts @@ -213,6 +213,7 @@ export interface WorkspaceTabSnapshot { knownAgentIds: Iterable; knownTerminalIds?: Iterable; standaloneTerminalIds: Iterable; + hasActivePendingTerminalCreate?: boolean; hasActivePendingDraftCreate?: boolean; } @@ -1866,6 +1867,7 @@ function addMissingEntityTabs(input: { autoOpenAgentIds: Set; representedAgentIds: Set; standaloneTerminalIds: Set; + hasActivePendingTerminalCreate: boolean; hasActivePendingDraftCreate: boolean; explorerPaneId: string | null; }): WorkspaceLayout { @@ -1873,6 +1875,7 @@ function addMissingEntityTabs(input: { autoOpenAgentIds, representedAgentIds, standaloneTerminalIds, + hasActivePendingTerminalCreate, hasActivePendingDraftCreate, explorerPaneId, } = input; @@ -1902,16 +1905,18 @@ function addMissingEntityTabs(input: { } const sortedTerminalIds = [...standaloneTerminalIds].sort(); - for (const terminalId of sortedTerminalIds) { - if (currentTerminalIds.has(terminalId)) { - continue; + if (!hasActivePendingTerminalCreate) { + for (const terminalId of sortedTerminalIds) { + if (currentTerminalIds.has(terminalId)) { + continue; + } + nextLayout = openEntityTabWithoutFocusing({ + layout: nextLayout, + target: { kind: "terminal", terminalId }, + explorerPaneId, + }); + currentTerminalIds.add(terminalId); } - nextLayout = openEntityTabWithoutFocusing({ - layout: nextLayout, - target: { kind: "terminal", terminalId }, - explorerPaneId, - }); - currentTerminalIds.add(terminalId); } return nextLayout; } @@ -1999,6 +2004,7 @@ export function reconcileWorkspaceTabs( autoOpenAgentIds: autoOpenSet, representedAgentIds, standaloneTerminalIds, + hasActivePendingTerminalCreate: snapshot.hasActivePendingTerminalCreate ?? false, hasActivePendingDraftCreate: snapshot.hasActivePendingDraftCreate ?? false, explorerPaneId: state.explorerPaneId, }); diff --git a/packages/app/src/stores/workspace-layout-store.test.ts b/packages/app/src/stores/workspace-layout-store.test.ts index 78c9816e4..34c30e25b 100644 --- a/packages/app/src/stores/workspace-layout-store.test.ts +++ b/packages/app/src/stores/workspace-layout-store.test.ts @@ -585,6 +585,35 @@ describe("workspace-layout-store actions", () => { expect(layout.focusedPaneId).toBe("explorer"); }); + it("keeps ambient browser opens out of the focused explorer pane", () => { + const workspaceKey = createWorkspaceKey(); + const store = workspaceLayoutStore.getState(); + store.openTabFocused(workspaceKey, { kind: "browser", browserId: "browser-1" }); + store.focusPane(workspaceKey, "explorer"); + + const browserTabId = store.openTabFocused(workspaceKey, { + kind: "browser", + browserId: "browser-2", + }); + + const layout = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey]; + expect(findPaneContainingTab(layout.root, browserTabId as string)?.id).toBe("main"); + expect(layout.focusedPaneId).toBe("main"); + }); + + it("keeps ambient draft opens out of the focused explorer pane", () => { + const workspaceKey = createWorkspaceKey(); + const store = workspaceLayoutStore.getState(); + store.openTabFocused(workspaceKey, { kind: "agent", agentId: "agent-1" }); + store.focusPane(workspaceKey, "explorer"); + + const draftTabId = store.openTabFocused(workspaceKey, { kind: "draft", draftId: "draft-1" }); + + const layout = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey]; + expect(findPaneContainingTab(layout.root, draftTabId as string)?.id).toBe("main"); + expect(layout.focusedPaneId).toBe("main"); + }); + it("honours an explicit pane id over the explorer reroute", () => { const workspaceKey = createWorkspaceKey(); const store = workspaceLayoutStore.getState(); @@ -601,6 +630,76 @@ describe("workspace-layout-store actions", () => { expect(layout.focusedPaneId).toBe("explorer"); }); + it("opens user-created entity tabs in the focused explorer pane", () => { + const workspaceKey = createWorkspaceKey(); + const store = workspaceLayoutStore.getState(); + store.openTabFocused(workspaceKey, { kind: "agent", agentId: "agent-1" }); + store.focusPane(workspaceKey, "explorer"); + + const terminalTabId = store.openTabInFocusedPane(workspaceKey, { + kind: "terminal", + terminalId: "terminal-1", + }); + + const layout = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey]; + expect(findPaneContainingTab(layout.root, terminalTabId as string)?.id).toBe("explorer"); + expect(layout.focusedPaneId).toBe("explorer"); + }); + + it("keeps an existing user-created entity tab in its original pane", () => { + const workspaceKey = createWorkspaceKey(); + const store = workspaceLayoutStore.getState(); + const terminalTabId = store.openTabInBackground(workspaceKey, { + kind: "terminal", + terminalId: "terminal-1", + }); + store.showPane(workspaceKey, "explorer"); + store.focusPane(workspaceKey, "explorer"); + + store.openTabInFocusedPane(workspaceKey, { + kind: "terminal", + terminalId: "terminal-1", + }); + + const layout = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey]; + expect(findPaneContainingTab(layout.root, terminalTabId as string)?.id).toBe("main"); + expect(findPaneById(layout.root, "main")).toBeTruthy(); + expect(layout.focusedPaneId).toBe("main"); + }); + + it("defers terminal reconciliation while a user terminal is being created", () => { + const workspaceKey = createWorkspaceKey(); + const store = workspaceLayoutStore.getState(); + store.focusPane(workspaceKey, "explorer"); + + store.reconcileTabs(workspaceKey, { + agentsHydrated: true, + terminalsHydrated: true, + activeAgentIds: [], + autoOpenAgentIds: [], + knownAgentIds: [], + knownTerminalIds: ["terminal-1"], + standaloneTerminalIds: ["terminal-1"], + hasActivePendingTerminalCreate: true, + }); + + let layout = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey]; + expect(findPaneContainingTab(layout.root, "terminal_terminal-1")).toBeNull(); + + store.reconcileTabs(workspaceKey, { + agentsHydrated: true, + terminalsHydrated: true, + activeAgentIds: [], + autoOpenAgentIds: [], + knownAgentIds: [], + knownTerminalIds: ["terminal-1"], + standaloneTerminalIds: ["terminal-1"], + }); + + layout = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey]; + expect(findPaneContainingTab(layout.root, "terminal_terminal-1")?.id).toBe("main"); + }); + it("keeps non-entity tabs in the focused explorer pane", () => { const workspaceKey = createWorkspaceKey(); const store = workspaceLayoutStore.getState(); diff --git a/packages/app/src/stores/workspace-layout-store.ts b/packages/app/src/stores/workspace-layout-store.ts index 7515cb86a..24d68c589 100644 --- a/packages/app/src/stores/workspace-layout-store.ts +++ b/packages/app/src/stores/workspace-layout-store.ts @@ -86,12 +86,18 @@ interface WorkspaceLayoutStore { target: WorkspaceTabTarget, options?: WorkspaceTabOpenOptions, ) => string | null; + openTabInFocusedPane: (workspaceKey: string, target: WorkspaceTabTarget) => string | null; openChildTabFocused: ( workspaceKey: string, target: WorkspaceTabTarget, parentTabId: string, options?: WorkspaceTabOpenOptions, ) => string | null; + openChildTabInFocusedPane: ( + workspaceKey: string, + target: WorkspaceTabTarget, + parentTabId: string, + ) => string | null; openTabInBackground: ( workspaceKey: string, target: WorkspaceTabTarget, @@ -146,11 +152,7 @@ interface WorkspaceLayoutStore { purgeWorkspace: (workspaceKey: string) => void; } -/** - * `paneId` states which pane the user acted in — the `+` menu of a pane's tab rail, - * a subagent link inside a pane. Without it the open is ambient (sidebar click, - * reconcile auto-open) and the store decides the pane. - */ +/** `paneId` is explicit placement for a pane-local affordance; omitted opens use policy. */ export interface WorkspaceTabOpenOptions { paneId?: string | null; } @@ -463,6 +465,17 @@ export function createWorkspaceLayoutStore( return result.tabId; }, + openTabInFocusedPane: (workspaceKey, target) => { + const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); + if (!normalizedWorkspaceKey) { + return null; + } + const layout = getWorkspaceLayout(get().layoutByWorkspace, normalizedWorkspaceKey); + const focusedPane = findPaneById(layout.root, layout.focusedPaneId); + return get().openTabFocused(normalizedWorkspaceKey, target, { + paneId: focusedPane?.hidden === true ? null : focusedPane?.id, + }); + }, openChildTabFocused: (workspaceKey, target, parentTabId, options) => { const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); const normalizedParentTabId = trimNonEmpty(parentTabId); @@ -502,6 +515,17 @@ export function createWorkspaceLayoutStore( return result.tabId; }, + openChildTabInFocusedPane: (workspaceKey, target, parentTabId) => { + const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); + if (!normalizedWorkspaceKey) { + return null; + } + const layout = getWorkspaceLayout(get().layoutByWorkspace, normalizedWorkspaceKey); + const focusedPane = findPaneById(layout.root, layout.focusedPaneId); + return get().openChildTabFocused(normalizedWorkspaceKey, target, parentTabId, { + paneId: focusedPane?.hidden === true ? null : focusedPane?.id, + }); + }, openTabInBackground: (workspaceKey, target, options) => { const normalizedWorkspaceKey = trimNonEmpty(workspaceKey); const normalizedTarget = normalizeWorkspaceTabTarget(target); diff --git a/packages/app/src/stores/workspace-subagents-integration.test.ts b/packages/app/src/stores/workspace-subagents-integration.test.ts index 580a8c6be..46130ae36 100644 --- a/packages/app/src/stores/workspace-subagents-integration.test.ts +++ b/packages/app/src/stores/workspace-subagents-integration.test.ts @@ -108,6 +108,7 @@ function reconcileWorkspaceTabs(workspaceKey: string, visibility: WorkspaceAgent terminalsHydrated: true, knownTerminalIds: [], standaloneTerminalIds: [], + hasActivePendingTerminalCreate: false, hasActivePendingDraftCreate: false, }), ); diff --git a/packages/app/src/workspace-tabs/agent-visibility.test.ts b/packages/app/src/workspace-tabs/agent-visibility.test.ts index b0fd6a861..8488c8f51 100644 --- a/packages/app/src/workspace-tabs/agent-visibility.test.ts +++ b/packages/app/src/workspace-tabs/agent-visibility.test.ts @@ -333,6 +333,7 @@ describe("workspace agent visibility", () => { terminalsHydrated: true, knownTerminalIds: ["terminal-1", "script-terminal"], standaloneTerminalIds: ["terminal-1"], + hasActivePendingTerminalCreate: false, hasActivePendingDraftCreate: false, }), ).toEqual({ @@ -343,6 +344,7 @@ describe("workspace agent visibility", () => { knownAgentIds: agentVisibility.knownAgentIds, knownTerminalIds: ["terminal-1", "script-terminal"], standaloneTerminalIds: ["terminal-1"], + hasActivePendingTerminalCreate: false, hasActivePendingDraftCreate: false, }); }); diff --git a/packages/app/src/workspace-tabs/agent-visibility.ts b/packages/app/src/workspace-tabs/agent-visibility.ts index f6ff8dbed..1f8f5c5ed 100644 --- a/packages/app/src/workspace-tabs/agent-visibility.ts +++ b/packages/app/src/workspace-tabs/agent-visibility.ts @@ -64,6 +64,7 @@ export function buildWorkspaceTabSnapshot(input: { terminalsHydrated: boolean; knownTerminalIds: Iterable; standaloneTerminalIds: Iterable; + hasActivePendingTerminalCreate: boolean; hasActivePendingDraftCreate: boolean; }): WorkspaceTabSnapshot { return { @@ -74,6 +75,7 @@ export function buildWorkspaceTabSnapshot(input: { knownAgentIds: input.agentVisibility.knownAgentIds, knownTerminalIds: input.knownTerminalIds, standaloneTerminalIds: input.standaloneTerminalIds, + hasActivePendingTerminalCreate: input.hasActivePendingTerminalCreate, hasActivePendingDraftCreate: input.hasActivePendingDraftCreate, }; }