Add workspace setup streaming and setup tab

This commit is contained in:
Mohamed Boudra
2026-03-31 09:09:38 +07:00
parent 266d042528
commit 1f6ffa9e0e
28 changed files with 1645 additions and 165 deletions
@@ -0,0 +1,72 @@
import type { SessionOutboundMessage } from "@server/shared/messages";
import { create } from "zustand";
import { buildWorkspaceTabPersistenceKey } from "@/stores/workspace-tabs-store";
export type WorkspaceSetupProgressPayload = Extract<
SessionOutboundMessage,
{ type: "workspace_setup_progress" }
>["payload"];
export interface WorkspaceSetupSnapshot extends WorkspaceSetupProgressPayload {
updatedAt: number;
}
interface WorkspaceSetupStoreState {
snapshots: Record<string, WorkspaceSetupSnapshot>;
upsertProgress: (input: { serverId: string; payload: WorkspaceSetupProgressPayload }) => void;
removeWorkspace: (input: { serverId: string; workspaceId: string }) => void;
clearServer: (serverId: string) => void;
}
function buildWorkspaceSetupKey(input: {
serverId: string;
workspaceId: string;
}): string | null {
return buildWorkspaceTabPersistenceKey(input);
}
export const useWorkspaceSetupStore = create<WorkspaceSetupStoreState>()((set) => ({
snapshots: {},
upsertProgress: ({ serverId, payload }) => {
const key = buildWorkspaceSetupKey({ serverId, workspaceId: payload.workspaceId });
if (!key) {
return;
}
set((state) => ({
snapshots: {
...state.snapshots,
[key]: {
...payload,
updatedAt: Date.now(),
},
},
}));
},
removeWorkspace: ({ serverId, workspaceId }) => {
const key = buildWorkspaceSetupKey({ serverId, workspaceId });
if (!key) {
return;
}
set((state) => {
if (!(key in state.snapshots)) {
return state;
}
const next = { ...state.snapshots };
delete next[key];
return { snapshots: next };
});
},
clearServer: (serverId) => {
set((state) => {
const nextEntries = Object.entries(state.snapshots).filter(
([key]) => !key.startsWith(`${serverId}:`),
);
if (nextEntries.length === Object.keys(state.snapshots).length) {
return state;
}
return { snapshots: Object.fromEntries(nextEntries) };
});
},
}));
@@ -200,4 +200,19 @@ describe("workspace-tabs-store retargetTab", () => {
expect(reopenedFileTabId).toBe(fileTabId);
expect(useWorkspaceTabsStore.getState().focusedTabIdByWorkspace[workspaceKey]).toBe(fileTabId);
});
it("builds a deterministic setup tab keyed by workspace id", () => {
const key = buildWorkspaceTabPersistenceKey({ serverId: SERVER_ID, workspaceId: WORKSPACE_ID });
expect(key).toBeTruthy();
const workspaceKey = key as string;
const tabId = useWorkspaceTabsStore.getState().openOrFocusTab({
serverId: SERVER_ID,
workspaceId: WORKSPACE_ID,
target: { kind: "setup", workspaceId: WORKSPACE_ID },
});
expect(tabId).toBe(`setup_${WORKSPACE_ID}`);
expect(useWorkspaceTabsStore.getState().focusedTabIdByWorkspace[workspaceKey]).toBe(tabId);
});
});
@@ -6,7 +6,8 @@ export type WorkspaceTabTarget =
| { kind: "draft"; draftId: string }
| { kind: "agent"; agentId: string }
| { kind: "terminal"; terminalId: string }
| { kind: "file"; path: string };
| { kind: "file"; path: string }
| { kind: "setup"; workspaceId: string };
export type WorkspaceTab = {
tabId: string;
@@ -60,6 +61,10 @@ function normalizeTabTarget(
const path = trimNonEmpty(value.path);
return path ? { kind: "file", path: path.replace(/\\/g, "/") } : null;
}
if (value.kind === "setup") {
const workspaceId = trimNonEmpty(value.workspaceId);
return workspaceId ? { kind: "setup", workspaceId: workspaceId.replace(/\\/g, "/") } : null;
}
return null;
}
@@ -79,6 +84,9 @@ function tabTargetsEqual(left: WorkspaceTabTarget, right: WorkspaceTabTarget): b
if (left.kind === "file" && right.kind === "file") {
return left.path === right.path;
}
if (left.kind === "setup" && right.kind === "setup") {
return left.workspaceId === right.workspaceId;
}
return false;
}
@@ -92,6 +100,9 @@ function buildDeterministicTabId(target: WorkspaceTabTarget): string {
if (target.kind === "terminal") {
return `terminal_${target.terminalId}`;
}
if (target.kind === "setup") {
return `setup_${target.workspaceId}`;
}
return `file_${target.path}`;
}