Refactor workspace layout id generation (#876)
This commit is contained in:
@@ -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
Reference in New Issue
Block a user