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,
|
filePath: path,
|
||||||
persistenceKey: buildWorkspaceTabPersistenceKey({ serverId, workspaceId }),
|
persistenceKey: buildWorkspaceTabPersistenceKey({ serverId, workspaceId }),
|
||||||
showMobileAgent: usePanelStore.getState().showMobileAgent,
|
showMobileAgent: usePanelStore.getState().showMobileAgent,
|
||||||
openWorkspaceTabFocused: useWorkspaceLayoutStore.getState().openTabFocused,
|
openWorkspaceTabInFocusedPane: useWorkspaceLayoutStore.getState().openTabInFocusedPane,
|
||||||
focusWorkspaceTab: useWorkspaceLayoutStore.getState().focusTab,
|
focusWorkspaceTab: useWorkspaceLayoutStore.getState().focusTab,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -101,7 +101,9 @@ export function CompactExplorerSidebarHost({ children, enabled }: CompactExplore
|
|||||||
const model = useActiveCompactExplorerSidebarModel(enabled);
|
const model = useActiveCompactExplorerSidebarModel(enabled);
|
||||||
const openFileExplorerForCheckout = usePanelStore((state) => state.openFileExplorerForCheckout);
|
const openFileExplorerForCheckout = usePanelStore((state) => state.openFileExplorerForCheckout);
|
||||||
const showMobileAgent = usePanelStore((state) => state.showMobileAgent);
|
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 focusWorkspaceTab = useWorkspaceLayoutStore((state) => state.focusTab);
|
||||||
|
|
||||||
const handleOpenExplorer = useCallback(() => {
|
const handleOpenExplorer = useCallback(() => {
|
||||||
@@ -127,11 +129,11 @@ export function CompactExplorerSidebarHost({ children, enabled }: CompactExplore
|
|||||||
filePath,
|
filePath,
|
||||||
persistenceKey: model.persistenceKey,
|
persistenceKey: model.persistenceKey,
|
||||||
showMobileAgent,
|
showMobileAgent,
|
||||||
openWorkspaceTabFocused,
|
openWorkspaceTabInFocusedPane,
|
||||||
focusWorkspaceTab,
|
focusWorkspaceTab,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[focusWorkspaceTab, model, openWorkspaceTabFocused, showMobileAgent],
|
[focusWorkspaceTab, model, openWorkspaceTabInFocusedPane, showMobileAgent],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1019,7 +1019,9 @@ function useDiffTabNavigation({
|
|||||||
cwd: string;
|
cwd: string;
|
||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
}) {
|
}) {
|
||||||
const openWorkspaceTabFocused = useWorkspaceLayoutStore((state) => state.openTabFocused);
|
const openWorkspaceTabInFocusedPane = useWorkspaceLayoutStore(
|
||||||
|
(state) => state.openTabInFocusedPane,
|
||||||
|
);
|
||||||
const closeWorkspaceTab = useWorkspaceLayoutStore((state) => state.closeTab);
|
const closeWorkspaceTab = useWorkspaceLayoutStore((state) => state.closeTab);
|
||||||
const persistenceKey = useMemo(
|
const persistenceKey = useMemo(
|
||||||
() => buildWorkspaceTabPersistenceKey({ serverId, workspaceId: workspaceId ?? cwd }),
|
() => buildWorkspaceTabPersistenceKey({ serverId, workspaceId: workspaceId ?? cwd }),
|
||||||
@@ -1040,12 +1042,12 @@ function useDiffTabNavigation({
|
|||||||
if (!persistenceKey || isMobile) {
|
if (!persistenceKey || isMobile) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
openWorkspaceTabFocused(persistenceKey, {
|
openWorkspaceTabInFocusedPane(persistenceKey, {
|
||||||
kind: "working_diff",
|
kind: "working_diff",
|
||||||
...(path ? { focusPath: path, focusRequestId: Date.now() } : {}),
|
...(path ? { focusPath: path, focusRequestId: Date.now() } : {}),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[isMobile, openWorkspaceTabFocused, persistenceKey],
|
[isMobile, openWorkspaceTabInFocusedPane, persistenceKey],
|
||||||
);
|
);
|
||||||
const toggleChanges = useCallback(() => {
|
const toggleChanges = useCallback(() => {
|
||||||
if (!persistenceKey || isMobile) {
|
if (!persistenceKey || isMobile) {
|
||||||
@@ -1060,10 +1062,10 @@ function useDiffTabNavigation({
|
|||||||
const openCommit = useCallback(
|
const openCommit = useCallback(
|
||||||
(sha: string) => {
|
(sha: string) => {
|
||||||
if (persistenceKey) {
|
if (persistenceKey) {
|
||||||
openWorkspaceTabFocused(persistenceKey, { kind: "commit_diff", sha });
|
openWorkspaceTabInFocusedPane(persistenceKey, { kind: "commit_diff", sha });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[openWorkspaceTabFocused, persistenceKey],
|
[openWorkspaceTabInFocusedPane, persistenceKey],
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
changesTabOpen,
|
changesTabOpen,
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ interface OpenWorkspaceFileFromExplorerInput {
|
|||||||
filePath: string;
|
filePath: string;
|
||||||
persistenceKey: string | null;
|
persistenceKey: string | null;
|
||||||
showMobileAgent: () => void;
|
showMobileAgent: () => void;
|
||||||
openWorkspaceTabFocused: (workspaceKey: string, target: WorkspaceTabTarget) => string | null;
|
openWorkspaceTabInFocusedPane: (
|
||||||
|
workspaceKey: string,
|
||||||
|
target: WorkspaceTabTarget,
|
||||||
|
) => string | null;
|
||||||
focusWorkspaceTab: (workspaceKey: string, tabId: string) => void;
|
focusWorkspaceTab: (workspaceKey: string, tabId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,7 +24,7 @@ export function openWorkspaceFileFromExplorer(input: OpenWorkspaceFileFromExplor
|
|||||||
if (!location) {
|
if (!location) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const tabId = input.openWorkspaceTabFocused(
|
const tabId = input.openWorkspaceTabInFocusedPane(
|
||||||
input.persistenceKey,
|
input.persistenceKey,
|
||||||
createWorkspaceFileTabTarget(location),
|
createWorkspaceFileTabTarget(location),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1466,6 +1466,10 @@ interface WorkspaceTerminalTabActionsInput {
|
|||||||
target: WorkspaceTabTarget,
|
target: WorkspaceTabTarget,
|
||||||
options?: WorkspaceTabOpenOptions,
|
options?: WorkspaceTabOpenOptions,
|
||||||
) => string | null;
|
) => string | null;
|
||||||
|
openWorkspaceTabInFocusedPane: (
|
||||||
|
workspaceKey: string,
|
||||||
|
target: WorkspaceTabTarget,
|
||||||
|
) => string | null;
|
||||||
labels: {
|
labels: {
|
||||||
workspacePathUnavailable: string;
|
workspacePathUnavailable: string;
|
||||||
terminalQueued: string;
|
terminalQueued: string;
|
||||||
@@ -1487,6 +1491,7 @@ interface WorkspaceTerminalTabActions {
|
|||||||
function useWorkspaceTerminalTabActions({
|
function useWorkspaceTerminalTabActions({
|
||||||
persistenceKey,
|
persistenceKey,
|
||||||
openWorkspaceTabFocused,
|
openWorkspaceTabFocused,
|
||||||
|
openWorkspaceTabInFocusedPane,
|
||||||
labels,
|
labels,
|
||||||
toast,
|
toast,
|
||||||
}: WorkspaceTerminalTabActionsInput): WorkspaceTerminalTabActions {
|
}: WorkspaceTerminalTabActionsInput): WorkspaceTerminalTabActions {
|
||||||
@@ -1495,18 +1500,22 @@ function useWorkspaceTerminalTabActions({
|
|||||||
if (!persistenceKey) {
|
if (!persistenceKey) {
|
||||||
return;
|
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(
|
const handleScriptTerminalSelected = useCallback(
|
||||||
(terminalId: string) => {
|
(terminalId: string) => {
|
||||||
if (!persistenceKey) {
|
if (!persistenceKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
openWorkspaceTabFocused(persistenceKey, { kind: "terminal", terminalId });
|
openWorkspaceTabInFocusedPane(persistenceKey, { kind: "terminal", terminalId });
|
||||||
},
|
},
|
||||||
[openWorkspaceTabFocused, persistenceKey],
|
[openWorkspaceTabInFocusedPane, persistenceKey],
|
||||||
);
|
);
|
||||||
const handleWorkspacePathUnavailable = useCallback(() => {
|
const handleWorkspacePathUnavailable = useCallback(() => {
|
||||||
toast.error(labels.workspacePathUnavailable);
|
toast.error(labels.workspacePathUnavailable);
|
||||||
@@ -1608,9 +1617,15 @@ function WorkspaceScreenContent({
|
|||||||
[normalizedServerId, normalizedWorkspaceId],
|
[normalizedServerId, normalizedWorkspaceId],
|
||||||
);
|
);
|
||||||
const openWorkspaceTabFocused = useWorkspaceLayoutStore((state) => state.openTabFocused);
|
const openWorkspaceTabFocused = useWorkspaceLayoutStore((state) => state.openTabFocused);
|
||||||
|
const openWorkspaceTabInFocusedPane = useWorkspaceLayoutStore(
|
||||||
|
(state) => state.openTabInFocusedPane,
|
||||||
|
);
|
||||||
const openWorkspaceChildTabFocused = useWorkspaceLayoutStore(
|
const openWorkspaceChildTabFocused = useWorkspaceLayoutStore(
|
||||||
(state) => state.openChildTabFocused,
|
(state) => state.openChildTabFocused,
|
||||||
);
|
);
|
||||||
|
const openWorkspaceChildTabInFocusedPane = useWorkspaceLayoutStore(
|
||||||
|
(state) => state.openChildTabInFocusedPane,
|
||||||
|
);
|
||||||
// File targets stay identity-stable so the same path reuses its tab. Keep navigation
|
// 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.
|
// requests separate so clicking an unchanged path:line can still recenter the pane.
|
||||||
const [fileNavigationRevisionByTabId, setFileNavigationRevisionByTabId] = useState<
|
const [fileNavigationRevisionByTabId, setFileNavigationRevisionByTabId] = useState<
|
||||||
@@ -1657,6 +1672,7 @@ function WorkspaceScreenContent({
|
|||||||
} = useWorkspaceTerminalTabActions({
|
} = useWorkspaceTerminalTabActions({
|
||||||
persistenceKey,
|
persistenceKey,
|
||||||
openWorkspaceTabFocused,
|
openWorkspaceTabFocused,
|
||||||
|
openWorkspaceTabInFocusedPane,
|
||||||
labels: {
|
labels: {
|
||||||
workspacePathUnavailable: t("workspace.header.toasts.workspacePathUnavailable"),
|
workspacePathUnavailable: t("workspace.header.toasts.workspacePathUnavailable"),
|
||||||
terminalQueued: t("workspace.header.toasts.terminalQueued"),
|
terminalQueued: t("workspace.header.toasts.terminalQueued"),
|
||||||
@@ -1951,6 +1967,7 @@ function WorkspaceScreenContent({
|
|||||||
|
|
||||||
const openWorkspaceDraftTab = useCallback(
|
const openWorkspaceDraftTab = useCallback(
|
||||||
function openWorkspaceDraftTab(input?: {
|
function openWorkspaceDraftTab(input?: {
|
||||||
|
ambient?: boolean;
|
||||||
draftId?: string;
|
draftId?: string;
|
||||||
focus?: boolean;
|
focus?: boolean;
|
||||||
paneId?: string | null;
|
paneId?: string | null;
|
||||||
@@ -1964,13 +1981,25 @@ function WorkspaceScreenContent({
|
|||||||
draftId: trimNonEmpty(input?.draftId) ?? generateDraftId(),
|
draftId: trimNonEmpty(input?.draftId) ?? generateDraftId(),
|
||||||
});
|
});
|
||||||
invariant(target?.kind === "draft", "Draft tab target must be valid");
|
invariant(target?.kind === "draft", "Draft tab target must be valid");
|
||||||
const placement = { paneId: input?.paneId };
|
if (input?.ambient) {
|
||||||
if (input?.focus === false) {
|
return openWorkspaceTabFocused(persistenceKey, target);
|
||||||
return openWorkspaceTabInBackground(persistenceKey, target, placement);
|
|
||||||
}
|
}
|
||||||
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(() => {
|
useEffect(() => {
|
||||||
@@ -2000,12 +2029,16 @@ function WorkspaceScreenContent({
|
|||||||
terminalsHydrated: terminalsQuery.isSuccess,
|
terminalsHydrated: terminalsQuery.isSuccess,
|
||||||
knownTerminalIds,
|
knownTerminalIds,
|
||||||
standaloneTerminalIds,
|
standaloneTerminalIds,
|
||||||
|
hasActivePendingTerminalCreate:
|
||||||
|
createTerminalMutation.isPending || pendingTerminalCreateInput !== null,
|
||||||
hasActivePendingDraftCreate: hasActivePendingDraftCreateInWorkspace,
|
hasActivePendingDraftCreate: hasActivePendingDraftCreateInWorkspace,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}, [
|
}, [
|
||||||
hasHydratedAgents,
|
hasHydratedAgents,
|
||||||
hasHydratedWorkspaceLayoutStore,
|
hasHydratedWorkspaceLayoutStore,
|
||||||
|
pendingTerminalCreateInput,
|
||||||
|
createTerminalMutation.isPending,
|
||||||
isRouteFocused,
|
isRouteFocused,
|
||||||
normalizedServerId,
|
normalizedServerId,
|
||||||
normalizedWorkspaceId,
|
normalizedWorkspaceId,
|
||||||
@@ -2047,12 +2080,12 @@ function WorkspaceScreenContent({
|
|||||||
if (!persistenceKey) {
|
if (!persistenceKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const tabId = openWorkspaceTabFocused(persistenceKey, { kind: "agent", agentId });
|
const tabId = openWorkspaceTabInFocusedPane(persistenceKey, { kind: "agent", agentId });
|
||||||
if (tabId) {
|
if (tabId) {
|
||||||
navigateToTabId(tabId);
|
navigateToTabId(tabId);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[navigateToTabId, openWorkspaceTabFocused, persistenceKey],
|
[navigateToTabId, openWorkspaceTabInFocusedPane, persistenceKey],
|
||||||
);
|
);
|
||||||
|
|
||||||
const emptyWorkspaceSeedRef = useRef<string | null>(null);
|
const emptyWorkspaceSeedRef = useRef<string | null>(null);
|
||||||
@@ -2155,7 +2188,7 @@ function WorkspaceScreenContent({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
emptyWorkspaceSeedRef.current = workspaceKey;
|
emptyWorkspaceSeedRef.current = workspaceKey;
|
||||||
openWorkspaceDraftTab();
|
openWorkspaceDraftTab({ ambient: true });
|
||||||
}, [
|
}, [
|
||||||
normalizedServerId,
|
normalizedServerId,
|
||||||
normalizedWorkspaceId,
|
normalizedWorkspaceId,
|
||||||
@@ -2180,12 +2213,15 @@ function WorkspaceScreenContent({
|
|||||||
if (!location) {
|
if (!location) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const tabId = openWorkspaceTabFocused(persistenceKey, createWorkspaceFileTabTarget(location));
|
const tabId = openWorkspaceTabInFocusedPane(
|
||||||
|
persistenceKey,
|
||||||
|
createWorkspaceFileTabTarget(location),
|
||||||
|
);
|
||||||
if (tabId) {
|
if (tabId) {
|
||||||
navigateToTabId(tabId);
|
navigateToTabId(tabId);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[navigateToTabId, openWorkspaceTabFocused, persistenceKey],
|
[navigateToTabId, openWorkspaceTabInFocusedPane, persistenceKey],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleOpenFileFromChat = useCallback(
|
const handleOpenFileFromChat = useCallback(
|
||||||
@@ -2202,8 +2238,8 @@ function WorkspaceScreenContent({
|
|||||||
}
|
}
|
||||||
const target = createWorkspaceFileTabTarget(normalizedLocation);
|
const target = createWorkspaceFileTabTarget(normalizedLocation);
|
||||||
const tabId = options?.parentTabId
|
const tabId = options?.parentTabId
|
||||||
? openWorkspaceChildTabFocused(persistenceKey, target, options.parentTabId)
|
? openWorkspaceChildTabInFocusedPane(persistenceKey, target, options.parentTabId)
|
||||||
: openWorkspaceTabFocused(persistenceKey, target);
|
: openWorkspaceTabInFocusedPane(persistenceKey, target);
|
||||||
if (tabId) {
|
if (tabId) {
|
||||||
requestFileNavigation(tabId);
|
requestFileNavigation(tabId);
|
||||||
navigateToTabId(tabId);
|
navigateToTabId(tabId);
|
||||||
@@ -2212,8 +2248,8 @@ function WorkspaceScreenContent({
|
|||||||
[
|
[
|
||||||
isMobile,
|
isMobile,
|
||||||
navigateToTabId,
|
navigateToTabId,
|
||||||
openWorkspaceChildTabFocused,
|
openWorkspaceChildTabInFocusedPane,
|
||||||
openWorkspaceTabFocused,
|
openWorkspaceTabInFocusedPane,
|
||||||
persistenceKey,
|
persistenceKey,
|
||||||
requestFileNavigation,
|
requestFileNavigation,
|
||||||
showMobileAgent,
|
showMobileAgent,
|
||||||
@@ -2379,13 +2415,19 @@ function WorkspaceScreenContent({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { browserId } = createWorkspaceBrowser();
|
const { browserId } = createWorkspaceBrowser();
|
||||||
openWorkspaceTabFocused(
|
if (input?.paneId) {
|
||||||
persistenceKey,
|
openWorkspaceTabFocused(
|
||||||
{ kind: "browser", browserId },
|
persistenceKey,
|
||||||
{ paneId: input?.paneId },
|
{ kind: "browser", browserId },
|
||||||
);
|
{
|
||||||
|
paneId: input.paneId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
openWorkspaceTabInFocusedPane(persistenceKey, { kind: "browser", browserId });
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[openWorkspaceTabFocused, persistenceKey],
|
[openWorkspaceTabFocused, openWorkspaceTabInFocusedPane, persistenceKey],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleOpenUrlInBrowserTab = useCallback(
|
const handleOpenUrlInBrowserTab = useCallback(
|
||||||
@@ -2746,14 +2788,14 @@ function WorkspaceScreenContent({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isMobile || !supportsDesktopPaneSplits()) {
|
if (isMobile || !supportsDesktopPaneSplits()) {
|
||||||
openWorkspaceTabFocused(persistenceKey, target);
|
openWorkspaceTabInFocusedPane(persistenceKey, target);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
openWorkspaceTabInExplorerPaneFocused(persistenceKey, { target });
|
openWorkspaceTabInExplorerPaneFocused(persistenceKey, { target });
|
||||||
}, [
|
}, [
|
||||||
isMobile,
|
isMobile,
|
||||||
normalizedWorkspaceId,
|
normalizedWorkspaceId,
|
||||||
openWorkspaceTabFocused,
|
openWorkspaceTabInFocusedPane,
|
||||||
openWorkspaceTabInExplorerPaneFocused,
|
openWorkspaceTabInExplorerPaneFocused,
|
||||||
persistenceKey,
|
persistenceKey,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -213,6 +213,7 @@ export interface WorkspaceTabSnapshot {
|
|||||||
knownAgentIds: Iterable<string>;
|
knownAgentIds: Iterable<string>;
|
||||||
knownTerminalIds?: Iterable<string>;
|
knownTerminalIds?: Iterable<string>;
|
||||||
standaloneTerminalIds: Iterable<string>;
|
standaloneTerminalIds: Iterable<string>;
|
||||||
|
hasActivePendingTerminalCreate?: boolean;
|
||||||
hasActivePendingDraftCreate?: boolean;
|
hasActivePendingDraftCreate?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1866,6 +1867,7 @@ function addMissingEntityTabs(input: {
|
|||||||
autoOpenAgentIds: Set<string>;
|
autoOpenAgentIds: Set<string>;
|
||||||
representedAgentIds: Set<string>;
|
representedAgentIds: Set<string>;
|
||||||
standaloneTerminalIds: Set<string>;
|
standaloneTerminalIds: Set<string>;
|
||||||
|
hasActivePendingTerminalCreate: boolean;
|
||||||
hasActivePendingDraftCreate: boolean;
|
hasActivePendingDraftCreate: boolean;
|
||||||
explorerPaneId: string | null;
|
explorerPaneId: string | null;
|
||||||
}): WorkspaceLayout {
|
}): WorkspaceLayout {
|
||||||
@@ -1873,6 +1875,7 @@ function addMissingEntityTabs(input: {
|
|||||||
autoOpenAgentIds,
|
autoOpenAgentIds,
|
||||||
representedAgentIds,
|
representedAgentIds,
|
||||||
standaloneTerminalIds,
|
standaloneTerminalIds,
|
||||||
|
hasActivePendingTerminalCreate,
|
||||||
hasActivePendingDraftCreate,
|
hasActivePendingDraftCreate,
|
||||||
explorerPaneId,
|
explorerPaneId,
|
||||||
} = input;
|
} = input;
|
||||||
@@ -1902,16 +1905,18 @@ function addMissingEntityTabs(input: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sortedTerminalIds = [...standaloneTerminalIds].sort();
|
const sortedTerminalIds = [...standaloneTerminalIds].sort();
|
||||||
for (const terminalId of sortedTerminalIds) {
|
if (!hasActivePendingTerminalCreate) {
|
||||||
if (currentTerminalIds.has(terminalId)) {
|
for (const terminalId of sortedTerminalIds) {
|
||||||
continue;
|
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;
|
return nextLayout;
|
||||||
}
|
}
|
||||||
@@ -1999,6 +2004,7 @@ export function reconcileWorkspaceTabs(
|
|||||||
autoOpenAgentIds: autoOpenSet,
|
autoOpenAgentIds: autoOpenSet,
|
||||||
representedAgentIds,
|
representedAgentIds,
|
||||||
standaloneTerminalIds,
|
standaloneTerminalIds,
|
||||||
|
hasActivePendingTerminalCreate: snapshot.hasActivePendingTerminalCreate ?? false,
|
||||||
hasActivePendingDraftCreate: snapshot.hasActivePendingDraftCreate ?? false,
|
hasActivePendingDraftCreate: snapshot.hasActivePendingDraftCreate ?? false,
|
||||||
explorerPaneId: state.explorerPaneId,
|
explorerPaneId: state.explorerPaneId,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -585,6 +585,35 @@ describe("workspace-layout-store actions", () => {
|
|||||||
expect(layout.focusedPaneId).toBe("explorer");
|
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", () => {
|
it("honours an explicit pane id over the explorer reroute", () => {
|
||||||
const workspaceKey = createWorkspaceKey();
|
const workspaceKey = createWorkspaceKey();
|
||||||
const store = workspaceLayoutStore.getState();
|
const store = workspaceLayoutStore.getState();
|
||||||
@@ -601,6 +630,76 @@ describe("workspace-layout-store actions", () => {
|
|||||||
expect(layout.focusedPaneId).toBe("explorer");
|
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", () => {
|
it("keeps non-entity tabs in the focused explorer pane", () => {
|
||||||
const workspaceKey = createWorkspaceKey();
|
const workspaceKey = createWorkspaceKey();
|
||||||
const store = workspaceLayoutStore.getState();
|
const store = workspaceLayoutStore.getState();
|
||||||
|
|||||||
@@ -86,12 +86,18 @@ interface WorkspaceLayoutStore {
|
|||||||
target: WorkspaceTabTarget,
|
target: WorkspaceTabTarget,
|
||||||
options?: WorkspaceTabOpenOptions,
|
options?: WorkspaceTabOpenOptions,
|
||||||
) => string | null;
|
) => string | null;
|
||||||
|
openTabInFocusedPane: (workspaceKey: string, target: WorkspaceTabTarget) => string | null;
|
||||||
openChildTabFocused: (
|
openChildTabFocused: (
|
||||||
workspaceKey: string,
|
workspaceKey: string,
|
||||||
target: WorkspaceTabTarget,
|
target: WorkspaceTabTarget,
|
||||||
parentTabId: string,
|
parentTabId: string,
|
||||||
options?: WorkspaceTabOpenOptions,
|
options?: WorkspaceTabOpenOptions,
|
||||||
) => string | null;
|
) => string | null;
|
||||||
|
openChildTabInFocusedPane: (
|
||||||
|
workspaceKey: string,
|
||||||
|
target: WorkspaceTabTarget,
|
||||||
|
parentTabId: string,
|
||||||
|
) => string | null;
|
||||||
openTabInBackground: (
|
openTabInBackground: (
|
||||||
workspaceKey: string,
|
workspaceKey: string,
|
||||||
target: WorkspaceTabTarget,
|
target: WorkspaceTabTarget,
|
||||||
@@ -146,11 +152,7 @@ interface WorkspaceLayoutStore {
|
|||||||
purgeWorkspace: (workspaceKey: string) => void;
|
purgeWorkspace: (workspaceKey: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** `paneId` is explicit placement for a pane-local affordance; omitted opens use policy. */
|
||||||
* `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.
|
|
||||||
*/
|
|
||||||
export interface WorkspaceTabOpenOptions {
|
export interface WorkspaceTabOpenOptions {
|
||||||
paneId?: string | null;
|
paneId?: string | null;
|
||||||
}
|
}
|
||||||
@@ -463,6 +465,17 @@ export function createWorkspaceLayoutStore(
|
|||||||
|
|
||||||
return result.tabId;
|
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) => {
|
openChildTabFocused: (workspaceKey, target, parentTabId, options) => {
|
||||||
const normalizedWorkspaceKey = trimNonEmpty(workspaceKey);
|
const normalizedWorkspaceKey = trimNonEmpty(workspaceKey);
|
||||||
const normalizedParentTabId = trimNonEmpty(parentTabId);
|
const normalizedParentTabId = trimNonEmpty(parentTabId);
|
||||||
@@ -502,6 +515,17 @@ export function createWorkspaceLayoutStore(
|
|||||||
|
|
||||||
return result.tabId;
|
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) => {
|
openTabInBackground: (workspaceKey, target, options) => {
|
||||||
const normalizedWorkspaceKey = trimNonEmpty(workspaceKey);
|
const normalizedWorkspaceKey = trimNonEmpty(workspaceKey);
|
||||||
const normalizedTarget = normalizeWorkspaceTabTarget(target);
|
const normalizedTarget = normalizeWorkspaceTabTarget(target);
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ function reconcileWorkspaceTabs(workspaceKey: string, visibility: WorkspaceAgent
|
|||||||
terminalsHydrated: true,
|
terminalsHydrated: true,
|
||||||
knownTerminalIds: [],
|
knownTerminalIds: [],
|
||||||
standaloneTerminalIds: [],
|
standaloneTerminalIds: [],
|
||||||
|
hasActivePendingTerminalCreate: false,
|
||||||
hasActivePendingDraftCreate: false,
|
hasActivePendingDraftCreate: false,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -333,6 +333,7 @@ describe("workspace agent visibility", () => {
|
|||||||
terminalsHydrated: true,
|
terminalsHydrated: true,
|
||||||
knownTerminalIds: ["terminal-1", "script-terminal"],
|
knownTerminalIds: ["terminal-1", "script-terminal"],
|
||||||
standaloneTerminalIds: ["terminal-1"],
|
standaloneTerminalIds: ["terminal-1"],
|
||||||
|
hasActivePendingTerminalCreate: false,
|
||||||
hasActivePendingDraftCreate: false,
|
hasActivePendingDraftCreate: false,
|
||||||
}),
|
}),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
@@ -343,6 +344,7 @@ describe("workspace agent visibility", () => {
|
|||||||
knownAgentIds: agentVisibility.knownAgentIds,
|
knownAgentIds: agentVisibility.knownAgentIds,
|
||||||
knownTerminalIds: ["terminal-1", "script-terminal"],
|
knownTerminalIds: ["terminal-1", "script-terminal"],
|
||||||
standaloneTerminalIds: ["terminal-1"],
|
standaloneTerminalIds: ["terminal-1"],
|
||||||
|
hasActivePendingTerminalCreate: false,
|
||||||
hasActivePendingDraftCreate: false,
|
hasActivePendingDraftCreate: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ export function buildWorkspaceTabSnapshot(input: {
|
|||||||
terminalsHydrated: boolean;
|
terminalsHydrated: boolean;
|
||||||
knownTerminalIds: Iterable<string>;
|
knownTerminalIds: Iterable<string>;
|
||||||
standaloneTerminalIds: Iterable<string>;
|
standaloneTerminalIds: Iterable<string>;
|
||||||
|
hasActivePendingTerminalCreate: boolean;
|
||||||
hasActivePendingDraftCreate: boolean;
|
hasActivePendingDraftCreate: boolean;
|
||||||
}): WorkspaceTabSnapshot {
|
}): WorkspaceTabSnapshot {
|
||||||
return {
|
return {
|
||||||
@@ -74,6 +75,7 @@ export function buildWorkspaceTabSnapshot(input: {
|
|||||||
knownAgentIds: input.agentVisibility.knownAgentIds,
|
knownAgentIds: input.agentVisibility.knownAgentIds,
|
||||||
knownTerminalIds: input.knownTerminalIds,
|
knownTerminalIds: input.knownTerminalIds,
|
||||||
standaloneTerminalIds: input.standaloneTerminalIds,
|
standaloneTerminalIds: input.standaloneTerminalIds,
|
||||||
|
hasActivePendingTerminalCreate: input.hasActivePendingTerminalCreate,
|
||||||
hasActivePendingDraftCreate: input.hasActivePendingDraftCreate,
|
hasActivePendingDraftCreate: input.hasActivePendingDraftCreate,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user