Refactor workspace layout id generation (#876)

This commit is contained in:
Mohamed Boudra
2026-05-10 07:34:11 +00:00
committed by GitHub
parent 183cda2b66
commit 2d0ed004e2
4 changed files with 796 additions and 777 deletions
@@ -1,5 +1,7 @@
import invariant from "tiny-invariant";
import type { WorkspaceTab, WorkspaceTabTarget } from "@/stores/workspace-tabs-store";
import { defaultWorkspaceLayoutIds } from "@/stores/workspace-layout-ids";
import type { WorkspaceLayoutNodeIdPrefix } from "@/stores/workspace-layout-ids";
import {
buildDeterministicWorkspaceTabId,
normalizeWorkspaceTabTarget,
@@ -86,7 +88,7 @@ interface InsertSplitInternalInput {
targetPaneId: string;
tabId: string;
position: "left" | "right" | "top" | "bottom";
createNodeId: (prefix: "pane" | "group") => string;
createNodeId: (prefix: WorkspaceLayoutNodeIdPrefix) => string;
}
interface InsertSplitInternalResult {
@@ -142,7 +144,7 @@ interface SplitPaneInLayoutInput {
tabId: string;
targetPaneId: string;
position: "left" | "right" | "top" | "bottom";
createNodeId: (prefix: "pane" | "group") => string;
createNodeId: (prefix: WorkspaceLayoutNodeIdPrefix) => string;
maxTreeDepth: number;
}
@@ -155,7 +157,7 @@ interface SplitPaneEmptyInLayoutInput {
layout: WorkspaceLayout;
targetPaneId: string;
position: "left" | "right" | "top" | "bottom";
createNodeId: (prefix: "pane" | "group") => string;
createNodeId: (prefix: WorkspaceLayoutNodeIdPrefix) => string;
maxTreeDepth: number;
}
@@ -232,14 +234,6 @@ function normalizeTabIds(list: unknown): string[] {
return next;
}
function generateNodeId(prefix: "pane" | "group"): string {
const randomValue =
typeof globalThis.crypto?.randomUUID === "function"
? globalThis.crypto.randomUUID()
: `${Date.now()}-${Math.random().toString(16).slice(2)}`;
return `${prefix}_${randomValue}`;
}
function createPaneNode(input: {
id: string;
tabs?: WorkspaceTab[];
@@ -996,13 +990,16 @@ export function insertSplit(
targetPaneId: string,
tabId: string,
position: "left" | "right" | "top" | "bottom",
createNodeId: (
prefix: WorkspaceLayoutNodeIdPrefix,
) => string = defaultWorkspaceLayoutIds.createNodeId,
): SplitNode {
return insertSplitInternal({
root: asInternalNode(root),
targetPaneId,
tabId,
position,
createNodeId: generateNodeId,
createNodeId,
}).root;
}
@@ -0,0 +1,17 @@
export type WorkspaceLayoutNodeIdPrefix = "pane" | "group";
export interface WorkspaceLayoutIdSource {
createNodeId: (prefix: WorkspaceLayoutNodeIdPrefix) => string;
createFocusRestorationToken: () => string;
}
function createRandomIdValue(): string {
return typeof globalThis.crypto?.randomUUID === "function"
? globalThis.crypto.randomUUID()
: `${Date.now()}-${Math.random().toString(16).slice(2)}`;
}
export const defaultWorkspaceLayoutIds: WorkspaceLayoutIdSource = {
createNodeId: (prefix) => `${prefix}_${createRandomIdValue()}`,
createFocusRestorationToken: () => `workspace-focus-${createRandomIdValue()}`,
};
@@ -20,6 +20,7 @@ import {
buildWorkspaceTabPersistenceKey,
collectAllPanes,
collectAllTabs,
createWorkspaceLayoutStore,
createDefaultLayout,
findPaneById,
findPaneContainingTab,
@@ -28,7 +29,6 @@ import {
insertSplit,
removePaneFromTree,
removeTabFromTree,
useWorkspaceLayoutStore,
type SplitNode,
type SplitPane,
} from "@/stores/workspace-layout-store";
@@ -36,6 +36,40 @@ import {
const SERVER_ID = "server-1";
const WORKSPACE_ID = "ws-main";
function createDeterministicWorkspaceLayoutIds() {
let values: string[] = [];
let fallbackIndex = 0;
function nextValue(): string {
const value = values.shift();
if (value) {
return value;
}
fallbackIndex += 1;
return `generated-${fallbackIndex}`;
}
return {
useValues: (nextValues: string[]) => {
values = nextValues.slice();
fallbackIndex = 0;
},
reset: () => {
values = [];
fallbackIndex = 0;
},
createNodeId: (prefix: "pane" | "group") => `${prefix}_${nextValue()}`,
createFocusRestorationToken: () => `workspace-focus-${nextValue()}`,
};
}
const workspaceLayoutIds = createDeterministicWorkspaceLayoutIds();
const workspaceLayoutStore = createWorkspaceLayoutStore(workspaceLayoutIds);
function useWorkspaceLayoutIds(...values: string[]) {
workspaceLayoutIds.useValues(values);
}
function createTab(tabId: string, target?: WorkspaceTab["target"]): WorkspaceTab {
return {
tabId,
@@ -168,13 +202,14 @@ describe("workspace-layout-store helpers", () => {
describe("workspace-layout-store tree transforms", () => {
beforeEach(() => {
vi.restoreAllMocks();
workspaceLayoutIds.reset();
});
it("insertSplit wraps root-level same-direction splits in a nested group", () => {
vi.spyOn(globalThis.crypto, "randomUUID")
.mockReturnValueOnce("11111111-1111-1111-1111-111111111111")
.mockReturnValueOnce("22222222-2222-2222-2222-222222222222");
useWorkspaceLayoutIds(
"11111111-1111-1111-1111-111111111111",
"22222222-2222-2222-2222-222222222222",
);
const root: SplitNode = {
kind: "group",
@@ -189,7 +224,7 @@ describe("workspace-layout-store tree transforms", () => {
},
};
const nextRoot = insertSplit(root, "right", "tab-c", "right");
const nextRoot = insertSplit(root, "right", "tab-c", "right", workspaceLayoutIds.createNodeId);
const nextGroup = expectGroup(nextRoot);
const nestedGroup = expectGroup(nextGroup.group.children[1]);
@@ -270,22 +305,20 @@ describe("workspace-layout-store tree transforms", () => {
describe("workspace-layout-store actions", () => {
beforeEach(() => {
useWorkspaceLayoutStore.setState({
workspaceLayoutIds.reset();
workspaceLayoutStore.setState({
layoutByWorkspace: {},
splitSizesByWorkspace: {},
pinnedAgentIdsByWorkspace: {},
hiddenAgentIdsByWorkspace: {},
focusRestorationByWorkspace: {},
});
vi.restoreAllMocks();
});
it("opens tabs into the focused pane and focuses duplicate opens instead of creating them", () => {
vi.spyOn(globalThis.crypto, "randomUUID").mockReturnValue(
"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
);
useWorkspaceLayoutIds("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
const workspaceKey = createWorkspaceKey();
const store = useWorkspaceLayoutStore.getState();
const store = workspaceLayoutStore.getState();
const firstTabId = store.openTabFocused(workspaceKey, {
kind: "file",
@@ -308,7 +341,7 @@ describe("workspace-layout-store actions", () => {
kind: "file",
path: "/repo/worktree/b.ts",
});
const layout = useWorkspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
const layout = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
expect(firstTabId).toBe("file_/repo/worktree/a.ts");
expect(secondTabId).toBe("file_/repo/worktree/b.ts");
@@ -322,14 +355,14 @@ describe("workspace-layout-store actions", () => {
it("openTabInBackground inserts a tab without stealing focus", () => {
const workspaceKey = createWorkspaceKey();
const store = useWorkspaceLayoutStore.getState();
const store = workspaceLayoutStore.getState();
const agentTabId = store.openTabFocused(workspaceKey, { kind: "agent", agentId: "agent-1" });
const setupTabId = store.openTabInBackground(workspaceKey, {
kind: "setup",
workspaceId: "ws-main",
});
const layout = useWorkspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
const layout = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
const pane = findPaneById(layout.root, "main")!;
expect(agentTabId).toBe("agent_agent-1");
@@ -341,7 +374,7 @@ describe("workspace-layout-store actions", () => {
it("openTabInBackground on an existing target is a no-op", () => {
const workspaceKey = createWorkspaceKey();
const store = useWorkspaceLayoutStore.getState();
const store = workspaceLayoutStore.getState();
const firstTabId = store.openTabFocused(workspaceKey, {
kind: "file",
@@ -355,7 +388,7 @@ describe("workspace-layout-store actions", () => {
kind: "file",
path: "/repo/worktree/a.ts",
});
const layoutAfter = useWorkspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
const layoutAfter = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
const pane = findPaneById(layoutAfter.root, "main")!;
expect(duplicateTabId).toBe(firstTabId);
@@ -365,27 +398,25 @@ describe("workspace-layout-store actions", () => {
it("unfocuses and restores the previous focused pane", () => {
const workspaceKey = createWorkspaceKey();
const store = useWorkspaceLayoutStore.getState();
const store = workspaceLayoutStore.getState();
store.openTabFocused(workspaceKey, { kind: "agent", agentId: "agent-1" });
const token = store.unfocusPane(workspaceKey);
expect(token).toBeTruthy();
expect(
useWorkspaceLayoutStore.getState().layoutByWorkspace[workspaceKey]?.focusedPaneId,
workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey]?.focusedPaneId,
).toBeNull();
store.restorePaneFocus(workspaceKey, token!);
expect(useWorkspaceLayoutStore.getState().layoutByWorkspace[workspaceKey]?.focusedPaneId).toBe(
expect(workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey]?.focusedPaneId).toBe(
"main",
);
});
it("does not restore stale focus after another pane is focused", () => {
vi.spyOn(globalThis.crypto, "randomUUID").mockReturnValue(
"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
);
useWorkspaceLayoutIds("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb");
const workspaceKey = createWorkspaceKey();
const store = useWorkspaceLayoutStore.getState();
const store = workspaceLayoutStore.getState();
const firstTabId = store.openTabFocused(workspaceKey, { kind: "draft", draftId: "draft-1" });
store.splitPane(workspaceKey, {
@@ -399,14 +430,14 @@ describe("workspace-layout-store actions", () => {
store.focusPane(workspaceKey, "pane_bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb");
store.restorePaneFocus(workspaceKey, token!);
expect(useWorkspaceLayoutStore.getState().layoutByWorkspace[workspaceKey]?.focusedPaneId).toBe(
expect(workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey]?.focusedPaneId).toBe(
"pane_bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
);
});
it("waits for nested focus restorations before restoring", () => {
const workspaceKey = createWorkspaceKey();
const store = useWorkspaceLayoutStore.getState();
const store = workspaceLayoutStore.getState();
store.openTabFocused(workspaceKey, { kind: "agent", agentId: "agent-1" });
const outerToken = store.unfocusPane(workspaceKey);
@@ -414,22 +445,22 @@ describe("workspace-layout-store actions", () => {
store.restorePaneFocus(workspaceKey, outerToken!);
expect(
useWorkspaceLayoutStore.getState().layoutByWorkspace[workspaceKey]?.focusedPaneId,
workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey]?.focusedPaneId,
).toBeNull();
store.restorePaneFocus(workspaceKey, innerToken!);
expect(useWorkspaceLayoutStore.getState().layoutByWorkspace[workspaceKey]?.focusedPaneId).toBe(
expect(workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey]?.focusedPaneId).toBe(
"main",
);
});
it("openTab creates distinct draft tabs for repeated Cmd+T/new-tab opens", () => {
const workspaceKey = createWorkspaceKey();
const store = useWorkspaceLayoutStore.getState();
const store = workspaceLayoutStore.getState();
const firstTabId = store.openTabFocused(workspaceKey, { kind: "draft", draftId: "draft-1" });
const secondTabId = store.openTabFocused(workspaceKey, { kind: "draft", draftId: "draft-2" });
const layout = useWorkspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
const layout = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
expect(firstTabId).toBe("draft-1");
expect(secondTabId).toBe("draft-2");
@@ -450,11 +481,9 @@ describe("workspace-layout-store actions", () => {
});
it("splitPaneEmpty plus openTab opens a draft tab in the new pane", () => {
vi.spyOn(globalThis.crypto, "randomUUID").mockReturnValueOnce(
"77777777-7777-7777-7777-777777777777",
);
useWorkspaceLayoutIds("77777777-7777-7777-7777-777777777777");
const workspaceKey = createWorkspaceKey();
const store = useWorkspaceLayoutStore.getState();
const store = workspaceLayoutStore.getState();
store.openTabFocused(workspaceKey, { kind: "file", path: "/repo/worktree/a.ts" });
const newPaneId = store.splitPaneEmpty(workspaceKey, {
@@ -465,7 +494,7 @@ describe("workspace-layout-store actions", () => {
kind: "draft",
draftId: "draft-split",
});
const layout = useWorkspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
const layout = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
expect(newPaneId).toBe("pane_77777777-7777-7777-7777-777777777777");
expect(draftTabId).toBe("draft-split");
@@ -476,11 +505,9 @@ describe("workspace-layout-store actions", () => {
});
it("focusTab moves workspace focus to the pane containing the tab", () => {
vi.spyOn(globalThis.crypto, "randomUUID").mockReturnValue(
"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
);
useWorkspaceLayoutIds("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb");
const workspaceKey = createWorkspaceKey();
const store = useWorkspaceLayoutStore.getState();
const store = workspaceLayoutStore.getState();
const fileTabId = store.openTabFocused(workspaceKey, {
kind: "file",
@@ -497,22 +524,20 @@ describe("workspace-layout-store actions", () => {
});
store.focusTab(workspaceKey, fileTabId!);
let layout = useWorkspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
let layout = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
expect(layout.focusedPaneId).toBe("main");
store.focusTab(workspaceKey, terminalTabId!);
layout = useWorkspaceLayoutStore.getState().layoutByWorkspace[workspaceKey]!;
layout = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey]!;
expect(splitPaneId).toBe("pane_bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb");
expect(layout.focusedPaneId).toBe(splitPaneId);
expect(findPaneById(layout.root, splitPaneId)?.focusedTabId).toBe(terminalTabId);
});
it("convertDraftToAgent replaces the draft tab with a canonical agent tab in the same pane", () => {
vi.spyOn(globalThis.crypto, "randomUUID").mockReturnValue(
"12121212-1212-1212-1212-121212121212",
);
useWorkspaceLayoutIds("12121212-1212-1212-1212-121212121212");
const workspaceKey = createWorkspaceKey();
const store = useWorkspaceLayoutStore.getState();
const store = workspaceLayoutStore.getState();
store.openTabFocused(workspaceKey, { kind: "file", path: "/repo/worktree/a.ts" });
const secondTabId = store.openTabFocused(workspaceKey, { kind: "draft", draftId: "draft-2" });
@@ -523,7 +548,7 @@ describe("workspace-layout-store actions", () => {
});
const nextTabId = store.convertDraftToAgent(workspaceKey, secondTabId!, "agent-1");
const layout = useWorkspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
const layout = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
const splitPane = findPaneById(layout.root, splitPaneId);
const convertedTab = collectAllTabs(layout.root).find((tab) => tab.tabId === nextTabId);
@@ -540,7 +565,7 @@ describe("workspace-layout-store actions", () => {
it("retargetTab keeps a draft tab in place while updating its target", () => {
const workspaceKey = createWorkspaceKey();
const store = useWorkspaceLayoutStore.getState();
const store = workspaceLayoutStore.getState();
const draftTabId = store.openTabFocused(workspaceKey, {
kind: "draft",
@@ -550,7 +575,7 @@ describe("workspace-layout-store actions", () => {
kind: "file",
path: "/repo/worktree/retargeted.ts",
});
const layout = useWorkspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
const layout = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
expect(draftTabId).toBe("draft-retarget");
expect(nextTabId).toBe(draftTabId);
@@ -565,11 +590,9 @@ describe("workspace-layout-store actions", () => {
});
it("retargetTab closes a draft tab and focuses the existing canonical target tab", () => {
vi.spyOn(globalThis.crypto, "randomUUID").mockReturnValueOnce(
"55555555-5555-5555-5555-555555555555",
);
useWorkspaceLayoutIds("55555555-5555-5555-5555-555555555555");
const workspaceKey = createWorkspaceKey();
const store = useWorkspaceLayoutStore.getState();
const store = workspaceLayoutStore.getState();
const existingFileTabId = store.openTabFocused(workspaceKey, {
kind: "file",
@@ -590,7 +613,7 @@ describe("workspace-layout-store actions", () => {
kind: "file",
path: "/repo/worktree/existing.ts",
});
const layout = useWorkspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
const layout = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
expect(existingFileTabId).toBe("file_/repo/worktree/existing.ts");
expect(draftTabId).toBe("draft-dup");
@@ -606,7 +629,7 @@ describe("workspace-layout-store actions", () => {
it("retargetTab closes a draft tab and focuses an existing matching target tab", () => {
const workspaceKey = createWorkspaceKey();
const store = useWorkspaceLayoutStore.getState();
const store = workspaceLayoutStore.getState();
const firstDraftTabId = store.openTabFocused(workspaceKey, {
kind: "draft",
@@ -625,7 +648,7 @@ describe("workspace-layout-store actions", () => {
kind: "agent",
agentId: "agent-1",
});
const layout = useWorkspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
const layout = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
expect(firstAgentTabId).toBe(firstDraftTabId);
expect(nextTabId).toBe(firstDraftTabId);
@@ -641,7 +664,7 @@ describe("workspace-layout-store actions", () => {
it("reorderTabs reorders tabs within the focused pane", () => {
const workspaceKey = createWorkspaceKey();
const store = useWorkspaceLayoutStore.getState();
const store = workspaceLayoutStore.getState();
const firstTabId = store.openTabFocused(workspaceKey, {
kind: "file",
@@ -657,7 +680,7 @@ describe("workspace-layout-store actions", () => {
});
store.reorderTabs(workspaceKey, [thirdTabId!, firstTabId!]);
const layout = useWorkspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
const layout = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
expect(findPaneById(layout.root, "main")).toEqual({
id: "main",
@@ -684,11 +707,9 @@ describe("workspace-layout-store actions", () => {
});
it("reorderTabsInPane reorders tabs in the requested pane without changing focused pane", () => {
vi.spyOn(globalThis.crypto, "randomUUID").mockReturnValue(
"34343434-3434-3434-3434-343434343434",
);
useWorkspaceLayoutIds("34343434-3434-3434-3434-343434343434");
const workspaceKey = createWorkspaceKey();
const store = useWorkspaceLayoutStore.getState();
const store = workspaceLayoutStore.getState();
store.openTabFocused(workspaceKey, { kind: "file", path: "/repo/worktree/a.ts" });
store.openTabFocused(workspaceKey, {
@@ -712,7 +733,7 @@ describe("workspace-layout-store actions", () => {
store.moveTabToPane(workspaceKey, fourthTabId!, splitPaneId!);
store.focusPane(workspaceKey, "main");
store.reorderTabsInPane(workspaceKey, splitPaneId!, [fourthTabId!, thirdTabId!]);
const layout = useWorkspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
const layout = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
expect(splitPaneId).toBe("pane_34343434-3434-3434-3434-343434343434");
expect(layout.focusedPaneId).toBe("main");
@@ -736,11 +757,9 @@ describe("workspace-layout-store actions", () => {
});
it("focusPane switches workspace focus to a different pane", () => {
vi.spyOn(globalThis.crypto, "randomUUID").mockReturnValue(
"56565656-5656-5656-5656-565656565656",
);
useWorkspaceLayoutIds("56565656-5656-5656-5656-565656565656");
const workspaceKey = createWorkspaceKey();
const store = useWorkspaceLayoutStore.getState();
const store = workspaceLayoutStore.getState();
store.openTabFocused(workspaceKey, { kind: "file", path: "/repo/worktree/a.ts" });
const secondTabId = store.openTabFocused(workspaceKey, {
@@ -754,22 +773,20 @@ describe("workspace-layout-store actions", () => {
});
store.focusPane(workspaceKey, "main");
let layout = useWorkspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
let layout = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
expect(layout.focusedPaneId).toBe("main");
store.focusPane(workspaceKey, splitPaneId!);
layout = useWorkspaceLayoutStore.getState().layoutByWorkspace[workspaceKey]!;
layout = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey]!;
expect(splitPaneId).toBe("pane_56565656-5656-5656-5656-565656565656");
expect(layout.focusedPaneId).toBe(splitPaneId);
});
it("closeTab collapses an emptied pane and keeps the nearest sibling focused", () => {
vi.spyOn(globalThis.crypto, "randomUUID").mockReturnValue(
"cccccccc-cccc-cccc-cccc-cccccccccccc",
);
useWorkspaceLayoutIds("cccccccc-cccc-cccc-cccc-cccccccccccc");
const workspaceKey = createWorkspaceKey();
const store = useWorkspaceLayoutStore.getState();
const store = workspaceLayoutStore.getState();
store.openTabFocused(workspaceKey, { kind: "file", path: "/repo/worktree/a.ts" });
const secondTabId = store.openTabFocused(workspaceKey, {
@@ -783,7 +800,7 @@ describe("workspace-layout-store actions", () => {
});
store.closeTab(workspaceKey, secondTabId!);
const layout = useWorkspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
const layout = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
expect(splitPaneId).toBe("pane_cccccccc-cccc-cccc-cccc-cccccccccccc");
expect(layout.focusedPaneId).toBe("main");
@@ -791,18 +808,19 @@ describe("workspace-layout-store actions", () => {
});
it("splitPane enforces the maximum depth of four", () => {
vi.spyOn(globalThis.crypto, "randomUUID")
.mockReturnValueOnce("11111111-1111-1111-1111-111111111111")
.mockReturnValueOnce("22222222-2222-2222-2222-222222222222")
.mockReturnValueOnce("33333333-3333-3333-3333-333333333333")
.mockReturnValueOnce("44444444-4444-4444-4444-444444444444")
.mockReturnValueOnce("55555555-5555-5555-5555-555555555555")
.mockReturnValueOnce("66666666-6666-6666-6666-666666666666")
.mockReturnValueOnce("77777777-7777-7777-7777-777777777777")
.mockReturnValueOnce("88888888-8888-8888-8888-888888888888");
useWorkspaceLayoutIds(
"11111111-1111-1111-1111-111111111111",
"22222222-2222-2222-2222-222222222222",
"33333333-3333-3333-3333-333333333333",
"44444444-4444-4444-4444-444444444444",
"55555555-5555-5555-5555-555555555555",
"66666666-6666-6666-6666-666666666666",
"77777777-7777-7777-7777-777777777777",
"88888888-8888-8888-8888-888888888888",
);
const workspaceKey = createWorkspaceKey();
const store = useWorkspaceLayoutStore.getState();
const store = workspaceLayoutStore.getState();
const a = store.openTabFocused(workspaceKey, { kind: "file", path: "/repo/worktree/a.ts" });
const b = store.openTabFocused(workspaceKey, { kind: "file", path: "/repo/worktree/b.ts" });
const c = store.openTabFocused(workspaceKey, { kind: "file", path: "/repo/worktree/c.ts" });
@@ -831,7 +849,7 @@ describe("workspace-layout-store actions", () => {
position: "bottom",
});
const layout = useWorkspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
const layout = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
expect(pane1).toBe("pane_11111111-1111-1111-1111-111111111111");
expect(pane2).toBe("pane_33333333-3333-3333-3333-333333333333");
expect(pane3).toBe("pane_55555555-5555-5555-5555-555555555555");
@@ -840,11 +858,9 @@ describe("workspace-layout-store actions", () => {
});
it("moveTabToPane collapses the source pane when its last tab moves out", () => {
vi.spyOn(globalThis.crypto, "randomUUID").mockReturnValue(
"dddddddd-dddd-dddd-dddd-dddddddddddd",
);
useWorkspaceLayoutIds("dddddddd-dddd-dddd-dddd-dddddddddddd");
const workspaceKey = createWorkspaceKey();
const store = useWorkspaceLayoutStore.getState();
const store = workspaceLayoutStore.getState();
const leftTabId = store.openTabFocused(workspaceKey, {
kind: "file",
@@ -861,7 +877,7 @@ describe("workspace-layout-store actions", () => {
});
store.moveTabToPane(workspaceKey, leftTabId!, splitPaneId!);
const layout = useWorkspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
const layout = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
expect(layout.focusedPaneId).toBe(splitPaneId);
expect(collectAllPanes(layout.root).map((pane) => pane.id)).toEqual([splitPaneId!]);
@@ -872,13 +888,14 @@ describe("workspace-layout-store actions", () => {
});
it("closeTab cascades group unwrapping when an inner split collapses to a single pane", () => {
vi.spyOn(globalThis.crypto, "randomUUID")
.mockReturnValueOnce("78787878-7878-7878-7878-787878787878")
.mockReturnValueOnce("89898989-8989-8989-8989-898989898989")
.mockReturnValueOnce("9a9a9a9a-9a9a-9a9a-9a9a-9a9a9a9a9a9a");
useWorkspaceLayoutIds(
"78787878-7878-7878-7878-787878787878",
"89898989-8989-8989-8989-898989898989",
"9a9a9a9a-9a9a-9a9a-9a9a-9a9a9a9a9a9a",
);
const workspaceKey = createWorkspaceKey();
const store = useWorkspaceLayoutStore.getState();
const store = workspaceLayoutStore.getState();
store.openTabFocused(workspaceKey, { kind: "file", path: "/repo/worktree/a.ts" });
const secondTabId = store.openTabFocused(workspaceKey, {
@@ -901,7 +918,7 @@ describe("workspace-layout-store actions", () => {
});
store.closeTab(workspaceKey, secondTabId!);
const layout = useWorkspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
const layout = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
const rootGroup = expectGroup(layout.root);
expect(paneBId).toBe("pane_78787878-7878-7878-7878-787878787878");
@@ -937,11 +954,9 @@ describe("workspace-layout-store actions", () => {
});
it("openTab focuses the existing tab instead of creating a duplicate entry", () => {
vi.spyOn(globalThis.crypto, "randomUUID").mockReturnValue(
"abababab-abab-abab-abab-abababababab",
);
useWorkspaceLayoutIds("abababab-abab-abab-abab-abababababab");
const workspaceKey = createWorkspaceKey();
const store = useWorkspaceLayoutStore.getState();
const store = workspaceLayoutStore.getState();
store.openTabFocused(workspaceKey, { kind: "file", path: "/repo/worktree/a.ts" });
const secondTabId = store.openTabFocused(workspaceKey, {
@@ -959,7 +974,7 @@ describe("workspace-layout-store actions", () => {
kind: "file",
path: "/repo/worktree/b.ts",
});
const layout = useWorkspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
const layout = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
expect(splitPaneId).toBe("pane_abababab-abab-abab-abab-abababababab");
expect(duplicateTabId).toBe(secondTabId);
@@ -971,13 +986,14 @@ describe("workspace-layout-store actions", () => {
});
it("resizeSplit keeps sizes normalized while enforcing the minimum proportion", () => {
vi.spyOn(globalThis.crypto, "randomUUID")
.mockReturnValueOnce("eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee")
.mockReturnValueOnce("ffffffff-ffff-ffff-ffff-ffffffffffff")
.mockReturnValueOnce("11111111-1111-1111-1111-111111111111");
useWorkspaceLayoutIds(
"eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee",
"ffffffff-ffff-ffff-ffff-ffffffffffff",
"11111111-1111-1111-1111-111111111111",
);
const workspaceKey = createWorkspaceKey();
const store = useWorkspaceLayoutStore.getState();
const store = workspaceLayoutStore.getState();
const a = store.openTabFocused(workspaceKey, { kind: "file", path: "/repo/worktree/a.ts" });
const b = store.openTabFocused(workspaceKey, { kind: "file", path: "/repo/worktree/b.ts" });
@@ -995,12 +1011,12 @@ describe("workspace-layout-store actions", () => {
position: "right",
});
const splitRoot = useWorkspaceLayoutStore.getState().layoutByWorkspace[workspaceKey].root;
const splitRoot = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey].root;
const splitGroup = expectGroup(splitRoot);
const nestedGroup = expectGroup(splitGroup.group.children[1]);
store.resizeSplit(workspaceKey, nestedGroup.group.id, [0.01, 0.99]);
const resizedRoot = useWorkspaceLayoutStore.getState().layoutByWorkspace[workspaceKey].root;
const resizedRoot = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey].root;
const resizedGroup = expectGroup(resizedRoot);
const resizedNestedGroup = expectGroup(resizedGroup.group.children[1]);
const total = resizedNestedGroup.group.sizes.reduce((sum, size) => sum + size, 0);
@@ -1014,11 +1030,11 @@ describe("workspace-layout-store actions", () => {
it("closing the last tab keeps a single empty pane in the layout", () => {
const workspaceKey = createWorkspaceKey();
const store = useWorkspaceLayoutStore.getState();
const store = workspaceLayoutStore.getState();
const tabId = store.openTabFocused(workspaceKey, { kind: "draft", draftId: "draft-1" });
store.closeTab(workspaceKey, tabId!);
const layout = useWorkspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
const layout = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
expect(layout).toEqual(createDefaultLayout());
});
@@ -1032,12 +1048,12 @@ describe("workspace-layout-store actions", () => {
expect(otherWorkspaceKey).toBeTruthy();
const store = useWorkspaceLayoutStore.getState();
const store = workspaceLayoutStore.getState();
store.pinAgent(workspaceKey, "agent-1");
store.pinAgent(workspaceKey, "agent-1");
store.pinAgent(otherWorkspaceKey as string, "agent-2");
let state = useWorkspaceLayoutStore.getState();
let state = workspaceLayoutStore.getState();
expect(Array.from(state.pinnedAgentIdsByWorkspace[workspaceKey] ?? [])).toEqual(["agent-1"]);
expect(Array.from(state.pinnedAgentIdsByWorkspace[otherWorkspaceKey as string] ?? [])).toEqual([
"agent-2",
@@ -1045,13 +1061,13 @@ describe("workspace-layout-store actions", () => {
store.unpinAgent(workspaceKey, "agent-1");
state = useWorkspaceLayoutStore.getState();
state = workspaceLayoutStore.getState();
expect(state.pinnedAgentIdsByWorkspace[workspaceKey]).toBeUndefined();
expect(Array.from(state.pinnedAgentIdsByWorkspace[otherWorkspaceKey as string] ?? [])).toEqual([
"agent-2",
]);
const partialize = useWorkspaceLayoutStore.persist.getOptions().partialize;
const partialize = workspaceLayoutStore.persist.getOptions().partialize;
expect(partialize).toBeTypeOf("function");
expect(partialize?.(state)).toEqual({
layoutByWorkspace: {},
@@ -1068,12 +1084,12 @@ describe("workspace-layout-store actions", () => {
expect(otherWorkspaceKey).toBeTruthy();
const store = useWorkspaceLayoutStore.getState();
const store = workspaceLayoutStore.getState();
store.hideAgent(workspaceKey, "agent-1");
store.hideAgent(workspaceKey, "agent-1");
store.hideAgent(otherWorkspaceKey as string, "agent-2");
let state = useWorkspaceLayoutStore.getState();
let state = workspaceLayoutStore.getState();
expect(Array.from(state.hiddenAgentIdsByWorkspace[workspaceKey] ?? [])).toEqual(["agent-1"]);
expect(Array.from(state.hiddenAgentIdsByWorkspace[otherWorkspaceKey as string] ?? [])).toEqual([
"agent-2",
@@ -1081,13 +1097,13 @@ describe("workspace-layout-store actions", () => {
store.unhideAgent(workspaceKey, "agent-1");
state = useWorkspaceLayoutStore.getState();
state = workspaceLayoutStore.getState();
expect(state.hiddenAgentIdsByWorkspace[workspaceKey]).toBeUndefined();
expect(Array.from(state.hiddenAgentIdsByWorkspace[otherWorkspaceKey as string] ?? [])).toEqual([
"agent-2",
]);
const partialize = useWorkspaceLayoutStore.persist.getOptions().partialize;
const partialize = workspaceLayoutStore.persist.getOptions().partialize;
expect(partialize).toBeTypeOf("function");
expect(partialize?.(state)).toEqual({
layoutByWorkspace: {},
@@ -1096,11 +1112,9 @@ describe("workspace-layout-store actions", () => {
});
it("convertDraftToAgent removes the draft and focuses the existing canonical agent tab", () => {
vi.spyOn(globalThis.crypto, "randomUUID").mockReturnValue(
"67676767-6767-6767-6767-676767676767",
);
useWorkspaceLayoutIds("67676767-6767-6767-6767-676767676767");
const workspaceKey = createWorkspaceKey();
const store = useWorkspaceLayoutStore.getState();
const store = workspaceLayoutStore.getState();
const draftTabId = store.openTabFocused(workspaceKey, {
kind: "draft",
@@ -1114,7 +1128,7 @@ describe("workspace-layout-store actions", () => {
});
const nextTabId = store.convertDraftToAgent(workspaceKey, draftTabId!, "agent-1");
const layout = useWorkspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
const layout = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
expect(splitPaneId).toBe("pane_67676767-6767-6767-6767-676767676767");
expect(nextTabId).toBe("agent_agent-1");
@@ -1126,7 +1140,7 @@ describe("workspace-layout-store actions", () => {
it("reconcileTabs canonicalizes duplicates and prunes stale entity tabs from hydrated snapshots", () => {
const workspaceKey = createWorkspaceKey();
useWorkspaceLayoutStore.setState((state) => ({
workspaceLayoutStore.setState((state) => ({
...state,
layoutByWorkspace: {
...state.layoutByWorkspace,
@@ -1169,7 +1183,7 @@ describe("workspace-layout-store actions", () => {
},
}));
useWorkspaceLayoutStore.getState().reconcileTabs(workspaceKey, {
workspaceLayoutStore.getState().reconcileTabs(workspaceKey, {
agentsHydrated: true,
terminalsHydrated: true,
activeAgentIds: ["agent-1"],
@@ -1179,7 +1193,7 @@ describe("workspace-layout-store actions", () => {
hasActivePendingDraftCreate: false,
});
const layout = useWorkspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
const layout = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
const tabs = collectAllTabs(layout.root);
expect(tabs.map((tab) => tab.tabId)).toEqual([
@@ -1200,14 +1214,14 @@ describe("workspace-layout-store actions", () => {
it("reconcileTabs does not re-add locally hidden agent tabs", () => {
const workspaceKey = createWorkspaceKey();
useWorkspaceLayoutStore.setState((state) => ({
workspaceLayoutStore.setState((state) => ({
...state,
hiddenAgentIdsByWorkspace: {
[workspaceKey]: new Set<string>(["agent-1"]),
},
}));
useWorkspaceLayoutStore.getState().reconcileTabs(workspaceKey, {
workspaceLayoutStore.getState().reconcileTabs(workspaceKey, {
agentsHydrated: true,
terminalsHydrated: true,
activeAgentIds: ["agent-1"],
@@ -1217,13 +1231,13 @@ describe("workspace-layout-store actions", () => {
hasActivePendingDraftCreate: false,
});
expect(useWorkspaceLayoutStore.getState().getWorkspaceTabs(workspaceKey)).toEqual([]);
expect(workspaceLayoutStore.getState().getWorkspaceTabs(workspaceKey)).toEqual([]);
});
it("reconcileTabs does not auto-open subagents omitted from autoOpenAgentIds", () => {
const workspaceKey = createWorkspaceKey();
useWorkspaceLayoutStore.getState().reconcileTabs(workspaceKey, {
workspaceLayoutStore.getState().reconcileTabs(workspaceKey, {
agentsHydrated: true,
terminalsHydrated: true,
activeAgentIds: ["parent-agent", "child-agent"],
@@ -1234,7 +1248,7 @@ describe("workspace-layout-store actions", () => {
});
expect(
useWorkspaceLayoutStore
workspaceLayoutStore
.getState()
.getWorkspaceTabs(workspaceKey)
.map((tab) => tab.tabId),
@@ -1243,7 +1257,7 @@ describe("workspace-layout-store actions", () => {
it("reconcileTabs keeps manually opened subagent tabs that remain active", () => {
const workspaceKey = createWorkspaceKey();
const store = useWorkspaceLayoutStore.getState();
const store = workspaceLayoutStore.getState();
store.openTabFocused(workspaceKey, { kind: "agent", agentId: "child-agent" });
@@ -1258,7 +1272,7 @@ describe("workspace-layout-store actions", () => {
});
expect(
useWorkspaceLayoutStore
workspaceLayoutStore
.getState()
.getWorkspaceTabs(workspaceKey)
.map((tab) => tab.tabId),
@@ -1267,7 +1281,7 @@ describe("workspace-layout-store actions", () => {
it("reconcileTabs prunes archived subagent tabs that are no longer active", () => {
const workspaceKey = createWorkspaceKey();
const store = useWorkspaceLayoutStore.getState();
const store = workspaceLayoutStore.getState();
store.openTabFocused(workspaceKey, { kind: "agent", agentId: "child-agent" });
@@ -1282,7 +1296,7 @@ describe("workspace-layout-store actions", () => {
});
expect(
useWorkspaceLayoutStore
workspaceLayoutStore
.getState()
.getWorkspaceTabs(workspaceKey)
.map((tab) => tab.tabId),
@@ -1291,7 +1305,7 @@ describe("workspace-layout-store actions", () => {
it("openTabFocused reopens hidden subagent tabs and clears hidden intent", () => {
const workspaceKey = createWorkspaceKey();
const store = useWorkspaceLayoutStore.getState();
const store = workspaceLayoutStore.getState();
store.hideAgent(workspaceKey, "child-agent");
store.reconcileTabs(workspaceKey, {
@@ -1304,11 +1318,11 @@ describe("workspace-layout-store actions", () => {
hasActivePendingDraftCreate: false,
});
expect(useWorkspaceLayoutStore.getState().getWorkspaceTabs(workspaceKey)).toEqual([]);
expect(workspaceLayoutStore.getState().getWorkspaceTabs(workspaceKey)).toEqual([]);
store.openTabFocused(workspaceKey, { kind: "agent", agentId: "child-agent" });
const state = useWorkspaceLayoutStore.getState();
const state = workspaceLayoutStore.getState();
expect(state.hiddenAgentIdsByWorkspace[workspaceKey]).toBeUndefined();
expect(state.getWorkspaceTabs(workspaceKey).map((tab) => tab.tabId)).toEqual([
"agent_child-agent",
@@ -1317,7 +1331,7 @@ describe("workspace-layout-store actions", () => {
it("reconcileTabs auto-opens only standalone terminals while keeping explicitly opened live terminals", () => {
const workspaceKey = createWorkspaceKey();
const store = useWorkspaceLayoutStore.getState();
const store = workspaceLayoutStore.getState();
const scriptTabId = store.openTabFocused(workspaceKey, {
kind: "terminal",
@@ -1335,8 +1349,8 @@ describe("workspace-layout-store actions", () => {
hasActivePendingDraftCreate: false,
});
const tabs = useWorkspaceLayoutStore.getState().getWorkspaceTabs(workspaceKey);
const layout = useWorkspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
const tabs = workspaceLayoutStore.getState().getWorkspaceTabs(workspaceKey);
const layout = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
expect(tabs.map((tab) => tab.tabId)).toEqual(["terminal_term-script", "terminal_term-manual"]);
expect(findPaneById(layout.root, layout.focusedPaneId)?.focusedTabId).toBe(scriptTabId);
});
@@ -1344,7 +1358,7 @@ describe("workspace-layout-store actions", () => {
it("reconcileTabs does not auto-open live non-standalone terminals", () => {
const workspaceKey = createWorkspaceKey();
useWorkspaceLayoutStore.getState().reconcileTabs(workspaceKey, {
workspaceLayoutStore.getState().reconcileTabs(workspaceKey, {
agentsHydrated: true,
terminalsHydrated: true,
activeAgentIds: [],
@@ -1355,46 +1369,44 @@ describe("workspace-layout-store actions", () => {
hasActivePendingDraftCreate: false,
});
expect(useWorkspaceLayoutStore.getState().getWorkspaceTabs(workspaceKey)).toEqual([]);
expect(workspaceLayoutStore.getState().getWorkspaceTabs(workspaceKey)).toEqual([]);
});
it("explicitly opening an agent tab clears hidden intent", () => {
const workspaceKey = createWorkspaceKey();
const store = useWorkspaceLayoutStore.getState();
const store = workspaceLayoutStore.getState();
store.hideAgent(workspaceKey, "agent-1");
store.openTabFocused(workspaceKey, { kind: "agent", agentId: "agent-1" });
const state = useWorkspaceLayoutStore.getState();
const state = workspaceLayoutStore.getState();
expect(state.hiddenAgentIdsByWorkspace[workspaceKey]).toBeUndefined();
expect(state.getWorkspaceTabs(workspaceKey).map((tab) => tab.tabId)).toEqual(["agent_agent-1"]);
});
it("pinning an agent clears hidden intent", () => {
const workspaceKey = createWorkspaceKey();
const store = useWorkspaceLayoutStore.getState();
const store = workspaceLayoutStore.getState();
store.hideAgent(workspaceKey, "agent-1");
expect(
useWorkspaceLayoutStore.getState().hiddenAgentIdsByWorkspace[workspaceKey],
).toBeDefined();
expect(workspaceLayoutStore.getState().hiddenAgentIdsByWorkspace[workspaceKey]).toBeDefined();
store.pinAgent(workspaceKey, "agent-1");
const state = useWorkspaceLayoutStore.getState();
const state = workspaceLayoutStore.getState();
expect(state.hiddenAgentIdsByWorkspace[workspaceKey]).toBeUndefined();
expect(Array.from(state.pinnedAgentIdsByWorkspace[workspaceKey] ?? [])).toEqual(["agent-1"]);
});
it("retargeting a tab to an agent clears hidden intent", () => {
const workspaceKey = createWorkspaceKey();
const store = useWorkspaceLayoutStore.getState();
const store = workspaceLayoutStore.getState();
store.hideAgent(workspaceKey, "agent-1");
const tabId = store.openTabFocused(workspaceKey, { kind: "file", path: "/repo/worktree/a.ts" });
store.retargetTab(workspaceKey, tabId!, { kind: "agent", agentId: "agent-1" });
const state = useWorkspaceLayoutStore.getState();
const state = workspaceLayoutStore.getState();
expect(state.hiddenAgentIdsByWorkspace[workspaceKey]).toBeUndefined();
});
});
File diff suppressed because it is too large Load Diff