Files
paseo/packages/app/src/stores/workspace-setup-store.ts
T
nikuscs 512c9b31a8 feat: open Changes as a workspace tab (#2298)
Open the complete working comparison in a desktop workspace tab while preserving inline sidebar diffs when the tab is closed.

Working and commit comparisons share one diff panel, and workspace layout is the sole owner of live tab persistence and identity.

Refs #1520
2026-07-22 20:57:18 +02:00

147 lines
4.5 KiB
TypeScript

import type { SessionOutboundMessage } from "@getpaseo/protocol/messages";
import { create } from "zustand";
import { buildWorkspaceTabPersistenceKey } from "@/workspace-tabs/model";
export type WorkspaceCreationMethod = "open_project" | "create_worktree";
export interface PendingWorkspaceSetup {
serverId: string;
sourceDirectory: string;
sourceWorkspaceId?: string;
displayName?: string;
creationMethod: WorkspaceCreationMethod;
}
export type WorkspaceSetupProgressPayload = Extract<
SessionOutboundMessage,
{ type: "workspace_setup_progress" }
>["payload"];
export type WorkspaceSetupStatusResult = Extract<
SessionOutboundMessage,
{ type: "workspace_setup_status_response" }
>["payload"];
export interface WorkspaceSetupStatusClient {
fetchWorkspaceSetupStatus: (workspaceId: string) => Promise<WorkspaceSetupStatusResult>;
}
export interface WorkspaceSetupSnapshot extends WorkspaceSetupProgressPayload {
updatedAt: number;
}
export function shouldShowWorkspaceSetup(snapshot: WorkspaceSetupSnapshot | null): boolean {
if (!snapshot) {
return false;
}
return snapshot.error !== null || snapshot.detail.commands.length > 0;
}
interface WorkspaceSetupStoreState {
pendingWorkspaceSetup: PendingWorkspaceSetup | null;
snapshots: Record<string, WorkspaceSetupSnapshot>;
requestedKeys: Set<string>;
beginWorkspaceSetup: (value: PendingWorkspaceSetup) => void;
clearWorkspaceSetup: () => void;
upsertProgress: (input: { serverId: string; payload: WorkspaceSetupProgressPayload }) => void;
ensureSetupStatus: (input: {
serverId: string;
workspaceId: string;
client: WorkspaceSetupStatusClient;
}) => 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, get) => ({
pendingWorkspaceSetup: null,
snapshots: {},
requestedKeys: new Set(),
beginWorkspaceSetup: (value) => {
set({ pendingWorkspaceSetup: value });
},
clearWorkspaceSetup: () => {
set({ pendingWorkspaceSetup: null });
},
upsertProgress: ({ serverId, payload }) => {
const key = buildWorkspaceSetupKey({ serverId, workspaceId: payload.workspaceId });
if (!key) {
return;
}
set((state) => ({
snapshots: {
...state.snapshots,
[key]: {
...payload,
updatedAt: Date.now(),
},
},
}));
},
ensureSetupStatus: async ({ serverId, workspaceId, client }) => {
const key = buildWorkspaceSetupKey({ serverId, workspaceId });
if (!key) {
return;
}
const state = get();
if (state.snapshots[key] || state.requestedKeys.has(key)) {
return;
}
// requestedKeys is a pure in-flight marker: it dedupes concurrent fetches and is
// released once the request settles. A settle that stored no snapshot (null snapshot,
// mismatched workspace, or error) leaves no marker, so a later call can retry; once a
// snapshot lands, the snapshots[key] guard above prevents redundant refetches.
set((current) => ({ requestedKeys: new Set(current.requestedKeys).add(key) }));
try {
const response = await client.fetchWorkspaceSetupStatus(workspaceId);
if (response.workspaceId === workspaceId && response.snapshot) {
get().upsertProgress({
serverId,
payload: { workspaceId: response.workspaceId, ...response.snapshot },
});
}
} catch {
// Swallowed: the finally clears the in-flight marker so a later call retries.
} finally {
set((current) => {
const next = new Set(current.requestedKeys);
next.delete(key);
return { requestedKeys: next };
});
}
},
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) };
});
},
}));