Preserve workspace diff stats across rehydration

This commit is contained in:
Mohamed Boudra
2026-04-07 16:48:51 +07:00
parent 21c7761403
commit 5ce4562eed
4 changed files with 90 additions and 3 deletions

View File

@@ -33,6 +33,7 @@ import {
type Agent, type Agent,
type SessionState, type SessionState,
type WorkspaceDescriptor, type WorkspaceDescriptor,
mergeWorkspaceSnapshotWithExisting,
normalizeWorkspaceDescriptor, normalizeWorkspaceDescriptor,
} from "@/stores/session-store"; } from "@/stores/session-store";
import { useDraftStore } from "@/stores/draft-store"; import { useDraftStore } from "@/stores/draft-store";
@@ -326,6 +327,7 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
} }
const workspaces = new Map<string, WorkspaceDescriptor>(); const workspaces = new Map<string, WorkspaceDescriptor>();
const existingWorkspaces = useSessionStore.getState().sessions[serverId]?.workspaces;
let cursor: string | null = null; let cursor: string | null = null;
let includeSubscribe = options?.subscribe ?? false; let includeSubscribe = options?.subscribe ?? false;
@@ -341,7 +343,13 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
for (const entry of payload.entries) { for (const entry of payload.entries) {
const workspace = normalizeWorkspaceDescriptor(entry); 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) { if (!payload.pageInfo.hasMore || !payload.pageInfo.nextCursor) {

View File

@@ -1,5 +1,9 @@
import { useCallback, useEffect, useMemo, useSyncExternalStore } from "react"; 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 { getHostRuntimeStore } from "@/runtime/host-runtime";
import { useSidebarOrderStore } from "@/stores/sidebar-order-store"; import { useSidebarOrderStore } from "@/stores/sidebar-order-store";
import type { WorkspaceDescriptor } from "@/stores/session-store"; import type { WorkspaceDescriptor } from "@/stores/session-store";
@@ -356,6 +360,7 @@ export function useSidebarWorkspacesList(options?: {
} }
void (async () => { void (async () => {
const next = new Map<string, WorkspaceDescriptor>(); const next = new Map<string, WorkspaceDescriptor>();
const existingWorkspaces = useSessionStore.getState().sessions[serverId]?.workspaces;
let cursor: string | null = null; let cursor: string | null = null;
try { try {
while (true) { while (true) {
@@ -365,7 +370,13 @@ export function useSidebarWorkspacesList(options?: {
}); });
for (const entry of payload.entries) { for (const entry of payload.entries) {
const workspace = toWorkspaceDescriptor(entry); 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) { if (!payload.pageInfo.hasMore || !payload.pageInfo.nextCursor) {
break; break;

View File

@@ -0,0 +1,53 @@
import { describe, expect, it } from "vitest";
import {
mergeWorkspaceSnapshotWithExisting,
type WorkspaceDescriptor,
} from "./session-store";
function createWorkspace(
input: Partial<WorkspaceDescriptor> & Pick<WorkspaceDescriptor, "id">,
): 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);
});
});

View File

@@ -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 ExplorerEntryKind = "file" | "directory";
export type ExplorerFileKind = "text" | "image" | "binary"; export type ExplorerFileKind = "text" | "image" | "binary";
export type ExplorerEncoding = "utf-8" | "base64" | "none"; export type ExplorerEncoding = "utf-8" | "base64" | "none";