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,