From b5161a4709c06ec17d97c45b76b79776ac9d8408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Mon, 15 Jun 2026 19:02:44 +0800 Subject: [PATCH] fix(desktop): refresh workspace diff previews (#724) Prefer the live git diff for currently changed workspace files so DIFF previews do not get stuck on stale transcript snapshots. Refresh an existing workspace preview tab when it is opened again instead of only reactivating cached content. Tested: bun test src/server/__tests__/workspace-service.test.ts Tested: cd desktop && bun run test src/stores/workspacePanelStore.test.ts --run Tested: bun run check:server Tested: bun run check:desktop Confidence: high Scope-risk: narrow --- .../src/stores/workspacePanelStore.test.ts | 32 +++++++- desktop/src/stores/workspacePanelStore.ts | 82 +++++++++++-------- .../__tests__/workspace-service.test.ts | 28 +++++++ src/server/services/workspaceService.ts | 61 ++++++++++---- 4 files changed, 153 insertions(+), 50 deletions(-) diff --git a/desktop/src/stores/workspacePanelStore.test.ts b/desktop/src/stores/workspacePanelStore.test.ts index 8f5fb994..1d59f406 100644 --- a/desktop/src/stores/workspacePanelStore.test.ts +++ b/desktop/src/stores/workspacePanelStore.test.ts @@ -342,7 +342,7 @@ describe('workspacePanelStore', () => { expect(useWorkspacePanelStore.getState().getMode('session-preview-mode')).toBe('workspace') }) - it('opens preview tabs, supports multiple kinds, and reuses duplicates without persistence', async () => { + it('opens preview tabs, supports multiple kinds, and refreshes duplicates without persistence', async () => { const storage = typeof globalThis.localStorage === 'undefined' ? null : globalThis.localStorage const setItemSpy = storage ? vi.spyOn(storage, 'setItem') : null @@ -364,7 +364,7 @@ describe('workspacePanelStore', () => { await useWorkspacePanelStore.getState().openPreview('session-preview', 'src/a.ts', 'diff') await useWorkspacePanelStore.getState().openPreview('session-preview', 'src/a.ts', 'file') - expect(mocks.getWorkspaceFileMock).toHaveBeenCalledTimes(1) + expect(mocks.getWorkspaceFileMock).toHaveBeenCalledTimes(2) expect(mocks.getWorkspaceDiffMock).toHaveBeenCalledTimes(1) const tabs = useWorkspacePanelStore.getState().previewTabsBySession['session-preview'] @@ -391,6 +391,34 @@ describe('workspacePanelStore', () => { } }) + it('refreshes an existing preview tab when the same path is opened again', async () => { + mocks.getWorkspaceDiffMock + .mockResolvedValueOnce({ + state: 'ok', + path: 'src/a.ts', + diff: '@@ -1 +1 @@\n-old\n+first', + }) + .mockResolvedValueOnce({ + state: 'ok', + path: 'src/a.ts', + diff: '@@ -1 +1 @@\n-old\n+latest', + }) + + await useWorkspacePanelStore.getState().openPreview('session-refresh', 'src/a.ts', 'diff') + await useWorkspacePanelStore.getState().openPreview('session-refresh', 'src/a.ts', 'diff') + + expect(mocks.getWorkspaceDiffMock).toHaveBeenCalledTimes(2) + expect(useWorkspacePanelStore.getState().previewTabsBySession['session-refresh']).toMatchObject([ + { + id: 'diff:src/a.ts', + kind: 'diff', + path: 'src/a.ts', + diff: '@@ -1 +1 @@\n-old\n+latest', + }, + ]) + expect(useWorkspacePanelStore.getState().activePreviewTabIdBySession['session-refresh']).toBe('diff:src/a.ts') + }) + it('closes exact tab id and preserves sibling preview for the same path', async () => { mocks.getWorkspaceFileMock.mockResolvedValue({ state: 'ok', diff --git a/desktop/src/stores/workspacePanelStore.ts b/desktop/src/stores/workspacePanelStore.ts index b602ea05..b57ac26d 100644 --- a/desktop/src/stores/workspacePanelStore.ts +++ b/desktop/src/stores/workspacePanelStore.ts @@ -457,50 +457,64 @@ export const useWorkspacePanelStore = create((set, get) => const requestKey = makePreviewKey(sessionId, tabId) const existing = get().previewTabsBySession[sessionId]?.find((tab) => tab.id === tabId) + const requestId = nextRequestId(previewRequestIds, requestKey) + if (existing) { set((state) => ({ activePreviewTabIdBySession: { ...state.activePreviewTabIdBySession, [sessionId]: tabId, }, + loading: { + ...state.loading, + previewByTabId: { + ...state.loading.previewByTabId, + [requestKey]: true, + }, + }, + errors: { + ...state.errors, + previewByTabId: { + ...state.errors.previewByTabId, + [requestKey]: null, + }, + }, })) - return - } + } else { + const baseTab: WorkspacePreviewTab = { + id: tabId, + path, + kind, + title: getPathTitle(path), + state: 'loading', + } - const requestId = nextRequestId(previewRequestIds, requestKey) - const baseTab: WorkspacePreviewTab = { - id: tabId, - path, - kind, - title: getPathTitle(path), - state: 'loading', + set((state) => ({ + previewTabsBySession: { + ...state.previewTabsBySession, + [sessionId]: [...(state.previewTabsBySession[sessionId] ?? []), baseTab], + }, + activePreviewTabIdBySession: { + ...state.activePreviewTabIdBySession, + [sessionId]: tabId, + }, + loading: { + ...state.loading, + previewByTabId: { + ...state.loading.previewByTabId, + [requestKey]: true, + }, + }, + errors: { + ...state.errors, + previewByTabId: { + ...state.errors.previewByTabId, + [requestKey]: null, + }, + }, + })) } - set((state) => ({ - previewTabsBySession: { - ...state.previewTabsBySession, - [sessionId]: [...(state.previewTabsBySession[sessionId] ?? []), baseTab], - }, - activePreviewTabIdBySession: { - ...state.activePreviewTabIdBySession, - [sessionId]: tabId, - }, - loading: { - ...state.loading, - previewByTabId: { - ...state.loading.previewByTabId, - [requestKey]: true, - }, - }, - errors: { - ...state.errors, - previewByTabId: { - ...state.errors.previewByTabId, - [requestKey]: null, - }, - }, - })) - try { if (kind === 'diff') { const result = await sessionsApi.getWorkspaceDiff(sessionId, path) diff --git a/src/server/__tests__/workspace-service.test.ts b/src/server/__tests__/workspace-service.test.ts index 0b0dc9a6..5d7c4fdd 100644 --- a/src/server/__tests__/workspace-service.test.ts +++ b/src/server/__tests__/workspace-service.test.ts @@ -187,6 +187,34 @@ describe('WorkspaceService', () => { expect(diff.diff?.length).toBeGreaterThan(0) }) + it('prefers the live git diff over stale transcript edits for a currently changed file', async () => { + const repoDir = await createGitWorkspace() + const service = new WorkspaceService( + async (sessionId) => sessionId === 'session-1' ? repoDir : null, + async () => [{ + id: 'assistant-1', + type: 'tool_use', + timestamp: new Date().toISOString(), + content: [{ + type: 'tool_use', + name: 'Edit', + input: { + file_path: 'tracked.txt', + old_string: 'before\n', + new_string: 'model snapshot\n', + }, + }], + }], + ) + + const diff = await service.getDiff('session-1', 'tracked.txt') + + expect(diff.state).toBe('ok') + expect(diff.diff).toContain('diff --git a/tracked.txt b/tracked.txt') + expect(diff.diff).toContain('+after') + expect(diff.diff).not.toContain('+model snapshot') + }) + it('returns explicit non-git and missing-workdir states', async () => { const nonGitDir = await makeTempDir('workspace-service-non-git-') const missingDir = path.join(await makeTempDir('workspace-service-missing-parent-'), 'missing') diff --git a/src/server/services/workspaceService.ts b/src/server/services/workspaceService.ts index eda596e0..3ad81ca8 100644 --- a/src/server/services/workspaceService.ts +++ b/src/server/services/workspaceService.ts @@ -562,25 +562,27 @@ export class WorkspaceService { } } - const sessionDiff = await this.getSessionDiff(sessionId, resolvedPath.relativePath) - if (sessionDiff) { - return { state: 'ok', path: resolvedPath.relativePath, diff: sessionDiff } - } - - const fileHistoryDiff = await this.getFileHistoryDiff( - sessionId, - resolvedPath.workspaceRoot, - resolvedPath.relativePath, - ) - if (fileHistoryDiff) { - return { state: 'ok', path: resolvedPath.relativePath, diff: fileHistoryDiff } - } - const repoInfo = await this.getGitRepoInfo(resolvedPath.workspaceRoot) if (repoInfo.kind === 'not_git_repo') { + const storedDiff = await this.getStoredWorkspaceDiff( + sessionId, + resolvedPath.workspaceRoot, + resolvedPath.relativePath, + ) + if (storedDiff) { + return { state: 'ok', path: resolvedPath.relativePath, diff: storedDiff } + } return { state: 'not_git_repo', path: resolvedPath.relativePath } } if (repoInfo.kind === 'error') { + const storedDiff = await this.getStoredWorkspaceDiff( + sessionId, + resolvedPath.workspaceRoot, + resolvedPath.relativePath, + ) + if (storedDiff) { + return { state: 'ok', path: resolvedPath.relativePath, diff: storedDiff } + } return { state: 'error', path: resolvedPath.relativePath, @@ -590,6 +592,14 @@ export class WorkspaceService { const statusEntries = await this.getStatusEntries(repoInfo.repoRoot) if (statusEntries.kind === 'error') { + const storedDiff = await this.getStoredWorkspaceDiff( + sessionId, + resolvedPath.workspaceRoot, + resolvedPath.relativePath, + ) + if (storedDiff) { + return { state: 'ok', path: resolvedPath.relativePath, diff: storedDiff } + } return { state: 'error', path: resolvedPath.relativePath, @@ -613,6 +623,14 @@ export class WorkspaceService { ) if (!statusEntry) { + const storedDiff = await this.getStoredWorkspaceDiff( + sessionId, + resolvedPath.workspaceRoot, + resolvedPath.relativePath, + ) + if (storedDiff) { + return { state: 'ok', path: resolvedPath.relativePath, diff: storedDiff } + } return { state: 'missing', path: resolvedPath.relativePath } } @@ -650,6 +668,21 @@ export class WorkspaceService { return { state: 'ok', path: resolvedPath.relativePath, diff: diff.diff } } + private async getStoredWorkspaceDiff( + sessionId: string, + workspaceRoot: string, + relativePath: string, + ): Promise { + const sessionDiff = await this.getSessionDiff(sessionId, relativePath) + if (sessionDiff) return sessionDiff + + return await this.getFileHistoryDiff( + sessionId, + workspaceRoot, + relativePath, + ) + } + private async getSessionDiff( sessionId: string, relativePath: string,