mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
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
This commit is contained in:
parent
a2e954b0f9
commit
b5161a4709
@ -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',
|
||||
|
||||
@ -457,50 +457,64 @@ export const useWorkspacePanelStore = create<WorkspacePanelStore>((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)
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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<string | null> {
|
||||
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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user