From 95931d498d72a28de4cf6dbcb66372e44f461841 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=99=E5=AD=90?= <113168461+706412584@users.noreply.github.com> Date: Fri, 12 Jun 2026 05:39:09 +0800 Subject: [PATCH] feat(desktop): default workspace panel to 'all' view (#23) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit R5 changes the workspace panel's default activeView from 'changed' to 'all' and removes the loadStatus auto-switching logic. Users now see the all-files tree on first open, regardless of whether the session has changed files. Explicit setActiveView still wins and survives subsequent loadStatus calls via hasUserSelectedView. The downstream WorkspacePanel component tests that asserted the old "auto-switch to changed view" behavior are updated: - 4 tests now explicitly setActiveView('changed') when they need to exercise the changed-files list. - 2 tests are rewritten/renamed (R5 prefix) to assert the new "default to all, never auto-flip" contract instead of the old switch-back behavior. Tested: - bun run lint (tsc --noEmit clean) - bun run test workspacePanelStore.test.ts (24/24 passed) - bun run test WorkspacePanel.test.tsx (27/27 passed) - bun run build (clean, 1.21s, no bundle impact) Note: 3 pre-existing failures in electron/services/{sidecarManager,terminal}.test.ts exist on origin/main baseline and are unrelated to this change. Co-authored-by: 你的姓名 --- .../workspace/WorkspacePanel.test.tsx | 24 ++++--- .../src/stores/workspacePanelStore.test.ts | 69 ++++++++----------- desktop/src/stores/workspacePanelStore.ts | 7 +- 3 files changed, 46 insertions(+), 54 deletions(-) diff --git a/desktop/src/components/workspace/WorkspacePanel.test.tsx b/desktop/src/components/workspace/WorkspacePanel.test.tsx index 255fd419..d0053417 100644 --- a/desktop/src/components/workspace/WorkspacePanel.test.tsx +++ b/desktop/src/components/workspace/WorkspacePanel.test.tsx @@ -329,6 +329,7 @@ describe('WorkspacePanel', () => { await act(() => { useWorkspacePanelStore.getState().openPanel('session-changed') + useWorkspacePanelStore.getState().setActiveView('session-changed', 'changed') }) const view = await renderPanel('session-changed') @@ -388,7 +389,7 @@ describe('WorkspacePanel', () => { expect(view.getAllByText('Diff').length).toBeGreaterThan(0) }) - it('refreshes status on open and switches back to changed files when new changes exist', async () => { + it('R5: refreshes status on open without overriding the all-files view when new changes exist', async () => { getMocks().getWorkspaceStatusMock.mockResolvedValue({ state: 'ok', workDir: '/repo', @@ -437,10 +438,14 @@ describe('WorkspacePanel', () => { await waitFor(() => { expect(getMocks().getWorkspaceStatusMock).toHaveBeenCalledWith('session-stale-all') }) + // R5: even when refreshed status reports new changed files, the active view stays on 'all'. await waitFor(() => { - expect(view.getByRole('button', { name: 'Changed files' })).toBeTruthy() + expect(useWorkspacePanelStore.getState().statusBySession['session-stale-all']?.changedFiles).toEqual([ + expect.objectContaining({ path: 'src/Fresh.ts' }), + ]) }) - expect(view.getByText('src/Fresh.ts')).toBeTruthy() + expect(useWorkspacePanelStore.getState().getActiveView('session-stale-all')).toBe('all') + expect(view.getByRole('button', { name: 'All files' })).toBeTruthy() }) it('loads workspace status when opened while the chat is running', async () => { @@ -470,6 +475,7 @@ describe('WorkspacePanel', () => { await act(() => { useWorkspacePanelStore.getState().openPanel('session-running-open') + useWorkspacePanelStore.getState().setActiveView('session-running-open', 'changed') }) const view = await renderPanel('session-running-open') @@ -507,6 +513,7 @@ describe('WorkspacePanel', () => { await act(() => { useWorkspacePanelStore.getState().openPanel('session-non-git') + useWorkspacePanelStore.getState().setActiveView('session-non-git', 'changed') }) const view = await renderPanel('session-non-git') @@ -526,7 +533,7 @@ describe('WorkspacePanel', () => { }) }) - it('opens to all files when the current turn has no changed files', async () => { + it('R5: opens to all files by default regardless of changed-file count', async () => { const statusRequest = deferred<{ state: 'ok' workDir: string @@ -549,7 +556,8 @@ describe('WorkspacePanel', () => { }) const view = await renderPanel('session-empty-tree') - expect(view.getByRole('button', { name: 'Changed files' })).toBeTruthy() + // R5: default view is 'all' — the toggle button should reflect that immediately. + expect(view.getByRole('button', { name: 'All files' })).toBeTruthy() await act(async () => { statusRequest.resolve({ @@ -625,10 +633,8 @@ describe('WorkspacePanel', () => { const view = await renderPanel('session-tree') - expect(view.getByRole('button', { name: 'Changed files' })).toBeTruthy() - - await clickElement(view.getByRole('button', { name: 'Changed files' })) - await clickElement(view.getByRole('menuitem', { name: 'All files' })) + // R5: default view is 'all', so the all-files tree loads without a manual toggle. + expect(view.getByRole('button', { name: 'All files' })).toBeTruthy() await waitFor(() => { expect(getMocks().getWorkspaceTreeMock).toHaveBeenCalledWith('session-tree', '') diff --git a/desktop/src/stores/workspacePanelStore.test.ts b/desktop/src/stores/workspacePanelStore.test.ts index 8f5fb994..3289920b 100644 --- a/desktop/src/stores/workspacePanelStore.test.ts +++ b/desktop/src/stores/workspacePanelStore.test.ts @@ -67,16 +67,16 @@ describe('workspacePanelStore', () => { const store = useWorkspacePanelStore.getState() expect(store.isPanelOpen('session-a')).toBe(false) - expect(store.getActiveView('session-a')).toBe('changed') + expect(store.getActiveView('session-a')).toBe('all') expect(store.width).toBe(WORKSPACE_PANEL_DEFAULT_WIDTH) store.openPanel('session-a') - store.setActiveView('session-a', 'all') + store.setActiveView('session-a', 'changed') expect(useWorkspacePanelStore.getState().isPanelOpen('session-a')).toBe(true) - expect(useWorkspacePanelStore.getState().getActiveView('session-a')).toBe('all') + expect(useWorkspacePanelStore.getState().getActiveView('session-a')).toBe('changed') expect(useWorkspacePanelStore.getState().isPanelOpen('session-b')).toBe(false) - expect(useWorkspacePanelStore.getState().getActiveView('session-b')).toBe('changed') + expect(useWorkspacePanelStore.getState().getActiveView('session-b')).toBe('all') store.togglePanel('session-b') expect(useWorkspacePanelStore.getState().isPanelOpen('session-b')).toBe(true) @@ -84,7 +84,7 @@ describe('workspacePanelStore', () => { store.closePanel('session-a') expect(useWorkspacePanelStore.getState().isPanelOpen('session-a')).toBe(false) - expect(useWorkspacePanelStore.getState().getActiveView('session-a')).toBe('all') + expect(useWorkspacePanelStore.getState().getActiveView('session-a')).toBe('changed') store.setWidth(120) expect(useWorkspacePanelStore.getState().width).toBe(WORKSPACE_PANEL_MIN_WIDTH) @@ -120,7 +120,11 @@ describe('workspacePanelStore', () => { expect(useWorkspacePanelStore.getState().errors.statusBySession['session-1']).toBeNull() }) - it('defaults an empty changed-files status to the all-files view', async () => { + it('R5: defaults activeView to "all" for a fresh session', () => { + expect(useWorkspacePanelStore.getState().getActiveView('session-fresh')).toBe('all') + }) + + it('R5: loadStatus does not flip activeView when status is empty', async () => { mocks.getWorkspaceStatusMock.mockResolvedValue({ state: 'ok', workDir: '/repo', @@ -131,7 +135,7 @@ describe('workspacePanelStore', () => { }) useWorkspacePanelStore.getState().openPanel('session-empty-changes') - expect(useWorkspacePanelStore.getState().getActiveView('session-empty-changes')).toBe('changed') + expect(useWorkspacePanelStore.getState().getActiveView('session-empty-changes')).toBe('all') await useWorkspacePanelStore.getState().loadStatus('session-empty-changes') @@ -139,7 +143,7 @@ describe('workspacePanelStore', () => { expect(useWorkspacePanelStore.getState().getActiveView('session-empty-changes')).toBe('all') }) - it('keeps the changed-files view when status contains changes', async () => { + it('R5: loadStatus does not flip activeView when status has changes', async () => { mocks.getWorkspaceStatusMock.mockResolvedValue({ state: 'ok', workDir: '/repo', @@ -157,44 +161,31 @@ describe('workspacePanelStore', () => { }) useWorkspacePanelStore.getState().openPanel('session-has-changes') + expect(useWorkspacePanelStore.getState().getActiveView('session-has-changes')).toBe('all') + await useWorkspacePanelStore.getState().loadStatus('session-has-changes') - expect(useWorkspacePanelStore.getState().getActiveView('session-has-changes')).toBe('changed') + expect(useWorkspacePanelStore.getState().getActiveView('session-has-changes')).toBe('all') }) - it('returns to changed-files when a refreshed default all-files view now has changes', async () => { - mocks.getWorkspaceStatusMock - .mockResolvedValueOnce({ - state: 'ok', - workDir: '/repo', - repoName: 'repo', - branch: 'main', - isGitRepo: true, - changedFiles: [], - }) - .mockResolvedValueOnce({ - state: 'ok', - workDir: '/repo', - repoName: 'repo', - branch: 'main', - isGitRepo: true, - changedFiles: [ - { - path: 'src/app.ts', - status: 'modified', - additions: 2, - deletions: 1, - }, - ], - }) + it('R5: setActiveView marks hasUserSelectedView and is preserved across loadStatus', async () => { + mocks.getWorkspaceStatusMock.mockResolvedValue({ + state: 'ok', + workDir: '/repo', + repoName: 'repo', + branch: 'main', + isGitRepo: true, + changedFiles: [], + }) - useWorkspacePanelStore.getState().openPanel('session-refresh-changes') - await useWorkspacePanelStore.getState().loadStatus('session-refresh-changes') - expect(useWorkspacePanelStore.getState().getActiveView('session-refresh-changes')).toBe('all') + useWorkspacePanelStore.getState().openPanel('session-explicit') + expect(useWorkspacePanelStore.getState().panelBySession['session-explicit']?.hasUserSelectedView).toBeFalsy() - await useWorkspacePanelStore.getState().loadStatus('session-refresh-changes') + useWorkspacePanelStore.getState().setActiveView('session-explicit', 'changed') + expect(useWorkspacePanelStore.getState().panelBySession['session-explicit']?.hasUserSelectedView).toBe(true) - expect(useWorkspacePanelStore.getState().getActiveView('session-refresh-changes')).toBe('changed') + await useWorkspacePanelStore.getState().loadStatus('session-explicit') + expect(useWorkspacePanelStore.getState().getActiveView('session-explicit')).toBe('changed') }) it('does not override an explicit all-files selection when refreshed status has changes', async () => { diff --git a/desktop/src/stores/workspacePanelStore.ts b/desktop/src/stores/workspacePanelStore.ts index b602ea05..d901b2c4 100644 --- a/desktop/src/stores/workspacePanelStore.ts +++ b/desktop/src/stores/workspacePanelStore.ts @@ -87,7 +87,7 @@ type WorkspacePanelStore = { const DEFAULT_PANEL_STATE: WorkspacePanelSessionState = { isOpen: false, - activeView: 'changed', + activeView: 'all', } const DEFAULT_WORKBENCH_MODE: WorkbenchMode = 'workspace' @@ -294,17 +294,12 @@ export const useWorkspacePanelStore = create((set, get) => set((state) => { const panel = getSessionPanelState(state.panelBySession, sessionId) - const nextActiveView = - !panel.hasUserSelectedView && result.state === 'ok' - ? result.changedFiles.length > 0 ? 'changed' : 'all' - : panel.activeView return { panelBySession: { ...state.panelBySession, [sessionId]: { ...panel, - activeView: nextActiveView, }, }, statusBySession: {