refactor(app): remove launcher tabs from workspace flow
This commit is contained in:
@@ -2,7 +2,6 @@ import invariant from "tiny-invariant";
|
||||
import type { WorkspaceTab, WorkspaceTabTarget } from "@/stores/workspace-tabs-store";
|
||||
import {
|
||||
buildDeterministicWorkspaceTabId,
|
||||
createLauncherId,
|
||||
normalizeWorkspaceTabTarget,
|
||||
workspaceTabTargetsEqual,
|
||||
} from "@/utils/workspace-tab-identity";
|
||||
@@ -111,10 +110,6 @@ interface OpenTabInLayoutResult {
|
||||
tabId: string;
|
||||
}
|
||||
|
||||
interface OpenLauncherTabInLayoutInput {
|
||||
layout: WorkspaceLayout;
|
||||
now: number;
|
||||
}
|
||||
|
||||
interface RetargetTabInLayoutInput {
|
||||
layout: WorkspaceLayout;
|
||||
@@ -1070,19 +1065,6 @@ export function openTabInLayout(input: OpenTabInLayoutInput): OpenTabInLayoutRes
|
||||
return insertNewTabIntoFocusedPane(input);
|
||||
}
|
||||
|
||||
export function openLauncherTabInLayout(
|
||||
input: OpenLauncherTabInLayoutInput,
|
||||
): OpenTabInLayoutResult {
|
||||
return insertNewTabIntoFocusedPane({
|
||||
layout: input.layout,
|
||||
target: {
|
||||
kind: "launcher",
|
||||
launcherId: createLauncherId(),
|
||||
},
|
||||
now: input.now,
|
||||
});
|
||||
}
|
||||
|
||||
export function closeTabInLayout(input: CloseTabInLayoutInput): WorkspaceLayout | null {
|
||||
const internalLayout = asInternalLayout(input.layout);
|
||||
const pane = findPaneContainingTab(internalLayout.root, input.tabId);
|
||||
@@ -1159,7 +1141,7 @@ export function retargetTabInLayout(
|
||||
}
|
||||
|
||||
return {
|
||||
// Preserve the existing tab id so launcher->entity transitions keep the same
|
||||
// Preserve the existing tab id so draft->entity transitions keep the same
|
||||
// React key during the first render. Reconciliation can canonicalize later.
|
||||
tabId: input.tabId,
|
||||
layout: {
|
||||
|
||||
@@ -262,36 +262,33 @@ describe("workspace-layout-store actions", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("openLauncherTab creates duplicate launcher tabs for repeated Cmd+T/new-tab opens", () => {
|
||||
vi.spyOn(globalThis.crypto, "randomUUID")
|
||||
.mockReturnValueOnce("11111111-1111-1111-1111-111111111111")
|
||||
.mockReturnValueOnce("22222222-2222-2222-2222-222222222222");
|
||||
it("openTab creates distinct draft tabs for repeated Cmd+T/new-tab opens", () => {
|
||||
const workspaceKey = createWorkspaceKey();
|
||||
const store = useWorkspaceLayoutStore.getState();
|
||||
|
||||
const firstTabId = store.openLauncherTab(workspaceKey);
|
||||
const secondTabId = store.openLauncherTab(workspaceKey);
|
||||
const firstTabId = store.openTab(workspaceKey, { kind: "draft", draftId: "draft-1" });
|
||||
const secondTabId = store.openTab(workspaceKey, { kind: "draft", draftId: "draft-2" });
|
||||
const layout = useWorkspaceLayoutStore.getState().layoutByWorkspace[workspaceKey]!;
|
||||
|
||||
expect(firstTabId).toBe("launcher_11111111-1111-1111-1111-111111111111");
|
||||
expect(secondTabId).toBe("launcher_22222222-2222-2222-2222-222222222222");
|
||||
expect(firstTabId).toBe("draft-1");
|
||||
expect(secondTabId).toBe("draft-2");
|
||||
expect(firstTabId).not.toBe(secondTabId);
|
||||
expect(findPaneById(layout.root, "main")?.tabIds).toEqual([firstTabId, secondTabId]);
|
||||
expect(collectAllTabs(layout.root)).toEqual([
|
||||
{
|
||||
tabId: firstTabId,
|
||||
target: { kind: "launcher", launcherId: "11111111-1111-1111-1111-111111111111" },
|
||||
target: { kind: "draft", draftId: "draft-1" },
|
||||
createdAt: expect.any(Number),
|
||||
},
|
||||
{
|
||||
tabId: secondTabId,
|
||||
target: { kind: "launcher", launcherId: "22222222-2222-2222-2222-222222222222" },
|
||||
target: { kind: "draft", draftId: "draft-2" },
|
||||
createdAt: expect.any(Number),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("splitPaneEmpty plus openLauncherTab opens a launcher tab in the new pane", () => {
|
||||
it("splitPaneEmpty plus openTab opens a draft tab in the new pane", () => {
|
||||
vi.spyOn(globalThis.crypto, "randomUUID").mockReturnValueOnce(
|
||||
"77777777-7777-7777-7777-777777777777",
|
||||
);
|
||||
@@ -303,15 +300,15 @@ describe("workspace-layout-store actions", () => {
|
||||
targetPaneId: "main",
|
||||
position: "right",
|
||||
});
|
||||
const launcherTabId = store.openLauncherTab(workspaceKey);
|
||||
const draftTabId = store.openTab(workspaceKey, { kind: "draft", draftId: "draft-split" });
|
||||
const layout = useWorkspaceLayoutStore.getState().layoutByWorkspace[workspaceKey]!;
|
||||
|
||||
expect(newPaneId).toBe("pane_77777777-7777-7777-7777-777777777777");
|
||||
expect(launcherTabId).toMatch(/^launcher_/);
|
||||
expect(draftTabId).toBe("draft-split");
|
||||
expect(layout.focusedPaneId).toBe(newPaneId);
|
||||
expect(findPaneById(layout.root, "main")?.tabIds).toEqual(["file_/repo/worktree/a.ts"]);
|
||||
expect(findPaneById(layout.root, newPaneId!)?.tabIds).toEqual([launcherTabId!]);
|
||||
expect(findPaneById(layout.root, newPaneId!)?.focusedTabId).toBe(launcherTabId);
|
||||
expect(findPaneById(layout.root, newPaneId!)?.tabIds).toEqual([draftTabId!]);
|
||||
expect(findPaneById(layout.root, newPaneId!)?.focusedTabId).toBe(draftTabId);
|
||||
});
|
||||
|
||||
it("focusTab moves workspace focus to the pane containing the tab", () => {
|
||||
@@ -371,37 +368,32 @@ describe("workspace-layout-store actions", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("retargetTab keeps a launcher tab in place while updating its target", () => {
|
||||
vi.spyOn(globalThis.crypto, "randomUUID").mockReturnValue(
|
||||
"33333333-3333-3333-3333-333333333333",
|
||||
);
|
||||
it("retargetTab keeps a draft tab in place while updating its target", () => {
|
||||
const workspaceKey = createWorkspaceKey();
|
||||
const store = useWorkspaceLayoutStore.getState();
|
||||
|
||||
const launcherTabId = store.openLauncherTab(workspaceKey);
|
||||
const nextTabId = store.retargetTab(workspaceKey, launcherTabId!, {
|
||||
const draftTabId = store.openTab(workspaceKey, { kind: "draft", draftId: "draft-retarget" });
|
||||
const nextTabId = store.retargetTab(workspaceKey, draftTabId!, {
|
||||
kind: "file",
|
||||
path: "/repo/worktree/launcher.ts",
|
||||
path: "/repo/worktree/retargeted.ts",
|
||||
});
|
||||
const layout = useWorkspaceLayoutStore.getState().layoutByWorkspace[workspaceKey]!;
|
||||
|
||||
expect(launcherTabId).toBe("launcher_33333333-3333-3333-3333-333333333333");
|
||||
expect(nextTabId).toBe(launcherTabId);
|
||||
expect(findPaneById(layout.root, "main")?.tabIds).toEqual([launcherTabId!]);
|
||||
expect(draftTabId).toBe("draft-retarget");
|
||||
expect(nextTabId).toBe(draftTabId);
|
||||
expect(findPaneById(layout.root, "main")?.tabIds).toEqual([draftTabId!]);
|
||||
expect(collectAllTabs(layout.root)).toEqual([
|
||||
{
|
||||
tabId: launcherTabId!,
|
||||
target: { kind: "file", path: "/repo/worktree/launcher.ts" },
|
||||
tabId: draftTabId!,
|
||||
target: { kind: "file", path: "/repo/worktree/retargeted.ts" },
|
||||
createdAt: expect.any(Number),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("retargetTab closes a launcher tab and focuses the existing canonical target tab", () => {
|
||||
it("retargetTab closes a draft tab and focuses the existing canonical target tab", () => {
|
||||
vi.spyOn(globalThis.crypto, "randomUUID")
|
||||
.mockReturnValueOnce("44444444-4444-4444-4444-444444444444")
|
||||
.mockReturnValueOnce("55555555-5555-5555-5555-555555555555")
|
||||
.mockReturnValueOnce("66666666-6666-6666-6666-666666666666");
|
||||
.mockReturnValueOnce("55555555-5555-5555-5555-555555555555");
|
||||
const workspaceKey = createWorkspaceKey();
|
||||
const store = useWorkspaceLayoutStore.getState();
|
||||
|
||||
@@ -409,64 +401,59 @@ describe("workspace-layout-store actions", () => {
|
||||
kind: "file",
|
||||
path: "/repo/worktree/existing.ts",
|
||||
});
|
||||
const launcherTabId = store.openLauncherTab(workspaceKey);
|
||||
const draftTabId = store.openTab(workspaceKey, { kind: "draft", draftId: "draft-dup" });
|
||||
const splitPaneId = store.splitPane(workspaceKey, {
|
||||
tabId: launcherTabId!,
|
||||
tabId: draftTabId!,
|
||||
targetPaneId: "main",
|
||||
position: "right",
|
||||
});
|
||||
const secondLauncherTabId = store.openLauncherTab(workspaceKey);
|
||||
const secondDraftTabId = store.openTab(workspaceKey, { kind: "draft", draftId: "draft-dup-2" });
|
||||
|
||||
const nextTabId = store.retargetTab(workspaceKey, secondLauncherTabId!, {
|
||||
const nextTabId = store.retargetTab(workspaceKey, secondDraftTabId!, {
|
||||
kind: "file",
|
||||
path: "/repo/worktree/existing.ts",
|
||||
});
|
||||
const layout = useWorkspaceLayoutStore.getState().layoutByWorkspace[workspaceKey]!;
|
||||
|
||||
expect(existingFileTabId).toBe("file_/repo/worktree/existing.ts");
|
||||
expect(launcherTabId).toBe("launcher_44444444-4444-4444-4444-444444444444");
|
||||
expect(draftTabId).toBe("draft-dup");
|
||||
expect(splitPaneId).toBe("pane_55555555-5555-5555-5555-555555555555");
|
||||
expect(secondLauncherTabId).toMatch(/^launcher_/);
|
||||
expect(secondLauncherTabId).not.toBe(launcherTabId);
|
||||
expect(nextTabId).toBe(existingFileTabId);
|
||||
expect(collectAllTabs(layout.root).map((tab) => tab.tabId)).toEqual([
|
||||
existingFileTabId!,
|
||||
launcherTabId!,
|
||||
draftTabId!,
|
||||
]);
|
||||
expect(layout.focusedPaneId).toBe("main");
|
||||
expect(findPaneById(layout.root, "main")?.focusedTabId).toBe(existingFileTabId);
|
||||
});
|
||||
|
||||
it("retargetTab closes a launcher tab and focuses an existing matching target tab", () => {
|
||||
vi.spyOn(globalThis.crypto, "randomUUID")
|
||||
.mockReturnValueOnce("77777777-7777-7777-7777-777777777777")
|
||||
.mockReturnValueOnce("88888888-8888-8888-8888-888888888888");
|
||||
it("retargetTab closes a draft tab and focuses an existing matching target tab", () => {
|
||||
const workspaceKey = createWorkspaceKey();
|
||||
const store = useWorkspaceLayoutStore.getState();
|
||||
|
||||
const firstLauncherTabId = store.openLauncherTab(workspaceKey);
|
||||
const firstAgentTabId = store.retargetTab(workspaceKey, firstLauncherTabId!, {
|
||||
const firstDraftTabId = store.openTab(workspaceKey, { kind: "draft", draftId: "draft-agent-1" });
|
||||
const firstAgentTabId = store.retargetTab(workspaceKey, firstDraftTabId!, {
|
||||
kind: "agent",
|
||||
agentId: "agent-1",
|
||||
});
|
||||
const secondLauncherTabId = store.openLauncherTab(workspaceKey);
|
||||
const secondDraftTabId = store.openTab(workspaceKey, { kind: "draft", draftId: "draft-agent-2" });
|
||||
|
||||
const nextTabId = store.retargetTab(workspaceKey, secondLauncherTabId!, {
|
||||
const nextTabId = store.retargetTab(workspaceKey, secondDraftTabId!, {
|
||||
kind: "agent",
|
||||
agentId: "agent-1",
|
||||
});
|
||||
const layout = useWorkspaceLayoutStore.getState().layoutByWorkspace[workspaceKey]!;
|
||||
|
||||
expect(firstAgentTabId).toBe(firstLauncherTabId);
|
||||
expect(nextTabId).toBe(firstLauncherTabId);
|
||||
expect(firstAgentTabId).toBe(firstDraftTabId);
|
||||
expect(nextTabId).toBe(firstDraftTabId);
|
||||
expect(collectAllTabs(layout.root)).toEqual([
|
||||
{
|
||||
tabId: firstLauncherTabId!,
|
||||
tabId: firstDraftTabId!,
|
||||
target: { kind: "agent", agentId: "agent-1" },
|
||||
createdAt: expect.any(Number),
|
||||
},
|
||||
]);
|
||||
expect(findPaneById(layout.root, "main")?.focusedTabId).toBe(firstLauncherTabId);
|
||||
expect(findPaneById(layout.root, "main")?.focusedTabId).toBe(firstDraftTabId);
|
||||
});
|
||||
|
||||
it("reorderTabs reorders tabs within the focused pane", () => {
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
insertSplit,
|
||||
moveTabToPaneInLayout,
|
||||
normalizeLayout,
|
||||
openLauncherTabInLayout,
|
||||
openTabInLayout,
|
||||
reconcileWorkspaceTabs,
|
||||
removePaneFromTree,
|
||||
@@ -67,7 +66,6 @@ interface WorkspaceLayoutStore {
|
||||
splitSizesByWorkspace: Record<string, Record<string, number[]>>;
|
||||
pinnedAgentIdsByWorkspace: Record<string, Set<string>>;
|
||||
openTab: (workspaceKey: string, target: WorkspaceTabTarget) => string | null;
|
||||
openLauncherTab: (workspaceKey: string) => string | null;
|
||||
closeTab: (workspaceKey: string, tabId: string) => void;
|
||||
focusTab: (workspaceKey: string, tabId: string) => void;
|
||||
retargetTab: (workspaceKey: string, tabId: string, target: WorkspaceTabTarget) => string | null;
|
||||
@@ -143,26 +141,6 @@ export const useWorkspaceLayoutStore = create<WorkspaceLayoutStore>()(
|
||||
|
||||
return result.tabId;
|
||||
},
|
||||
openLauncherTab: (workspaceKey) => {
|
||||
const normalizedWorkspaceKey = trimNonEmpty(workspaceKey);
|
||||
if (!normalizedWorkspaceKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = openLauncherTabInLayout({
|
||||
layout: getWorkspaceLayout(get().layoutByWorkspace, normalizedWorkspaceKey),
|
||||
now: Date.now(),
|
||||
});
|
||||
|
||||
set((state) => ({
|
||||
layoutByWorkspace: {
|
||||
...state.layoutByWorkspace,
|
||||
[normalizedWorkspaceKey]: result.layout,
|
||||
},
|
||||
}));
|
||||
|
||||
return result.tabId;
|
||||
},
|
||||
closeTab: (workspaceKey, tabId) => {
|
||||
const normalizedWorkspaceKey = trimNonEmpty(workspaceKey);
|
||||
const normalizedTabId = trimNonEmpty(tabId);
|
||||
|
||||
@@ -140,36 +140,35 @@ describe("workspace-tabs-store retargetTab", () => {
|
||||
expect(order).toEqual([draftTabId]);
|
||||
});
|
||||
|
||||
it("openLauncherTab creates distinct launcher tabs without deduplicating", () => {
|
||||
vi.spyOn(globalThis.crypto, "randomUUID")
|
||||
.mockReturnValueOnce("11111111-1111-1111-1111-111111111111")
|
||||
.mockReturnValueOnce("22222222-2222-2222-2222-222222222222");
|
||||
it("openDraftTab creates a draft tab and deduplicates by draftId", () => {
|
||||
const key = buildWorkspaceTabPersistenceKey({ serverId: SERVER_ID, workspaceId: WORKSPACE_ID });
|
||||
expect(key).toBeTruthy();
|
||||
const workspaceKey = key as string;
|
||||
|
||||
const firstTabId = useWorkspaceTabsStore.getState().openLauncherTab({
|
||||
const firstTabId = useWorkspaceTabsStore.getState().openDraftTab({
|
||||
serverId: SERVER_ID,
|
||||
workspaceId: WORKSPACE_ID,
|
||||
draftId: "draft-1",
|
||||
});
|
||||
const secondTabId = useWorkspaceTabsStore.getState().openLauncherTab({
|
||||
const secondTabId = useWorkspaceTabsStore.getState().openDraftTab({
|
||||
serverId: SERVER_ID,
|
||||
workspaceId: WORKSPACE_ID,
|
||||
draftId: "draft-2",
|
||||
});
|
||||
|
||||
const state = useWorkspaceTabsStore.getState();
|
||||
expect(firstTabId).toBe("launcher_11111111-1111-1111-1111-111111111111");
|
||||
expect(secondTabId).toBe("launcher_22222222-2222-2222-2222-222222222222");
|
||||
expect(firstTabId).toBe("draft-1");
|
||||
expect(secondTabId).toBe("draft-2");
|
||||
expect(state.tabOrderByWorkspace[workspaceKey]).toEqual([firstTabId, secondTabId]);
|
||||
expect(state.uiTabsByWorkspace[workspaceKey]).toEqual([
|
||||
{
|
||||
tabId: "launcher_11111111-1111-1111-1111-111111111111",
|
||||
target: { kind: "launcher", launcherId: "11111111-1111-1111-1111-111111111111" },
|
||||
tabId: "draft-1",
|
||||
target: { kind: "draft", draftId: "draft-1" },
|
||||
createdAt: expect.any(Number),
|
||||
},
|
||||
{
|
||||
tabId: "launcher_22222222-2222-2222-2222-222222222222",
|
||||
target: { kind: "launcher", launcherId: "22222222-2222-2222-2222-222222222222" },
|
||||
tabId: "draft-2",
|
||||
target: { kind: "draft", draftId: "draft-2" },
|
||||
createdAt: expect.any(Number),
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -3,7 +3,6 @@ import { create } from "zustand";
|
||||
import { createJSONStorage, persist } from "zustand/middleware";
|
||||
import {
|
||||
buildDeterministicWorkspaceTabId,
|
||||
createLauncherId,
|
||||
normalizeWorkspaceTabTarget,
|
||||
workspaceTabTargetsEqual,
|
||||
} from "@/utils/workspace-tab-identity";
|
||||
@@ -13,7 +12,6 @@ export type WorkspaceTabTarget =
|
||||
| { kind: "agent"; agentId: string }
|
||||
| { kind: "terminal"; terminalId: string }
|
||||
| { kind: "file"; path: string }
|
||||
| { kind: "launcher"; launcherId: string }
|
||||
| { kind: "setup"; workspaceId: string };
|
||||
|
||||
export type WorkspaceTab = {
|
||||
@@ -84,7 +82,6 @@ type WorkspaceTabsState = {
|
||||
workspaceId: string;
|
||||
target: WorkspaceTabTarget;
|
||||
}) => string | null;
|
||||
openLauncherTab: (input: { serverId: string; workspaceId: string }) => string | null;
|
||||
openOrFocusTab: (input: {
|
||||
serverId: string;
|
||||
workspaceId: string;
|
||||
@@ -171,13 +168,6 @@ export const useWorkspaceTabsStore = create<WorkspaceTabsState>()(
|
||||
|
||||
return resolvedTabId;
|
||||
},
|
||||
openLauncherTab: ({ serverId, workspaceId }) => {
|
||||
return get().openOrFocusTab({
|
||||
serverId,
|
||||
workspaceId,
|
||||
target: { kind: "launcher", launcherId: createLauncherId() },
|
||||
});
|
||||
},
|
||||
openOrFocusTab: ({ serverId, workspaceId, target }) => {
|
||||
const tabId = get().ensureTab({ serverId, workspaceId, target });
|
||||
if (!tabId) {
|
||||
|
||||
Reference in New Issue
Block a user