From 18c6a430339f4db3b749b1c4d185fa4d99067cf3 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 17:17:08 +0800 Subject: [PATCH] fix(desktop): collapse workspace navigator during previews (#706, #709) Keep the file navigator hidden when a preview is active so code and diff tabs get the full workspace width. Defer all-files tree loading until the navigator is explicitly opened to avoid unnecessary tree requests while previewing. Tested: cd desktop && bun run test src/components/workspace/WorkspacePanel.test.tsx --run Tested: bun run check:desktop Confidence: high Scope-risk: narrow --- .../workspace/WorkspacePanel.test.tsx | 120 ++++++++ .../components/workspace/WorkspacePanel.tsx | 263 ++++++++++-------- desktop/src/i18n/locales/en.ts | 2 + desktop/src/i18n/locales/jp.ts | 2 + desktop/src/i18n/locales/kr.ts | 2 + desktop/src/i18n/locales/zh-TW.ts | 2 + desktop/src/i18n/locales/zh.ts | 2 + 7 files changed, 274 insertions(+), 119 deletions(-) diff --git a/desktop/src/components/workspace/WorkspacePanel.test.tsx b/desktop/src/components/workspace/WorkspacePanel.test.tsx index 71f30172..10389ec3 100644 --- a/desktop/src/components/workspace/WorkspacePanel.test.tsx +++ b/desktop/src/components/workspace/WorkspacePanel.test.tsx @@ -804,6 +804,126 @@ describe('WorkspacePanel', () => { expect(view.getAllByText('b.ts').length).toBeGreaterThanOrEqual(1) }) + it('keeps the file navigator hidden while previewing until explicitly opened', async () => { + await setWorkspaceState((state) => ({ + ...state, + panelBySession: { + ...state.panelBySession, + 'session-preview-focused': { + isOpen: true, + activeView: 'changed', + hasUserSelectedView: true, + }, + }, + statusBySession: { + ...state.statusBySession, + 'session-preview-focused': { + state: 'ok', + workDir: '/repo', + repoName: 'repo', + branch: 'main', + isGitRepo: true, + changedFiles: [ + { + path: 'src/app.ts', + status: 'modified', + additions: 4, + deletions: 1, + }, + ], + }, + }, + previewTabsBySession: { + ...state.previewTabsBySession, + 'session-preview-focused': [{ + id: 'diff:src/app.ts', + path: 'src/app.ts', + kind: 'diff', + title: 'app.ts', + diff: '@@ -1 +1 @@\n-old\n+new', + state: 'ok', + }], + }, + activePreviewTabIdBySession: { + ...state.activePreviewTabIdBySession, + 'session-preview-focused': 'diff:src/app.ts', + }, + })) + + const view = await renderPanel('session-preview-focused') + + expect(view.getByTestId('workspace-code').textContent).toContain('+new') + expect(view.queryByRole('button', { name: 'Changed files' })).toBeNull() + expect(view.queryByPlaceholderText('Filter files...')).toBeNull() + + await clickElement(view.getByRole('button', { name: 'Show file navigator' })) + + expect(view.getByRole('button', { name: 'Changed files' })).toBeTruthy() + expect(view.getByPlaceholderText('Filter files...')).toBeTruthy() + expect(view.getByText('src/app.ts')).toBeTruthy() + expect(view.getByRole('button', { name: 'Hide file navigator' })).toBeTruthy() + }) + + it('defers all-files tree loading while the file navigator is hidden behind a preview', async () => { + getMocks().getWorkspaceTreeMock.mockResolvedValue({ + state: 'ok', + path: '', + entries: [{ name: 'src', path: 'src', isDirectory: true }], + }) + + await setWorkspaceState((state) => ({ + ...state, + panelBySession: { + ...state.panelBySession, + 'session-preview-hidden-tree': { + isOpen: true, + activeView: 'all', + hasUserSelectedView: true, + }, + }, + statusBySession: { + ...state.statusBySession, + 'session-preview-hidden-tree': { + state: 'ok', + workDir: '/repo', + repoName: 'repo', + branch: 'main', + isGitRepo: true, + changedFiles: [], + }, + }, + previewTabsBySession: { + ...state.previewTabsBySession, + 'session-preview-hidden-tree': [{ + id: 'file:src/app.ts', + path: 'src/app.ts', + kind: 'file', + title: 'app.ts', + content: 'export const ready = true', + language: 'typescript', + state: 'ok', + size: 25, + }], + }, + activePreviewTabIdBySession: { + ...state.activePreviewTabIdBySession, + 'session-preview-hidden-tree': 'file:src/app.ts', + }, + })) + + const view = await renderPanel('session-preview-hidden-tree') + await flushReactWork() + + expect(getMocks().getWorkspaceTreeMock).not.toHaveBeenCalled() + + await clickElement(view.getByRole('button', { name: 'Show file navigator' })) + + await waitFor(() => { + expect(getMocks().getWorkspaceTreeMock).toHaveBeenCalledWith('session-preview-hidden-tree', '') + }) + expect(view.getAllByText('src').length).toBeGreaterThanOrEqual(2) + }) + it('uses theme tokens for the panel, preview tabs, and code surface in dark mode', async () => { await setSettingsState({ ...settingsInitialState, locale: 'en', theme: 'dark' }) await setWorkspaceState((state) => ({ diff --git a/desktop/src/components/workspace/WorkspacePanel.tsx b/desktop/src/components/workspace/WorkspacePanel.tsx index 5e42bb08..d2b74313 100644 --- a/desktop/src/components/workspace/WorkspacePanel.tsx +++ b/desktop/src/components/workspace/WorkspacePanel.tsx @@ -917,6 +917,7 @@ export function WorkspacePanel({ sessionId, embedded = false }: WorkspacePanelPr const addToast = useUIStore((state) => state.addToast) const [filterQuery, setFilterQuery] = useState('') const [isViewMenuOpen, setIsViewMenuOpen] = useState(false) + const [isNavigatorOpen, setIsNavigatorOpen] = useState(false) const [previewTabContextMenu, setPreviewTabContextMenu] = useState<{ tabId: string; x: number; y: number } | null>(null) const [fileContextMenu, setFileContextMenu] = useState(null) const width = useWorkspacePanelStore((state) => state.width) @@ -959,6 +960,8 @@ export function WorkspacePanel({ sessionId, embedded = false }: WorkspacePanelPr const expandedPathSet = new Set(expandedPaths) const activePreviewTab = previewTabs.find((tab) => tab.id === activePreviewTabId) ?? previewTabs[previewTabs.length - 1] ?? null + const hasPreviewTabs = previewTabs.length > 0 + const isNavigatorVisible = !hasPreviewTabs || isNavigatorOpen const activeTreePath = activePreviewTab?.kind === 'file' ? activePreviewTab.path : null const filteredChangedFiles = useMemo( () => (status?.changedFiles ?? []).filter((file) => changedFileMatchesFilter(file, normalizedFilterQuery)), @@ -999,9 +1002,9 @@ export function WorkspacePanel({ sessionId, embedded = false }: WorkspacePanelPr }, [chatState, isOpen, loadStatus, sessionId, statusLoading]) useEffect(() => { - if (!isOpen || activeView !== 'all' || rootTree || rootTreeLoading || rootTreeError) return + if (!isOpen || !isNavigatorVisible || activeView !== 'all' || rootTree || rootTreeLoading || rootTreeError) return void loadTree(sessionId, '') - }, [activeView, isOpen, loadTree, rootTree, rootTreeError, rootTreeLoading, sessionId]) + }, [activeView, isNavigatorVisible, isOpen, loadTree, rootTree, rootTreeError, rootTreeLoading, sessionId]) useEffect(() => { if (!previewTabContextMenu && !fileContextMenu) return @@ -1013,9 +1016,20 @@ export function WorkspacePanel({ sessionId, embedded = false }: WorkspacePanelPr return () => document.removeEventListener('click', close) }, [fileContextMenu, previewTabContextMenu]) + useEffect(() => { + if (!hasPreviewTabs && isNavigatorOpen) { + setIsNavigatorOpen(false) + } + }, [hasPreviewTabs, isNavigatorOpen]) + + useEffect(() => { + if (!isNavigatorVisible) { + setIsViewMenuOpen(false) + } + }, [isNavigatorVisible]) + if (!isOpen) return null - const hasPreviewTabs = previewTabs.length > 0 const panelWidth = hasPreviewTabs ? width : Math.min(width, 520) const panelMaxWidth = hasPreviewTabs ? 'min(62%, calc(100% - 328px))' : '36%' const panelMinWidth = hasPreviewTabs ? 'min(420px, 54%)' : 'min(340px, 40%)' @@ -1280,60 +1294,69 @@ export function WorkspacePanel({ sessionId, embedded = false }: WorkspacePanelPr const renderPreviewTabs = () => ( <>
- {previewTabs.length === 0 ? ( -
- docs - {t('workspace.preview')} -
- ) : ( - previewTabs.map((tab) => { - const kindLabel = getPreviewKindLabel(t, tab.kind) - const isActive = tab.id === activePreviewTab?.id +
+ {previewTabs.length === 0 ? ( +
+ docs + {t('workspace.preview')} +
+ ) : ( + previewTabs.map((tab) => { + const kindLabel = getPreviewKindLabel(t, tab.kind) + const isActive = tab.id === activePreviewTab?.id - return ( -
handlePreviewTabContextMenu(event, tab.id)} - className={`group flex h-8 min-w-[118px] max-w-[210px] shrink-0 items-center gap-2 rounded-[8px] px-2 text-left text-[13px] transition-colors ${ - isActive - ? 'bg-[var(--color-surface-selected)] text-[var(--color-text-primary)]' - : 'text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)]' - }`} - > - - -
- ) - }) - )} + + +
+ ) + }) + )} +
+ setIsNavigatorOpen((open) => !open)} + /> {previewTabContextMenu && ( @@ -1400,82 +1423,84 @@ export function WorkspacePanel({ sessionId, embedded = false }: WorkspacePanelPr style={embedded ? undefined : { width: panelWidth, maxWidth: panelMaxWidth, minWidth: panelMinWidth }} > {hasPreviewTabs && ( -
+
{renderPreviewTabs()} {renderPreviewContent()}
)} -
-
-
- - {isViewMenuOpen && ( -
+
+
+ - ) - })} -
- )} -
+ + {activeView === 'changed' ? t('workspace.changedFiles') : t('workspace.allFiles')} + + expand_more + + {isViewMenuOpen && ( +
+ {(['changed', 'all'] as const).map((view) => { + const selected = activeView === view + return ( + + ) + })} +
+ )} +
-
- - {!embedded && ( +
closePanel(sessionId)} + icon="refresh" + label={t('workspace.refresh')} + onClick={handleRefresh} /> - )} + {!embedded && ( + closePanel(sessionId)} + /> + )} +
+
+ + + +
+ {activeView === 'changed' ? renderChangedView() : renderAllFilesView()}
- - - -
- {activeView === 'changed' ? renderChangedView() : renderAllFilesView()} -
-
+ )} {fileContextMenu && (
= { 'workspace.refresh': 'ワークスペースを更新', 'workspace.closePanel': 'ワークスペースパネルを閉じる', 'workspace.resizePanel': 'ワークスペースパネルのサイズを変更', + 'workspace.showNavigator': 'ファイルナビゲーターを表示', + 'workspace.hideNavigator': 'ファイルナビゲーターを非表示', 'workbench.modeSwitch': 'ワークベンチモード', 'workbench.modeWorkspace': 'ファイル', 'workbench.modeBrowser': 'ブラウザ', diff --git a/desktop/src/i18n/locales/kr.ts b/desktop/src/i18n/locales/kr.ts index d94f6897..07a8717d 100644 --- a/desktop/src/i18n/locales/kr.ts +++ b/desktop/src/i18n/locales/kr.ts @@ -127,6 +127,8 @@ export const kr: Record = { 'workspace.refresh': '작업 공간 새로 고침', 'workspace.closePanel': '작업 공간 패널 닫기', 'workspace.resizePanel': '작업 공간 패널 크기 조정', + 'workspace.showNavigator': '파일 탐색기 표시', + 'workspace.hideNavigator': '파일 탐색기 숨기기', 'workbench.modeSwitch': '워크벤치 모드', 'workbench.modeWorkspace': '파일', 'workbench.modeBrowser': '브라우저', diff --git a/desktop/src/i18n/locales/zh-TW.ts b/desktop/src/i18n/locales/zh-TW.ts index 615e4a7c..4dc93070 100644 --- a/desktop/src/i18n/locales/zh-TW.ts +++ b/desktop/src/i18n/locales/zh-TW.ts @@ -127,6 +127,8 @@ export const zh: Record = { 'workspace.refresh': '重新整理工作區', 'workspace.closePanel': '關閉工作區面板', 'workspace.resizePanel': '調整工作區寬度', + 'workspace.showNavigator': '顯示檔案導覽', + 'workspace.hideNavigator': '隱藏檔案導覽', 'workbench.modeSwitch': '工作臺模式', 'workbench.modeWorkspace': '檔案', 'workbench.modeBrowser': '瀏覽器', diff --git a/desktop/src/i18n/locales/zh.ts b/desktop/src/i18n/locales/zh.ts index de8adddd..314eb46a 100644 --- a/desktop/src/i18n/locales/zh.ts +++ b/desktop/src/i18n/locales/zh.ts @@ -127,6 +127,8 @@ export const zh: Record = { 'workspace.refresh': '刷新工作区', 'workspace.closePanel': '关闭工作区面板', 'workspace.resizePanel': '调整工作区宽度', + 'workspace.showNavigator': '显示文件导航', + 'workspace.hideNavigator': '隐藏文件导航', 'workbench.modeSwitch': '工作台模式', 'workbench.modeWorkspace': '文件', 'workbench.modeBrowser': '浏览器',