mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-14 13:22:37 +00:00
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:
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
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<string | null>(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();
|
||||
if (input?.paneId) {
|
||||
openWorkspaceTabFocused(
|
||||
persistenceKey,
|
||||
{ kind: "browser", browserId },
|
||||
{ paneId: input?.paneId },
|
||||
);
|
||||
{
|
||||
paneId: input.paneId,
|
||||
},
|
||||
[openWorkspaceTabFocused, persistenceKey],
|
||||
);
|
||||
} else {
|
||||
openWorkspaceTabInFocusedPane(persistenceKey, { kind: "browser", browserId });
|
||||
}
|
||||
},
|
||||
[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,
|
||||
]);
|
||||
|
||||
@@ -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,6 +1905,7 @@ function addMissingEntityTabs(input: {
|
||||
}
|
||||
|
||||
const sortedTerminalIds = [...standaloneTerminalIds].sort();
|
||||
if (!hasActivePendingTerminalCreate) {
|
||||
for (const terminalId of sortedTerminalIds) {
|
||||
if (currentTerminalIds.has(terminalId)) {
|
||||
continue;
|
||||
@@ -1913,6 +1917,7 @@ function addMissingEntityTabs(input: {
|
||||
});
|
||||
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,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -64,6 +64,7 @@ export function buildWorkspaceTabSnapshot(input: {
|
||||
terminalsHydrated: boolean;
|
||||
knownTerminalIds: Iterable<string>;
|
||||
standaloneTerminalIds: Iterable<string>;
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user