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.
This commit is contained in:
Mohamed Boudra
2026-08-19 15:52:51 +02:00
parent 133c38c5c3
commit ce905d9483
12 changed files with 267 additions and 51 deletions
@@ -213,6 +213,7 @@ export interface WorkspaceTabSnapshot {
knownAgentIds: Iterable<string>;
knownTerminalIds?: Iterable<string>;
standaloneTerminalIds: Iterable<string>;
hasActivePendingTerminalCreate?: boolean;
hasActivePendingDraftCreate?: boolean;
}
@@ -1866,6 +1867,7 @@ function addMissingEntityTabs(input: {
autoOpenAgentIds: Set<string>;
representedAgentIds: Set<string>;
standaloneTerminalIds: Set<string>;
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,
});
@@ -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();
@@ -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);
@@ -108,6 +108,7 @@ function reconcileWorkspaceTabs(workspaceKey: string, visibility: WorkspaceAgent
terminalsHydrated: true,
knownTerminalIds: [],
standaloneTerminalIds: [],
hasActivePendingTerminalCreate: false,
hasActivePendingDraftCreate: false,
}),
);