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:
@@ -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,
|
||||
}),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user