fix(app): retain workspace explorer pane
Desktop workspaces need a stable hidden explorer pane so setup and pull-request tabs have a consistent destination without changing the active pane. Mobile keeps pull requests in its explorer sidebar.
This commit is contained in:
@@ -53,9 +53,10 @@ import { traceInstant } from "@/performance/native-trace";
|
|||||||
import { useSessionStore, type WorkspaceDescriptor } from "@/stores/session-store";
|
import { useSessionStore, type WorkspaceDescriptor } from "@/stores/session-store";
|
||||||
import {
|
import {
|
||||||
collectAllTabs,
|
collectAllTabs,
|
||||||
createDefaultLayout,
|
createWorkspaceLayoutWithExplorer,
|
||||||
findPaneById,
|
findPaneById,
|
||||||
getFocusedBrowserId,
|
getFocusedBrowserId,
|
||||||
|
resolveExplorerPaneId,
|
||||||
type WorkspaceLayout,
|
type WorkspaceLayout,
|
||||||
useWorkspaceLayoutStore,
|
useWorkspaceLayoutStore,
|
||||||
useWorkspaceLayoutStoreHydrated,
|
useWorkspaceLayoutStoreHydrated,
|
||||||
@@ -1360,8 +1361,12 @@ function buildWorkspaceTerminalScopeKey(serverId: string, workspaceId: string):
|
|||||||
return `${serverId}:${workspaceId}`;
|
return `${serverId}:${workspaceId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function canObservePullRequest(isRouteFocused: boolean, isGitCheckout: boolean): boolean {
|
function canObservePullRequest(
|
||||||
return isRouteFocused && isGitCheckout;
|
isRouteFocused: boolean,
|
||||||
|
isGitCheckout: boolean,
|
||||||
|
isCompact: boolean,
|
||||||
|
): boolean {
|
||||||
|
return isRouteFocused && isGitCheckout && !isCompact && supportsDesktopPaneSplits();
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ToggleWorkspaceExplorerPaneInput {
|
interface ToggleWorkspaceExplorerPaneInput {
|
||||||
@@ -1383,15 +1388,28 @@ function toggleWorkspaceExplorerPane(input: ToggleWorkspaceExplorerPaneInput): v
|
|||||||
}
|
}
|
||||||
|
|
||||||
const store = useWorkspaceLayoutStore.getState();
|
const store = useWorkspaceLayoutStore.getState();
|
||||||
const layout = store.layoutByWorkspace[input.persistenceKey] ?? createDefaultLayout();
|
const layout =
|
||||||
|
store.layoutByWorkspace[input.persistenceKey] ?? createWorkspaceLayoutWithExplorer();
|
||||||
const parentTabId = findPaneById(layout.root, layout.focusedPaneId)?.focusedTabId ?? null;
|
const parentTabId = findPaneById(layout.root, layout.focusedPaneId)?.focusedTabId ?? null;
|
||||||
const explorerPaneId = store.explorerPaneIdByWorkspace[input.persistenceKey] ?? null;
|
const explorerPaneId = resolveExplorerPaneId(
|
||||||
|
layout,
|
||||||
|
store.explorerPaneIdByWorkspace[input.persistenceKey],
|
||||||
|
);
|
||||||
const explorerPane = findPaneById(layout.root, explorerPaneId);
|
const explorerPane = findPaneById(layout.root, explorerPaneId);
|
||||||
if (explorerPane) {
|
if (explorerPane) {
|
||||||
if (explorerPane.hidden === true) {
|
if (explorerPane.hidden === true) {
|
||||||
store.showPane(input.persistenceKey, explorerPane.id);
|
store.showPane(input.persistenceKey, explorerPane.id);
|
||||||
} else {
|
} else {
|
||||||
store.hidePane(input.persistenceKey, explorerPane.id);
|
store.hidePane(input.persistenceKey, explorerPane.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (explorerPane.tabIds.length === 0) {
|
||||||
|
const tabId = parentTabId
|
||||||
|
? store.openChildTabFocused(input.persistenceKey, { kind: "working_diff" }, parentTabId)
|
||||||
|
: store.openTabFocused(input.persistenceKey, { kind: "working_diff" });
|
||||||
|
if (tabId) {
|
||||||
|
store.moveTabToPane(input.persistenceKey, tabId, explorerPane.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1415,8 +1433,10 @@ function isWorkspaceExplorerPaneOpen(
|
|||||||
if (!persistenceKey) {
|
if (!persistenceKey) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const paneId = state.explorerPaneIdByWorkspace[persistenceKey];
|
|
||||||
const layout = state.layoutByWorkspace[persistenceKey];
|
const layout = state.layoutByWorkspace[persistenceKey];
|
||||||
|
const paneId = layout
|
||||||
|
? resolveExplorerPaneId(layout, state.explorerPaneIdByWorkspace[persistenceKey])
|
||||||
|
: null;
|
||||||
const pane = paneId && layout ? findPaneById(layout.root, paneId) : null;
|
const pane = paneId && layout ? findPaneById(layout.root, paneId) : null;
|
||||||
return Boolean(pane && pane.hidden !== true);
|
return Boolean(pane && pane.hidden !== true);
|
||||||
}
|
}
|
||||||
@@ -1706,7 +1726,7 @@ function WorkspaceScreenContent({
|
|||||||
workspaceKey: persistenceKey,
|
workspaceKey: persistenceKey,
|
||||||
serverId: normalizedServerId,
|
serverId: normalizedServerId,
|
||||||
cwd: workspaceDirectory,
|
cwd: workspaceDirectory,
|
||||||
enabled: canObservePullRequest(isRouteFocused, isGitCheckout),
|
enabled: canObservePullRequest(isRouteFocused, isGitCheckout, isMobile),
|
||||||
});
|
});
|
||||||
|
|
||||||
const isCompactExplorerOpen = usePanelStore((state) =>
|
const isCompactExplorerOpen = usePanelStore((state) =>
|
||||||
@@ -1785,6 +1805,12 @@ function WorkspaceScreenContent({
|
|||||||
const openWorkspaceTabInBackground = useWorkspaceLayoutStore(
|
const openWorkspaceTabInBackground = useWorkspaceLayoutStore(
|
||||||
(state) => state.openTabInBackground,
|
(state) => state.openTabInBackground,
|
||||||
);
|
);
|
||||||
|
const openWorkspaceTabInExplorerPaneBackground = useWorkspaceLayoutStore(
|
||||||
|
(state) => state.openTabInExplorerPaneBackground,
|
||||||
|
);
|
||||||
|
const openWorkspaceTabInExplorerPaneFocused = useWorkspaceLayoutStore(
|
||||||
|
(state) => state.openTabInExplorerPaneFocused,
|
||||||
|
);
|
||||||
const focusWorkspaceTab = useWorkspaceLayoutStore((state) => state.focusTab);
|
const focusWorkspaceTab = useWorkspaceLayoutStore((state) => state.focusTab);
|
||||||
const closeWorkspaceTab = useWorkspaceLayoutStore((state) => state.closeTab);
|
const closeWorkspaceTab = useWorkspaceLayoutStore((state) => state.closeTab);
|
||||||
const unpinWorkspaceAgent = useWorkspaceLayoutStore((state) => state.unpinAgent);
|
const unpinWorkspaceAgent = useWorkspaceLayoutStore((state) => state.unpinAgent);
|
||||||
@@ -1994,7 +2020,6 @@ function WorkspaceScreenContent({
|
|||||||
),
|
),
|
||||||
[normalizedWorkspaceId, uiTabs],
|
[normalizedWorkspaceId, uiTabs],
|
||||||
);
|
);
|
||||||
|
|
||||||
const navigateToTabId = useCallback(
|
const navigateToTabId = useCallback(
|
||||||
function navigateToTabId(tabId: string) {
|
function navigateToTabId(tabId: string) {
|
||||||
if (!tabId || !persistenceKey) {
|
if (!tabId || !persistenceKey) {
|
||||||
@@ -2020,6 +2045,64 @@ function WorkspaceScreenContent({
|
|||||||
const emptyWorkspaceSeedRef = useRef<string | null>(null);
|
const emptyWorkspaceSeedRef = useRef<string | null>(null);
|
||||||
const autoOpenedSetupTabWorkspaceRef = useRef<string | null>(null);
|
const autoOpenedSetupTabWorkspaceRef = useRef<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isRouteFocused) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!persistenceKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!workspaceSetupSnapshot || !showWorkspaceSetup) {
|
||||||
|
if (autoOpenedSetupTabWorkspaceRef.current === persistenceKey) {
|
||||||
|
autoOpenedSetupTabWorkspaceRef.current = null;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const snapshotAge = Date.now() - workspaceSetupSnapshot.updatedAt;
|
||||||
|
const shouldAutoOpen =
|
||||||
|
workspaceSetupSnapshot.status === "running" ||
|
||||||
|
snapshotAge <= WORKSPACE_SETUP_AUTO_OPEN_WINDOW_MS;
|
||||||
|
if (!shouldAutoOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (hasSetupTab) {
|
||||||
|
autoOpenedSetupTabWorkspaceRef.current = persistenceKey;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (autoOpenedSetupTabWorkspaceRef.current === persistenceKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = normalizeWorkspaceTabTarget({
|
||||||
|
kind: "setup",
|
||||||
|
workspaceId: normalizedWorkspaceId,
|
||||||
|
});
|
||||||
|
if (!target) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabId =
|
||||||
|
isMobile || !supportsDesktopPaneSplits()
|
||||||
|
? openWorkspaceTabInBackground(persistenceKey, target)
|
||||||
|
: openWorkspaceTabInExplorerPaneBackground(persistenceKey, target);
|
||||||
|
if (!tabId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
autoOpenedSetupTabWorkspaceRef.current = persistenceKey;
|
||||||
|
}, [
|
||||||
|
hasSetupTab,
|
||||||
|
isMobile,
|
||||||
|
isRouteFocused,
|
||||||
|
normalizedWorkspaceId,
|
||||||
|
openWorkspaceTabInBackground,
|
||||||
|
openWorkspaceTabInExplorerPaneBackground,
|
||||||
|
persistenceKey,
|
||||||
|
showWorkspaceSetup,
|
||||||
|
workspaceSetupSnapshot,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isRouteFocused || !client || !normalizedServerId || !normalizedWorkspaceId) {
|
if (!isRouteFocused || !client || !normalizedServerId || !normalizedWorkspaceId) {
|
||||||
return;
|
return;
|
||||||
@@ -2075,59 +2158,6 @@ function WorkspaceScreenContent({
|
|||||||
workspaceAgentVisibility.activeAgentIds.size,
|
workspaceAgentVisibility.activeAgentIds.size,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isRouteFocused) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!persistenceKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!workspaceSetupSnapshot || !showWorkspaceSetup) {
|
|
||||||
if (autoOpenedSetupTabWorkspaceRef.current === persistenceKey) {
|
|
||||||
autoOpenedSetupTabWorkspaceRef.current = null;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const snapshotAge = Date.now() - workspaceSetupSnapshot.updatedAt;
|
|
||||||
const shouldAutoOpen =
|
|
||||||
workspaceSetupSnapshot.status === "running" ||
|
|
||||||
snapshotAge <= WORKSPACE_SETUP_AUTO_OPEN_WINDOW_MS;
|
|
||||||
if (!shouldAutoOpen) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (hasSetupTab) {
|
|
||||||
autoOpenedSetupTabWorkspaceRef.current = persistenceKey;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (autoOpenedSetupTabWorkspaceRef.current === persistenceKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const target = normalizeWorkspaceTabTarget({
|
|
||||||
kind: "setup",
|
|
||||||
workspaceId: normalizedWorkspaceId,
|
|
||||||
});
|
|
||||||
if (!target) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tabId = openWorkspaceTabInBackground(persistenceKey, target);
|
|
||||||
if (!tabId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
autoOpenedSetupTabWorkspaceRef.current = persistenceKey;
|
|
||||||
}, [
|
|
||||||
hasSetupTab,
|
|
||||||
isRouteFocused,
|
|
||||||
normalizedWorkspaceId,
|
|
||||||
openWorkspaceTabInBackground,
|
|
||||||
persistenceKey,
|
|
||||||
showWorkspaceSetup,
|
|
||||||
workspaceSetupSnapshot,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const handleOpenFileFromExplorer = useCallback(
|
const handleOpenFileFromExplorer = useCallback(
|
||||||
function handleOpenFileFromExplorer(filePath: string) {
|
function handleOpenFileFromExplorer(filePath: string) {
|
||||||
if (!persistenceKey) {
|
if (!persistenceKey) {
|
||||||
@@ -2704,8 +2734,18 @@ function WorkspaceScreenContent({
|
|||||||
if (!target) {
|
if (!target) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
openWorkspaceTabFocused(persistenceKey, target);
|
if (isMobile || !supportsDesktopPaneSplits()) {
|
||||||
}, [normalizedWorkspaceId, openWorkspaceTabFocused, persistenceKey]);
|
openWorkspaceTabFocused(persistenceKey, target);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
openWorkspaceTabInExplorerPaneFocused(persistenceKey, { target });
|
||||||
|
}, [
|
||||||
|
isMobile,
|
||||||
|
normalizedWorkspaceId,
|
||||||
|
openWorkspaceTabFocused,
|
||||||
|
openWorkspaceTabInExplorerPaneFocused,
|
||||||
|
persistenceKey,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleBulkCloseTabs = useCallback(
|
const handleBulkCloseTabs = useCallback(
|
||||||
async (input: { tabsToClose: WorkspaceTabDescriptor[]; title: string; logLabel: string }) => {
|
async (input: { tabsToClose: WorkspaceTabDescriptor[]; title: string; logLabel: string }) => {
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ interface ReorderFocusedPaneTabsInLayoutInput {
|
|||||||
interface CloseTabInLayoutInput {
|
interface CloseTabInLayoutInput {
|
||||||
layout: WorkspaceLayout;
|
layout: WorkspaceLayout;
|
||||||
tabId: string;
|
tabId: string;
|
||||||
|
preserveEmptyPaneId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SplitPaneInLayoutInput {
|
interface SplitPaneInLayoutInput {
|
||||||
@@ -211,6 +212,8 @@ export interface WorkspaceTabSnapshot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_PANE_ID = "main";
|
const DEFAULT_PANE_ID = "main";
|
||||||
|
export const DEFAULT_EXPLORER_PANE_ID = "explorer";
|
||||||
|
const DEFAULT_LAYOUT_GROUP_ID = "workspace-root";
|
||||||
|
|
||||||
function trimNonEmpty(value: string | null | undefined): string | null {
|
function trimNonEmpty(value: string | null | undefined): string | null {
|
||||||
if (typeof value !== "string") {
|
if (typeof value !== "string") {
|
||||||
@@ -1051,6 +1054,22 @@ export function createDefaultLayout(): WorkspaceLayout {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The desktop companion pane exists before it is first shown. */
|
||||||
|
export function createWorkspaceLayoutWithExplorer(): WorkspaceLayout {
|
||||||
|
return {
|
||||||
|
root: createGroupNode({
|
||||||
|
id: DEFAULT_LAYOUT_GROUP_ID,
|
||||||
|
direction: "horizontal",
|
||||||
|
children: [
|
||||||
|
createPaneNode({ id: DEFAULT_PANE_ID }),
|
||||||
|
createPaneNode({ id: DEFAULT_EXPLORER_PANE_ID, hidden: true }),
|
||||||
|
],
|
||||||
|
sizes: [0.5, 0.5],
|
||||||
|
}),
|
||||||
|
focusedPaneId: DEFAULT_PANE_ID,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function insertSplit(
|
export function insertSplit(
|
||||||
root: SplitNode,
|
root: SplitNode,
|
||||||
targetPaneId: string,
|
targetPaneId: string,
|
||||||
@@ -1079,7 +1098,10 @@ export function removePaneFromTree(root: SplitNode, paneId: string): SplitNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function removeTabFromTree(root: SplitNode, tabId: string): SplitNode {
|
export function removeTabFromTree(root: SplitNode, tabId: string): SplitNode {
|
||||||
return detachTabFromTree(asInternalNode(root), { tabId }).root;
|
return detachTabFromTree(asInternalNode(root), {
|
||||||
|
tabId,
|
||||||
|
preserveEmptyPaneId: DEFAULT_PANE_ID,
|
||||||
|
}).root;
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertNewTabIntoFocusedPane(input: {
|
function insertNewTabIntoFocusedPane(input: {
|
||||||
@@ -1182,6 +1204,9 @@ export function closeTabInLayout(input: CloseTabInLayoutInput): WorkspaceLayout
|
|||||||
if (!pane) {
|
if (!pane) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
const preserveEmptyPaneId =
|
||||||
|
input.preserveEmptyPaneId ??
|
||||||
|
(pane.id === DEFAULT_PANE_ID || pane.id === DEFAULT_EXPLORER_PANE_ID ? pane.id : null);
|
||||||
|
|
||||||
const closeSuccessorTabId = getCloseSuccessorTabId({
|
const closeSuccessorTabId = getCloseSuccessorTabId({
|
||||||
pane,
|
pane,
|
||||||
@@ -1190,7 +1215,10 @@ export function closeTabInLayout(input: CloseTabInLayoutInput): WorkspaceLayout
|
|||||||
parentTabIdByTabId: input.layout.parentTabIdByTabId,
|
parentTabIdByTabId: input.layout.parentTabIdByTabId,
|
||||||
});
|
});
|
||||||
const fallbackPaneId = findNearestSiblingPaneId(internalLayout.root, pane.id);
|
const fallbackPaneId = findNearestSiblingPaneId(internalLayout.root, pane.id);
|
||||||
const nextRoot = removeTabFromTree(internalLayout.root, input.tabId) as SplitNodeInternal;
|
const nextRoot = detachTabFromTree(internalLayout.root, {
|
||||||
|
tabId: input.tabId,
|
||||||
|
preserveEmptyPaneId,
|
||||||
|
}).root;
|
||||||
const parentTabIdByTabId = normalizeParentTabMap({
|
const parentTabIdByTabId = normalizeParentTabMap({
|
||||||
raw: input.layout.parentTabIdByTabId,
|
raw: input.layout.parentTabIdByTabId,
|
||||||
openTabIds: new Set(collectAllTabs(nextRoot).map((tab) => tab.tabId)),
|
openTabIds: new Set(collectAllTabs(nextRoot).map((tab) => tab.tabId)),
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
collectAllTabs,
|
collectAllTabs,
|
||||||
createWorkspaceLayoutStore,
|
createWorkspaceLayoutStore,
|
||||||
createDefaultLayout,
|
createDefaultLayout,
|
||||||
|
createWorkspaceLayoutWithExplorer,
|
||||||
findPaneById,
|
findPaneById,
|
||||||
findPaneContainingTab,
|
findPaneContainingTab,
|
||||||
getFocusedBrowserId,
|
getFocusedBrowserId,
|
||||||
@@ -329,7 +330,7 @@ describe("workspace-layout-store actions", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps layouts written before split sizes were persisted", async () => {
|
it("migrates legacy layouts to include the hidden registered explorer pane", async () => {
|
||||||
const legacyLayout = createDefaultLayout();
|
const legacyLayout = createDefaultLayout();
|
||||||
await AsyncStorage.setItem(
|
await AsyncStorage.setItem(
|
||||||
"workspace-layout-state",
|
"workspace-layout-state",
|
||||||
@@ -342,7 +343,12 @@ describe("workspace-layout-store actions", () => {
|
|||||||
|
|
||||||
await restored.persist.rehydrate();
|
await restored.persist.rehydrate();
|
||||||
|
|
||||||
expect(restored.getState().layoutByWorkspace.legacy).toEqual(legacyLayout);
|
const state = restored.getState();
|
||||||
|
const layout = state.layoutByWorkspace.legacy;
|
||||||
|
const explorerPaneId = state.explorerPaneIdByWorkspace.legacy;
|
||||||
|
expect(findPaneById(layout.root, "main")).toBeTruthy();
|
||||||
|
expect(explorerPaneId).toBeTruthy();
|
||||||
|
expect(findPaneById(layout.root, explorerPaneId)?.hidden).toBe(true);
|
||||||
expect(restored.getState().splitSizesByWorkspace).toEqual({});
|
expect(restored.getState().splitSizesByWorkspace).toEqual({});
|
||||||
await expect(AsyncStorage.getItem("workspace-layout-state")).resolves.not.toBeNull();
|
await expect(AsyncStorage.getItem("workspace-layout-state")).resolves.not.toBeNull();
|
||||||
});
|
});
|
||||||
@@ -407,6 +413,40 @@ describe("workspace-layout-store actions", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("reveals the registered explorer pane for a first detected pull request", () => {
|
||||||
|
const workspaceKey = createWorkspaceKey();
|
||||||
|
const store = workspaceLayoutStore.getState();
|
||||||
|
|
||||||
|
store.observePullRequest(workspaceKey, "url:https://example.test/pulls/1");
|
||||||
|
|
||||||
|
const state = workspaceLayoutStore.getState();
|
||||||
|
const layout = state.layoutByWorkspace[workspaceKey];
|
||||||
|
const explorerPaneId = state.explorerPaneIdByWorkspace[workspaceKey];
|
||||||
|
expect(explorerPaneId).toBe("explorer");
|
||||||
|
expect(findPaneById(layout.root, explorerPaneId)?.hidden).toBeUndefined();
|
||||||
|
expect(findPaneContainingTab(layout.root, "pull_request")?.id).toBe(explorerPaneId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates setup in the hidden explorer pane without changing focus", () => {
|
||||||
|
const workspaceKey = createWorkspaceKey();
|
||||||
|
const store = workspaceLayoutStore.getState();
|
||||||
|
const agentTabId = store.openTabFocused(workspaceKey, { kind: "agent", agentId: "agent-1" });
|
||||||
|
|
||||||
|
const tabId = store.openTabInExplorerPaneBackground(workspaceKey, {
|
||||||
|
kind: "setup",
|
||||||
|
workspaceId: WORKSPACE_ID,
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = workspaceLayoutStore.getState();
|
||||||
|
const layout = state.layoutByWorkspace[workspaceKey];
|
||||||
|
const explorerPaneId = state.explorerPaneIdByWorkspace[workspaceKey];
|
||||||
|
expect(tabId).toBe("setup_ws-main");
|
||||||
|
expect(layout.focusedPaneId).toBe("main");
|
||||||
|
expect(findPaneById(layout.root, explorerPaneId)?.hidden).toBe(true);
|
||||||
|
expect(findPaneById(layout.root, "main")?.focusedTabId).toBe(agentTabId);
|
||||||
|
expect(findPaneContainingTab(layout.root, tabId!)?.id).toBe(explorerPaneId);
|
||||||
|
});
|
||||||
|
|
||||||
it("places an auto-added pull request in the registered explorer pane", () => {
|
it("places an auto-added pull request in the registered explorer pane", () => {
|
||||||
const workspaceKey = createWorkspaceKey();
|
const workspaceKey = createWorkspaceKey();
|
||||||
const store = workspaceLayoutStore.getState();
|
const store = workspaceLayoutStore.getState();
|
||||||
@@ -445,8 +485,6 @@ describe("workspace-layout-store actions", () => {
|
|||||||
it("opens assistant files in an ensured explorer pane and reveals it on the next open", () => {
|
it("opens assistant files in an ensured explorer pane and reveals it on the next open", () => {
|
||||||
const workspaceKey = createWorkspaceKey();
|
const workspaceKey = createWorkspaceKey();
|
||||||
const store = workspaceLayoutStore.getState();
|
const store = workspaceLayoutStore.getState();
|
||||||
useWorkspaceLayoutIds("explorer");
|
|
||||||
|
|
||||||
const tabId = store.openTabInExplorerPaneFocused(workspaceKey, {
|
const tabId = store.openTabInExplorerPaneFocused(workspaceKey, {
|
||||||
target: { kind: "file", path: "/repo/worktree/a.ts" },
|
target: { kind: "file", path: "/repo/worktree/a.ts" },
|
||||||
parentTabId: "agent_agent-a",
|
parentTabId: "agent_agent-a",
|
||||||
@@ -454,7 +492,7 @@ describe("workspace-layout-store actions", () => {
|
|||||||
const createdState = workspaceLayoutStore.getState();
|
const createdState = workspaceLayoutStore.getState();
|
||||||
const explorerPaneId = createdState.explorerPaneIdByWorkspace[workspaceKey];
|
const explorerPaneId = createdState.explorerPaneIdByWorkspace[workspaceKey];
|
||||||
|
|
||||||
expect(explorerPaneId).toBe("pane_explorer");
|
expect(explorerPaneId).toBe("explorer");
|
||||||
expect(
|
expect(
|
||||||
findPaneContainingTab(createdState.layoutByWorkspace[workspaceKey].root, tabId!)?.id,
|
findPaneContainingTab(createdState.layoutByWorkspace[workspaceKey].root, tabId!)?.id,
|
||||||
).toBe(explorerPaneId);
|
).toBe(explorerPaneId);
|
||||||
@@ -1276,7 +1314,7 @@ describe("workspace-layout-store actions", () => {
|
|||||||
expect(collectAllPanes(layout.root).map((pane) => pane.id)).toEqual(["main"]);
|
expect(collectAllPanes(layout.root).map((pane) => pane.id)).toEqual(["main"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("splitPane enforces the maximum depth of four", () => {
|
it("splitPane preserves four user-created levels beneath the explorer split", () => {
|
||||||
useWorkspaceLayoutIds(
|
useWorkspaceLayoutIds(
|
||||||
"11111111-1111-1111-1111-111111111111",
|
"11111111-1111-1111-1111-111111111111",
|
||||||
"22222222-2222-2222-2222-222222222222",
|
"22222222-2222-2222-2222-222222222222",
|
||||||
@@ -1323,7 +1361,7 @@ describe("workspace-layout-store actions", () => {
|
|||||||
expect(pane2).toBe("pane_33333333-3333-3333-3333-333333333333");
|
expect(pane2).toBe("pane_33333333-3333-3333-3333-333333333333");
|
||||||
expect(pane3).toBe("pane_55555555-5555-5555-5555-555555555555");
|
expect(pane3).toBe("pane_55555555-5555-5555-5555-555555555555");
|
||||||
expect(pane4).toBeNull();
|
expect(pane4).toBeNull();
|
||||||
expect(getTreeDepth(layout.root)).toBe(4);
|
expect(getTreeDepth(layout.root)).toBe(5);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("moveTabToPane collapses the source pane when its last tab moves out", () => {
|
it("moveTabToPane collapses the source pane when its last tab moves out", () => {
|
||||||
@@ -1388,38 +1426,13 @@ describe("workspace-layout-store actions", () => {
|
|||||||
|
|
||||||
store.closeTab(workspaceKey, secondTabId!);
|
store.closeTab(workspaceKey, secondTabId!);
|
||||||
const layout = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
|
const layout = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
|
||||||
const rootGroup = expectGroup(layout.root);
|
|
||||||
|
|
||||||
expect(paneBId).toBe("pane_78787878-7878-7878-7878-787878787878");
|
expect(paneBId).toBe("pane_78787878-7878-7878-7878-787878787878");
|
||||||
expect(paneCId).toBe("pane_9a9a9a9a-9a9a-9a9a-9a9a-9a9a9a9a9a9a");
|
expect(paneCId).toBe("pane_9a9a9a9a-9a9a-9a9a-9a9a-9a9a9a9a9a9a");
|
||||||
expect(layout.focusedPaneId).toBe(paneCId);
|
expect(layout.focusedPaneId).toBe(paneCId);
|
||||||
expect(rootGroup.group.direction).toBe("horizontal");
|
expect(findPaneById(layout.root, "main")?.tabIds).toEqual(["file_/repo/worktree/a.ts"]);
|
||||||
expect(rootGroup.group.children).toHaveLength(2);
|
expect(findPaneById(layout.root, paneCId)?.tabIds).toEqual(["file_/repo/worktree/c.ts"]);
|
||||||
expect(
|
expect(findPaneById(layout.root, "explorer")?.hidden).toBe(true);
|
||||||
rootGroup.group.children.map((child) => {
|
|
||||||
expect(child.kind).toBe("pane");
|
|
||||||
if (child.kind !== "pane") {
|
|
||||||
throw new Error("Expected pane child");
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
id: child.pane.id,
|
|
||||||
tabIds: child.pane.tabIds,
|
|
||||||
focusedTabId: child.pane.focusedTabId,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
).toEqual([
|
|
||||||
{
|
|
||||||
id: "main",
|
|
||||||
tabIds: ["file_/repo/worktree/a.ts"],
|
|
||||||
focusedTabId: "file_/repo/worktree/a.ts",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: paneCId!,
|
|
||||||
tabIds: ["file_/repo/worktree/c.ts"],
|
|
||||||
focusedTabId: "file_/repo/worktree/c.ts",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
expect(rootGroup.group.sizes).toEqual([0.5, 0.5]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("openTab focuses the existing tab instead of creating a duplicate entry", () => {
|
it("openTab focuses the existing tab instead of creating a duplicate entry", () => {
|
||||||
@@ -1546,17 +1559,21 @@ describe("workspace-layout-store actions", () => {
|
|||||||
const farRightPaneId = store.splitPane(workspaceKey, {
|
const farRightPaneId = store.splitPane(workspaceKey, {
|
||||||
tabId: c!,
|
tabId: c!,
|
||||||
targetPaneId: rightPaneId!,
|
targetPaneId: rightPaneId!,
|
||||||
position: "right",
|
position: "bottom",
|
||||||
});
|
});
|
||||||
|
|
||||||
const splitRoot = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey].root;
|
const splitRoot = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey].root;
|
||||||
const splitGroup = expectGroup(splitRoot);
|
const splitGroup = expectGroup(splitRoot);
|
||||||
const nestedGroup = expectGroup(splitGroup.group.children[1]);
|
const nestedGroup = expectGroup(
|
||||||
|
splitGroup.group.children.find((child) => child.kind === "group")!,
|
||||||
|
);
|
||||||
store.resizeSplit(workspaceKey, nestedGroup.group.id, [0.01, 0.99]);
|
store.resizeSplit(workspaceKey, nestedGroup.group.id, [0.01, 0.99]);
|
||||||
|
|
||||||
const resizedRoot = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey].root;
|
const resizedRoot = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey].root;
|
||||||
const resizedGroup = expectGroup(resizedRoot);
|
const resizedGroup = expectGroup(resizedRoot);
|
||||||
const resizedNestedGroup = expectGroup(resizedGroup.group.children[1]);
|
const resizedNestedGroup = expectGroup(
|
||||||
|
resizedGroup.group.children.find((child) => child.kind === "group")!,
|
||||||
|
);
|
||||||
const total = resizedNestedGroup.group.sizes.reduce((sum, size) => sum + size, 0);
|
const total = resizedNestedGroup.group.sizes.reduce((sum, size) => sum + size, 0);
|
||||||
|
|
||||||
expect(rightPaneId).toBe("pane_eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee");
|
expect(rightPaneId).toBe("pane_eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee");
|
||||||
@@ -1574,7 +1591,7 @@ describe("workspace-layout-store actions", () => {
|
|||||||
store.closeTab(workspaceKey, tabId!);
|
store.closeTab(workspaceKey, tabId!);
|
||||||
const layout = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
|
const layout = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
|
||||||
|
|
||||||
expect(layout).toEqual(createDefaultLayout());
|
expect(layout).toEqual(createWorkspaceLayoutWithExplorer());
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps pinned archived agents in memory per workspace without persisting them", () => {
|
it("keeps pinned archived agents in memory per workspace without persisting them", () => {
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import {
|
|||||||
collectAllTabs,
|
collectAllTabs,
|
||||||
convertDraftToAgentInLayout,
|
convertDraftToAgentInLayout,
|
||||||
createDefaultLayout,
|
createDefaultLayout,
|
||||||
|
createWorkspaceLayoutWithExplorer,
|
||||||
|
DEFAULT_EXPLORER_PANE_ID,
|
||||||
findPaneById,
|
findPaneById,
|
||||||
findPaneContainingTab,
|
findPaneContainingTab,
|
||||||
focusPaneInLayout,
|
focusPaneInLayout,
|
||||||
@@ -50,6 +52,7 @@ export {
|
|||||||
collectAllPanes,
|
collectAllPanes,
|
||||||
collectAllTabs,
|
collectAllTabs,
|
||||||
createDefaultLayout,
|
createDefaultLayout,
|
||||||
|
createWorkspaceLayoutWithExplorer,
|
||||||
findPaneById,
|
findPaneById,
|
||||||
findPaneContainingTab,
|
findPaneContainingTab,
|
||||||
getFocusedBrowserId,
|
getFocusedBrowserId,
|
||||||
@@ -85,6 +88,10 @@ interface WorkspaceLayoutStore {
|
|||||||
parentTabId: string,
|
parentTabId: string,
|
||||||
) => string | null;
|
) => string | null;
|
||||||
openTabInBackground: (workspaceKey: string, target: WorkspaceTabTarget) => string | null;
|
openTabInBackground: (workspaceKey: string, target: WorkspaceTabTarget) => string | null;
|
||||||
|
openTabInExplorerPaneBackground: (
|
||||||
|
workspaceKey: string,
|
||||||
|
target: WorkspaceTabTarget,
|
||||||
|
) => string | null;
|
||||||
ensureExplorerPane: (workspaceKey: string) => EnsureExplorerPaneResult | null;
|
ensureExplorerPane: (workspaceKey: string) => EnsureExplorerPaneResult | null;
|
||||||
openTabInExplorerPaneFocused: (
|
openTabInExplorerPaneFocused: (
|
||||||
workspaceKey: string,
|
workspaceKey: string,
|
||||||
@@ -140,7 +147,9 @@ interface WorkspaceFocusRestorationState {
|
|||||||
tokens: string[];
|
tokens: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_TREE_DEPTH = 4;
|
// The root companion split is always present for the hidden explorer pane.
|
||||||
|
// Preserve four user-created split levels beneath it.
|
||||||
|
const MAX_TREE_DEPTH = 5;
|
||||||
|
|
||||||
const WorkspaceDraftTabSetupStorageSchema = z.strictObject({
|
const WorkspaceDraftTabSetupStorageSchema = z.strictObject({
|
||||||
provider: z.string(),
|
provider: z.string(),
|
||||||
@@ -293,7 +302,52 @@ function getWorkspaceLayout(
|
|||||||
state: Record<string, WorkspaceLayout>,
|
state: Record<string, WorkspaceLayout>,
|
||||||
workspaceKey: string,
|
workspaceKey: string,
|
||||||
): WorkspaceLayout {
|
): WorkspaceLayout {
|
||||||
return normalizeLayout(state[workspaceKey] ?? createDefaultLayout());
|
return normalizeLayout(state[workspaceKey] ?? createWorkspaceLayoutWithExplorer());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveExplorerPaneId(
|
||||||
|
layout: WorkspaceLayout,
|
||||||
|
registeredPaneId: string | null | undefined,
|
||||||
|
): string | null {
|
||||||
|
const registeredPane = findPaneById(layout.root, registeredPaneId ?? null);
|
||||||
|
if (registeredPane) {
|
||||||
|
return registeredPane.id;
|
||||||
|
}
|
||||||
|
const defaultPane = findPaneById(layout.root, DEFAULT_EXPLORER_PANE_ID);
|
||||||
|
return defaultPane?.id ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensurePersistedExplorerPane(input: {
|
||||||
|
layout: WorkspaceLayout;
|
||||||
|
registeredPaneId: string | null | undefined;
|
||||||
|
ids: WorkspaceLayoutIdSource;
|
||||||
|
}): { layout: WorkspaceLayout; paneId: string } | null {
|
||||||
|
const existingPaneId = resolveExplorerPaneId(input.layout, input.registeredPaneId);
|
||||||
|
if (existingPaneId) {
|
||||||
|
return { layout: input.layout, paneId: existingPaneId };
|
||||||
|
}
|
||||||
|
const targetPaneId =
|
||||||
|
findPaneById(input.layout.root, input.layout.focusedPaneId)?.id ??
|
||||||
|
collectAllPanes(input.layout.root)[0]?.id;
|
||||||
|
if (!targetPaneId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const split = splitPaneEmptyInLayout({
|
||||||
|
layout: input.layout,
|
||||||
|
targetPaneId,
|
||||||
|
position: "right",
|
||||||
|
createNodeId: input.ids.createNodeId,
|
||||||
|
maxTreeDepth: MAX_TREE_DEPTH,
|
||||||
|
});
|
||||||
|
if (!split) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const hiddenLayout = setPaneHiddenInLayout({
|
||||||
|
layout: split.layout,
|
||||||
|
paneId: split.paneId,
|
||||||
|
hidden: true,
|
||||||
|
});
|
||||||
|
return { layout: hiddenLayout ?? split.layout, paneId: split.paneId };
|
||||||
}
|
}
|
||||||
|
|
||||||
function withoutFocusRestoration(
|
function withoutFocusRestoration(
|
||||||
@@ -447,6 +501,71 @@ export function createWorkspaceLayoutStore(
|
|||||||
|
|
||||||
return result.tabId;
|
return result.tabId;
|
||||||
},
|
},
|
||||||
|
openTabInExplorerPaneBackground: (workspaceKey, target) => {
|
||||||
|
const normalizedWorkspaceKey = trimNonEmpty(workspaceKey);
|
||||||
|
const normalizedTarget = normalizeWorkspaceTabTarget(target);
|
||||||
|
if (!normalizedWorkspaceKey || !normalizedTarget) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let tabId: string | null = null;
|
||||||
|
set((state) => {
|
||||||
|
const layout = getWorkspaceLayout(state.layoutByWorkspace, normalizedWorkspaceKey);
|
||||||
|
const explorerPaneId = resolveExplorerPaneId(
|
||||||
|
layout,
|
||||||
|
state.explorerPaneIdByWorkspace[normalizedWorkspaceKey],
|
||||||
|
);
|
||||||
|
const explorerPane = findPaneById(layout.root, explorerPaneId);
|
||||||
|
if (!explorerPane) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalFocusedPaneId = layout.focusedPaneId;
|
||||||
|
const revealedLayout = focusPaneInLayout({ layout, paneId: explorerPane.id }) ?? layout;
|
||||||
|
const result = openTabInLayoutBackground({
|
||||||
|
layout: revealedLayout,
|
||||||
|
target: normalizedTarget,
|
||||||
|
now: Date.now(),
|
||||||
|
});
|
||||||
|
const currentPane = findPaneContainingTab(result.layout.root, result.tabId);
|
||||||
|
const placedLayout =
|
||||||
|
currentPane?.id === explorerPane.id
|
||||||
|
? result.layout
|
||||||
|
: (moveTabToPaneInLayout({
|
||||||
|
layout: result.layout,
|
||||||
|
tabId: result.tabId,
|
||||||
|
toPaneId: explorerPane.id,
|
||||||
|
}) ?? result.layout);
|
||||||
|
const restoredVisibility = explorerPane.hidden
|
||||||
|
? (setPaneHiddenInLayout({
|
||||||
|
layout: placedLayout,
|
||||||
|
paneId: explorerPane.id,
|
||||||
|
hidden: true,
|
||||||
|
}) ?? placedLayout)
|
||||||
|
: placedLayout;
|
||||||
|
const restoredLayout =
|
||||||
|
originalFocusedPaneId && originalFocusedPaneId !== explorerPane.id
|
||||||
|
? (focusPaneInLayout({
|
||||||
|
layout: restoredVisibility,
|
||||||
|
paneId: originalFocusedPaneId,
|
||||||
|
}) ?? restoredVisibility)
|
||||||
|
: restoredVisibility;
|
||||||
|
|
||||||
|
tabId = result.tabId;
|
||||||
|
return {
|
||||||
|
...withoutFocusRestoration(state, normalizedWorkspaceKey),
|
||||||
|
explorerPaneIdByWorkspace: {
|
||||||
|
...state.explorerPaneIdByWorkspace,
|
||||||
|
[normalizedWorkspaceKey]: explorerPane.id,
|
||||||
|
},
|
||||||
|
layoutByWorkspace: {
|
||||||
|
...state.layoutByWorkspace,
|
||||||
|
[normalizedWorkspaceKey]: restoredLayout,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return tabId;
|
||||||
|
},
|
||||||
ensureExplorerPane: (workspaceKey) => {
|
ensureExplorerPane: (workspaceKey) => {
|
||||||
const normalizedWorkspaceKey = trimNonEmpty(workspaceKey);
|
const normalizedWorkspaceKey = trimNonEmpty(workspaceKey);
|
||||||
if (!normalizedWorkspaceKey) {
|
if (!normalizedWorkspaceKey) {
|
||||||
@@ -455,9 +574,15 @@ export function createWorkspaceLayoutStore(
|
|||||||
|
|
||||||
const store = get();
|
const store = get();
|
||||||
const layout = getWorkspaceLayout(store.layoutByWorkspace, normalizedWorkspaceKey);
|
const layout = getWorkspaceLayout(store.layoutByWorkspace, normalizedWorkspaceKey);
|
||||||
const explorerPaneId = store.explorerPaneIdByWorkspace[normalizedWorkspaceKey] ?? null;
|
const explorerPaneId = resolveExplorerPaneId(
|
||||||
|
layout,
|
||||||
|
store.explorerPaneIdByWorkspace[normalizedWorkspaceKey],
|
||||||
|
);
|
||||||
const explorerPane = findPaneById(layout.root, explorerPaneId);
|
const explorerPane = findPaneById(layout.root, explorerPaneId);
|
||||||
if (explorerPane) {
|
if (explorerPane) {
|
||||||
|
if (store.explorerPaneIdByWorkspace[normalizedWorkspaceKey] !== explorerPane.id) {
|
||||||
|
store.setExplorerPaneId(normalizedWorkspaceKey, explorerPane.id);
|
||||||
|
}
|
||||||
store.focusPane(normalizedWorkspaceKey, explorerPane.id);
|
store.focusPane(normalizedWorkspaceKey, explorerPane.id);
|
||||||
return { paneId: explorerPane.id, created: false };
|
return { paneId: explorerPane.id, created: false };
|
||||||
}
|
}
|
||||||
@@ -521,11 +646,16 @@ export function createWorkspaceLayoutStore(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const layout = getWorkspaceLayout(state.layoutByWorkspace, normalizedWorkspaceKey);
|
const layout = getWorkspaceLayout(state.layoutByWorkspace, normalizedWorkspaceKey);
|
||||||
const explorerPaneId = state.explorerPaneIdByWorkspace[normalizedWorkspaceKey] ?? null;
|
const explorerPaneId = resolveExplorerPaneId(
|
||||||
|
layout,
|
||||||
|
state.explorerPaneIdByWorkspace[normalizedWorkspaceKey],
|
||||||
|
);
|
||||||
const explorerPane = findPaneById(layout.root, explorerPaneId);
|
const explorerPane = findPaneById(layout.root, explorerPaneId);
|
||||||
const placementLayout = explorerPane
|
if (!explorerPane) {
|
||||||
? (focusPaneInLayout({ layout, paneId: explorerPane.id }) ?? layout)
|
return state;
|
||||||
: layout;
|
}
|
||||||
|
const placementLayout =
|
||||||
|
focusPaneInLayout({ layout, paneId: explorerPane.id }) ?? layout;
|
||||||
const result = openTabInLayoutFocused({
|
const result = openTabInLayoutFocused({
|
||||||
layout: placementLayout,
|
layout: placementLayout,
|
||||||
target: { kind: "pull_request" },
|
target: { kind: "pull_request" },
|
||||||
@@ -538,6 +668,10 @@ export function createWorkspaceLayoutStore(
|
|||||||
...state.acknowledgedPullRequestByWorkspace,
|
...state.acknowledgedPullRequestByWorkspace,
|
||||||
[normalizedWorkspaceKey]: normalizedIdentity,
|
[normalizedWorkspaceKey]: normalizedIdentity,
|
||||||
},
|
},
|
||||||
|
explorerPaneIdByWorkspace: {
|
||||||
|
...state.explorerPaneIdByWorkspace,
|
||||||
|
[normalizedWorkspaceKey]: explorerPane.id,
|
||||||
|
},
|
||||||
layoutByWorkspace: {
|
layoutByWorkspace: {
|
||||||
...state.layoutByWorkspace,
|
...state.layoutByWorkspace,
|
||||||
[normalizedWorkspaceKey]: result.layout,
|
[normalizedWorkspaceKey]: result.layout,
|
||||||
@@ -553,9 +687,20 @@ export function createWorkspaceLayoutStore(
|
|||||||
}
|
}
|
||||||
|
|
||||||
set((state) => {
|
set((state) => {
|
||||||
|
const layout = getWorkspaceLayout(state.layoutByWorkspace, normalizedWorkspaceKey);
|
||||||
|
const explorerPaneId = resolveExplorerPaneId(
|
||||||
|
layout,
|
||||||
|
state.explorerPaneIdByWorkspace[normalizedWorkspaceKey],
|
||||||
|
);
|
||||||
|
const closingPane = findPaneContainingTab(layout.root, normalizedTabId);
|
||||||
|
const preserveEmptyPaneId =
|
||||||
|
closingPane?.id === "main" || closingPane?.id === explorerPaneId
|
||||||
|
? closingPane.id
|
||||||
|
: null;
|
||||||
const nextLayout = closeTabInLayout({
|
const nextLayout = closeTabInLayout({
|
||||||
layout: getWorkspaceLayout(state.layoutByWorkspace, normalizedWorkspaceKey),
|
layout,
|
||||||
tabId: normalizedTabId,
|
tabId: normalizedTabId,
|
||||||
|
preserveEmptyPaneId,
|
||||||
});
|
});
|
||||||
if (!nextLayout) {
|
if (!nextLayout) {
|
||||||
return state;
|
return state;
|
||||||
@@ -1252,11 +1397,30 @@ export function createWorkspaceLayoutStore(
|
|||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return currentState;
|
return currentState;
|
||||||
}
|
}
|
||||||
|
const layoutByWorkspace: Record<string, WorkspaceLayout> = {};
|
||||||
|
const explorerPaneIdByWorkspace: Record<string, string | null> = {
|
||||||
|
...result.data.explorerPaneIdByWorkspace,
|
||||||
|
};
|
||||||
|
for (const [workspaceKey, persistedLayout] of Object.entries(
|
||||||
|
result.data.layoutByWorkspace,
|
||||||
|
)) {
|
||||||
|
const explorer = ensurePersistedExplorerPane({
|
||||||
|
layout: normalizeLayout(persistedLayout),
|
||||||
|
registeredPaneId: explorerPaneIdByWorkspace[workspaceKey],
|
||||||
|
ids,
|
||||||
|
});
|
||||||
|
if (!explorer) {
|
||||||
|
layoutByWorkspace[workspaceKey] = normalizeLayout(persistedLayout);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
layoutByWorkspace[workspaceKey] = explorer.layout;
|
||||||
|
explorerPaneIdByWorkspace[workspaceKey] = explorer.paneId;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...currentState,
|
...currentState,
|
||||||
layoutByWorkspace: result.data.layoutByWorkspace,
|
layoutByWorkspace,
|
||||||
splitSizesByWorkspace: result.data.splitSizesByWorkspace ?? {},
|
splitSizesByWorkspace: result.data.splitSizesByWorkspace ?? {},
|
||||||
explorerPaneIdByWorkspace: result.data.explorerPaneIdByWorkspace ?? {},
|
explorerPaneIdByWorkspace,
|
||||||
acknowledgedPullRequestByWorkspace:
|
acknowledgedPullRequestByWorkspace:
|
||||||
result.data.acknowledgedPullRequestByWorkspace ?? {},
|
result.data.acknowledgedPullRequestByWorkspace ?? {},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user