diff --git a/packages/app/src/contexts/session-context.tsx b/packages/app/src/contexts/session-context.tsx index 3b58c5d2f..675caa210 100644 --- a/packages/app/src/contexts/session-context.tsx +++ b/packages/app/src/contexts/session-context.tsx @@ -33,6 +33,7 @@ import { type Agent, type SessionState, type WorkspaceDescriptor, + mergeWorkspaceSnapshotWithExisting, normalizeWorkspaceDescriptor, } from "@/stores/session-store"; import { useDraftStore } from "@/stores/draft-store"; @@ -326,6 +327,7 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider } const workspaces = new Map(); + const existingWorkspaces = useSessionStore.getState().sessions[serverId]?.workspaces; let cursor: string | null = null; let includeSubscribe = options?.subscribe ?? false; @@ -341,7 +343,13 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider for (const entry of payload.entries) { const workspace = normalizeWorkspaceDescriptor(entry); - workspaces.set(workspace.id, workspace); + workspaces.set( + workspace.id, + mergeWorkspaceSnapshotWithExisting({ + incoming: workspace, + existing: existingWorkspaces?.get(workspace.id), + }), + ); } if (!payload.pageInfo.hasMore || !payload.pageInfo.nextCursor) { diff --git a/packages/app/src/hooks/use-sidebar-workspaces-list.ts b/packages/app/src/hooks/use-sidebar-workspaces-list.ts index 205deb6a4..947c83423 100644 --- a/packages/app/src/hooks/use-sidebar-workspaces-list.ts +++ b/packages/app/src/hooks/use-sidebar-workspaces-list.ts @@ -1,5 +1,9 @@ import { useCallback, useEffect, useMemo, useSyncExternalStore } from "react"; -import { normalizeWorkspaceDescriptor, useSessionStore } from "@/stores/session-store"; +import { + mergeWorkspaceSnapshotWithExisting, + normalizeWorkspaceDescriptor, + useSessionStore, +} from "@/stores/session-store"; import { getHostRuntimeStore } from "@/runtime/host-runtime"; import { useSidebarOrderStore } from "@/stores/sidebar-order-store"; import type { WorkspaceDescriptor } from "@/stores/session-store"; @@ -356,6 +360,7 @@ export function useSidebarWorkspacesList(options?: { } void (async () => { const next = new Map(); + const existingWorkspaces = useSessionStore.getState().sessions[serverId]?.workspaces; let cursor: string | null = null; try { while (true) { @@ -365,7 +370,13 @@ export function useSidebarWorkspacesList(options?: { }); for (const entry of payload.entries) { const workspace = toWorkspaceDescriptor(entry); - next.set(workspace.id, workspace); + next.set( + workspace.id, + mergeWorkspaceSnapshotWithExisting({ + incoming: workspace, + existing: existingWorkspaces?.get(workspace.id), + }), + ); } if (!payload.pageInfo.hasMore || !payload.pageInfo.nextCursor) { break; diff --git a/packages/app/src/stores/session-store.test.ts b/packages/app/src/stores/session-store.test.ts new file mode 100644 index 000000000..b265b7b70 --- /dev/null +++ b/packages/app/src/stores/session-store.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from "vitest"; +import { + mergeWorkspaceSnapshotWithExisting, + type WorkspaceDescriptor, +} from "./session-store"; + +function createWorkspace( + input: Partial & Pick, +): WorkspaceDescriptor { + return { + id: input.id, + projectId: input.projectId ?? "remote:github.com/getpaseo/paseo", + projectDisplayName: input.projectDisplayName ?? "getpaseo/paseo", + projectRootPath: input.projectRootPath ?? "/tmp/repo", + projectKind: input.projectKind ?? "git", + workspaceKind: input.workspaceKind ?? "local_checkout", + name: input.name ?? "main", + status: input.status ?? "done", + activityAt: input.activityAt ?? null, + diffStat: input.diffStat ?? null, + }; +} + +describe("mergeWorkspaceSnapshotWithExisting", () => { + it("preserves the last known diff stat when a snapshot only has baseline null data", () => { + const existing = createWorkspace({ + id: "/tmp/repo", + diffStat: { additions: 4, deletions: 2 }, + }); + const incoming = createWorkspace({ + id: "/tmp/repo", + diffStat: null, + }); + + expect(mergeWorkspaceSnapshotWithExisting({ incoming, existing })).toEqual({ + ...incoming, + diffStat: { additions: 4, deletions: 2 }, + }); + }); + + it("uses the incoming diff stat when the server provides a known value", () => { + const existing = createWorkspace({ + id: "/tmp/repo", + diffStat: { additions: 4, deletions: 2 }, + }); + const incoming = createWorkspace({ + id: "/tmp/repo", + diffStat: { additions: 0, deletions: 0 }, + }); + + expect(mergeWorkspaceSnapshotWithExisting({ incoming, existing })).toEqual(incoming); + }); +}); diff --git a/packages/app/src/stores/session-store.ts b/packages/app/src/stores/session-store.ts index 11071eb16..cc31a8a9f 100644 --- a/packages/app/src/stores/session-store.ts +++ b/packages/app/src/stores/session-store.ts @@ -142,6 +142,21 @@ export function normalizeWorkspaceDescriptor( }; } +export function mergeWorkspaceSnapshotWithExisting(input: { + incoming: WorkspaceDescriptor; + existing?: WorkspaceDescriptor | null; +}): WorkspaceDescriptor { + const { incoming, existing } = input; + if (!existing || existing.id !== incoming.id) { + return incoming; + } + + return { + ...incoming, + diffStat: incoming.diffStat ?? existing.diffStat, + }; +} + export type ExplorerEntryKind = "file" | "directory"; export type ExplorerFileKind = "text" | "image" | "binary"; export type ExplorerEncoding = "utf-8" | "base64" | "none";