From d04712ce89356354f2eafce47c7670451b9215e9 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Mon, 20 Jul 2026 19:25:12 +0300 Subject: [PATCH] fix: stop stale file focus requests from stealing context panel selection --- .../ui/src/components/views/FilesView.tsx | 311 +++++++++--------- 1 file changed, 154 insertions(+), 157 deletions(-) diff --git a/packages/ui/src/components/views/FilesView.tsx b/packages/ui/src/components/views/FilesView.tsx index d3603d18..21eeca30 100644 --- a/packages/ui/src/components/views/FilesView.tsx +++ b/packages/ui/src/components/views/FilesView.tsx @@ -811,10 +811,10 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { const selectedPath = useFilesViewTabsStore((state) => (root ? (state.byRoot[root]?.selectedPath ?? null) : null)); const expandedPaths = useFilesViewTabsStore((state) => (root ? (state.byRoot[root]?.expandedPaths ?? EMPTY_PATHS) : EMPTY_PATHS)); const addOpenPath = useFilesViewTabsStore((state) => state.addOpenPath); - const removeOpenPath = useFilesViewTabsStore((state) => state.removeOpenPath); - const removeOpenPathsByPrefix = useFilesViewTabsStore((state) => state.removeOpenPathsByPrefix); - const removeExpandedPathsByPrefix = useFilesViewTabsStore((state) => state.removeExpandedPathsByPrefix); - const setSelectedPath = useFilesViewTabsStore((state) => state.setSelectedPath); + const removeOpenPath = useFilesViewTabsStore((state) => state.removeOpenPath); + const removeOpenPathsByPrefix = useFilesViewTabsStore((state) => state.removeOpenPathsByPrefix); + const removeExpandedPathsByPrefix = useFilesViewTabsStore((state) => state.removeExpandedPathsByPrefix); + const setSelectedPath = useFilesViewTabsStore((state) => state.setSelectedPath); const toggleExpandedPath = useFilesViewTabsStore((state) => state.toggleExpandedPath); const expandPaths = useFilesViewTabsStore((state) => state.expandPaths); @@ -1204,22 +1204,22 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { return; } - const message = error instanceof Error ? error.message : String(error ?? ''); - if (message === 'Directory not found' && root && normalizedDir !== root) { - removeExpandedPathsByPrefix(root, normalizedDir); - setLoadErrorsByDir((prev) => { - if (!prev[normalizedDir]) return prev; - const next = { ...prev }; - delete next[normalizedDir]; - return next; - }); - return; - } - console.error('Failed to load files directory:', error); - setLoadErrorsByDir((prev) => ({ - ...prev, - [normalizedDir]: message, - })); + const message = error instanceof Error ? error.message : String(error ?? ''); + if (message === 'Directory not found' && root && normalizedDir !== root) { + removeExpandedPathsByPrefix(root, normalizedDir); + setLoadErrorsByDir((prev) => { + if (!prev[normalizedDir]) return prev; + const next = { ...prev }; + delete next[normalizedDir]; + return next; + }); + return; + } + console.error('Failed to load files directory:', error); + setLoadErrorsByDir((prev) => ({ + ...prev, + [normalizedDir]: message, + })); }) .finally(() => { if (!isCurrentRequest()) { @@ -1231,7 +1231,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { inFlightDirsRef.current = new Set(inFlightDirsRef.current); inFlightDirsRef.current.delete(normalizedDir); }); - }, [files, mapDirectoryEntries, removeExpandedPathsByPrefix, root]); + }, [files, mapDirectoryEntries, removeExpandedPathsByPrefix, root]); const refreshRoot = React.useCallback(async () => { if (!root) { @@ -2322,14 +2322,14 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { const canEdit = Boolean(selectedFile && !selectedFileIsOutsideWorkspace && !isSelectedImage && !isSelectedPdf && files.writeFile && fileContent.length <= MAX_VIEW_CHARS); const isMarkdown = Boolean(selectedFile?.path && isMarkdownFile(selectedFile.path)); const isJson = Boolean(selectedFile?.path && isJsonFile(selectedFile.path)); - const isHtml = Boolean(selectedFile?.path && isHtmlFile(selectedFile.path)); - const isDrawio = Boolean(selectedFile?.path && isDrawioFile(selectedFile.path)); - const isTextFile = Boolean(selectedFile && !isSelectedImage && !isSelectedPdf); - const canUseShikiFileView = isTextFile && !isMarkdown && !isDrawio && !(isHtml && htmlViewMode === 'preview'); - const isEditingFile = (isMarkdown && mdViewMode === 'edit') - || (isHtml && htmlViewMode === 'edit') - || (isJson && jsonViewMode === 'text') - || (!isMarkdown && !isHtml && !isJson && textViewMode === 'edit'); + const isHtml = Boolean(selectedFile?.path && isHtmlFile(selectedFile.path)); + const isDrawio = Boolean(selectedFile?.path && isDrawioFile(selectedFile.path)); + const isTextFile = Boolean(selectedFile && !isSelectedImage && !isSelectedPdf); + const canUseShikiFileView = isTextFile && !isMarkdown && !isDrawio && !(isHtml && htmlViewMode === 'preview'); + const isEditingFile = (isMarkdown && mdViewMode === 'edit') + || (isHtml && htmlViewMode === 'edit') + || (isJson && jsonViewMode === 'text') + || (!isMarkdown && !isHtml && !isJson && textViewMode === 'edit'); const staticLanguageExtension = React.useMemo( () => (selectedFilePath ? languageByExtension(selectedFilePath) : null), [selectedFilePath], @@ -2373,7 +2373,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { return; } - setTextViewMode(textViewModeByPathRef.current[selectedPath] ?? 'edit'); + setTextViewMode(textViewModeByPathRef.current[selectedPath] ?? 'edit'); // Respect per-type localStorage preference when available, // falling back to the setting-derived default when nothing is stored. @@ -2398,7 +2398,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { // Ignore localStorage errors } setHtmlViewMode(htmlViewModeByPathRef.current[selectedPath] ?? htmlDefault); - setDrawioViewMode(drawioViewModeByPathRef.current[selectedPath] ?? (settingsDefaultFileViewerPreview ? 'preview' : 'edit')); + setDrawioViewMode(drawioViewModeByPathRef.current[selectedPath] ?? (settingsDefaultFileViewerPreview ? 'preview' : 'edit')); let jsonDefault: 'tree' | 'text' = settingsDefaultFileViewerPreview ? 'tree' : 'text'; try { @@ -2562,28 +2562,28 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { }, [htmlViewMode]); React.useEffect(() => { - const applyDefaultFileViewerMode = (enabled: boolean) => { - const previewMode: PreviewViewMode = enabled ? 'preview' : 'edit'; - const nextJsonMode: 'tree' | 'text' = enabled ? 'tree' : 'text'; - - for (const path of openPaths) { - textViewModeByPathRef.current[path] = 'edit'; - if (isMarkdownFile(path)) { - mdViewModeByPathRef.current[path] = previewMode; - } - if (isHtmlFile(path)) { - htmlViewModeByPathRef.current[path] = previewMode; - } - if (isDrawioFile(path)) { - drawioViewModeByPathRef.current[path] = previewMode; - } - } - - setTextViewMode('edit'); - setMdViewMode(previewMode); - setHtmlViewMode(previewMode); - setDrawioViewMode(previewMode); - setJsonViewMode(nextJsonMode); + const applyDefaultFileViewerMode = (enabled: boolean) => { + const previewMode: PreviewViewMode = enabled ? 'preview' : 'edit'; + const nextJsonMode: 'tree' | 'text' = enabled ? 'tree' : 'text'; + + for (const path of openPaths) { + textViewModeByPathRef.current[path] = 'edit'; + if (isMarkdownFile(path)) { + mdViewModeByPathRef.current[path] = previewMode; + } + if (isHtmlFile(path)) { + htmlViewModeByPathRef.current[path] = previewMode; + } + if (isDrawioFile(path)) { + drawioViewModeByPathRef.current[path] = previewMode; + } + } + + setTextViewMode('edit'); + setMdViewMode(previewMode); + setHtmlViewMode(previewMode); + setDrawioViewMode(previewMode); + setJsonViewMode(nextJsonMode); try { localStorage.setItem(MD_VIEWER_MODE_KEY, previewMode); @@ -2762,32 +2762,30 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { } if (selectedFile?.path !== targetPath) { - if (confirmDiscardOpen) { - return; - } - void handleSelectFile(toFileNode(targetPath)); + // Selection is owned by the tab sync / user. A pending focus request must + // not steal selection back (e.g. after the user switched to another tab + // while this file was still loading). Wait; clear once it loads or the + // request is superseded. return; } - if (fileLoading || loadedFilePath !== targetPath || fileError || isSelectedImage || isSelectedPdf) { + if (fileLoading || loadedFilePath !== targetPath) { return; } - if (canEdit && textViewMode === 'edit') { - const view = editorViewRef.current; - if (!view) { - return; - } - view.focus(); + // Best-effort focus: preview renderers (markdown/html preview, drawio, + // JSON tree, images, PDFs) never mount a CodeMirror editor, so the request + // must clear regardless — otherwise it lingers and replays on every + // dependency change. + if (!fileError && !isSelectedImage && !isSelectedPdf && canEdit && textViewMode === 'edit') { + editorViewRef.current?.focus(); } setPendingFileFocusPath(null); }, [ canEdit, - confirmDiscardOpen, fileError, fileLoading, - handleSelectFile, isSelectedImage, isSelectedPdf, loadedFilePath, @@ -2796,7 +2794,6 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { selectedFile?.path, setPendingFileFocusPath, textViewMode, - toFileNode, ]); const nudgeEditorSelectionAboveKeyboard = React.useCallback((view: EditorView | null) => { @@ -3158,7 +3155,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { return (
- {canEdit && isEditingFile && ( + {canEdit && isEditingFile && ( <> {isSaving ? ( @@ -3308,35 +3305,35 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { /> )} - {isMarkdown && ( - withTooltip( - t(getMdViewMode() === 'preview' ? 'filesView.editor.switchToEditMode' : 'filesView.editor.switchToPreviewMode'), - - ) - )} - - {isHtmlFile(selectedFile?.path ?? '') && ( - { - saveHtmlViewMode(getHtmlViewMode() === 'preview' ? 'edit' : 'preview'); - }} - /> - )} + {isMarkdown && ( + withTooltip( + t(getMdViewMode() === 'preview' ? 'filesView.editor.switchToEditMode' : 'filesView.editor.switchToPreviewMode'), + + ) + )} + + {isHtmlFile(selectedFile?.path ?? '') && ( + { + saveHtmlViewMode(getHtmlViewMode() === 'preview' ? 'edit' : 'preview'); + }} + /> + )} {isMarkdown && getMdViewMode() === 'preview' && showMessageTTSButtons && ( @@ -3736,68 +3733,68 @@ export const FilesView: React.FC = ({ mode = 'full' }) => {
{selectedFile && !isSearchOpen && !(settingsExpandedEditorToolbar && !isMobile) && ( -
{ - if (toolbarDropdownOpenCountRef.current > 0) return; - setIsFloatingToolbarOpen(false); +
{ + if (toolbarDropdownOpenCountRef.current > 0) return; + setIsFloatingToolbarOpen(false); }} > - {isFloatingToolbarOpen ? ( - renderFloatingFileControls() - ) : ( -
- {isMarkdown ? ( - - - - - - - - {t(getMdViewMode() === 'preview' ? 'filesView.editor.switchToEditMode' : 'filesView.editor.switchToPreviewMode')} - - - ) : null} - - - setIsFloatingToolbarOpen(true)} - > - - - - {t('filesView.editor.controlsTitle')} - -
- )} -
- )} + {isFloatingToolbarOpen ? ( + renderFloatingFileControls() + ) : ( +
+ {isMarkdown ? ( + + + + + + + + {t(getMdViewMode() === 'preview' ? 'filesView.editor.switchToEditMode' : 'filesView.editor.switchToPreviewMode')} + + + ) : null} + + + setIsFloatingToolbarOpen(true)} + > + + + + {t('filesView.editor.controlsTitle')} + +
+ )} +
+ )} {!selectedFile ? (
{t('filesView.editor.pickFileFromTree')}