From 2290cf4b5978b8ec34318353f5c88da281841b7f Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Sun, 17 May 2026 23:31:28 +0300 Subject: [PATCH] Reduce React Doctor diagnostics in FilesView (#1271) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Reduce React Doctor diagnostics in FilesView * fix: address Greptile review feedback — use Base UI initialFocus and restore useMemo for displayedContent - Replace useEffect-based dialog focus with Base UI Popup's initialFocus prop to avoid race with built-in focus management - Drop unused no-op onOpenAutoFocus/onCloseAutoFocus from dialog wrapper - Restore useMemo for displayedContent (200K char slice) to avoid per-render string allocation for large files - Update DirectoryExplorerDialog to use initialFocus={false} --- .../session/DirectoryExplorerDialog.tsx | 2 +- packages/ui/src/components/ui/dialog.tsx | 6 - .../ui/src/components/views/FilesView.tsx | 339 ++++++++++-------- 3 files changed, 189 insertions(+), 158 deletions(-) diff --git a/packages/ui/src/components/session/DirectoryExplorerDialog.tsx b/packages/ui/src/components/session/DirectoryExplorerDialog.tsx index 73cf26c1..583f7571 100644 --- a/packages/ui/src/components/session/DirectoryExplorerDialog.tsx +++ b/packages/ui/src/components/session/DirectoryExplorerDialog.tsx @@ -707,7 +707,7 @@ export const DirectoryExplorerDialog: React.FC = ( event.preventDefault()} + initialFocus={false} >
diff --git a/packages/ui/src/components/ui/dialog.tsx b/packages/ui/src/components/ui/dialog.tsx index 06bb166a..bdccc80e 100644 --- a/packages/ui/src/components/ui/dialog.tsx +++ b/packages/ui/src/components/ui/dialog.tsx @@ -84,20 +84,14 @@ DialogOverlay.displayName = "DialogOverlay"; type DialogContentProps = Omit, "children"> & { showCloseButton?: boolean children?: React.ReactNode - onOpenAutoFocus?: (event: Event) => void - onCloseAutoFocus?: (event: Event) => void } function DialogContent({ className, children, showCloseButton = true, - onOpenAutoFocus, - onCloseAutoFocus, ...props }: DialogContentProps) { - void onOpenAutoFocus - void onCloseAutoFocus const { t } = useI18n() return ( diff --git a/packages/ui/src/components/views/FilesView.tsx b/packages/ui/src/components/views/FilesView.tsx index 8d153bc1..6a17a7cd 100644 --- a/packages/ui/src/components/views/FilesView.tsx +++ b/packages/ui/src/components/views/FilesView.tsx @@ -108,7 +108,7 @@ const OpenInAppListIcon = ({ label, iconDataUrl }: { label: string; iconDataUrl? setFailed(true)} /> ); @@ -117,7 +117,7 @@ const OpenInAppListIcon = ({ label, iconDataUrl }: { label: string; iconDataUrl? return ( @@ -221,7 +221,7 @@ const FileStatusDot: React.FC<{ status: FileStatus }> = ({ status }) => { 'git-deleted': 'var(--status-error)', }[status]; - return ; + return ; }; const shouldIgnoreEntryName = (name: string): boolean => DEFAULT_IGNORED_DIR_NAMES.has(name); @@ -358,9 +358,9 @@ const FileRow: React.FC = ({ > {isDir ? ( isExpanded ? ( - + ) : ( - + ) ) : ( getFileIcon(node.path, node.extension) @@ -392,16 +392,16 @@ const FileRow: React.FC = ({ setContextMenuPath(null)}> {canRename && ( { e.stopPropagation(); onOpenDialog('rename', node); }}> - {t('sidebarFilesTree.menu.rename')} + {t('sidebarFilesTree.menu.rename')} )} { @@ -414,7 +414,7 @@ const FileRow: React.FC = ({ toast.error(t('sidebarFilesTree.toast.copyFailed')); }); }}> - {t('sidebarFilesTree.menu.copyPath')} + {t('sidebarFilesTree.menu.copyPath')} { e.stopPropagation(); @@ -427,19 +427,19 @@ const FileRow: React.FC = ({ toast.error(t('sidebarFilesTree.toast.copyFailed')); }); }}> - {t('filesView.tree.menu.copyRelativePath')} + {t('filesView.tree.menu.copyRelativePath')} {!isDir && downloadFile && ( { e.stopPropagation(); void downloadFile(node.path); }}> - {t('sidebarFilesTree.menu.save')} + {t('sidebarFilesTree.menu.save')} )} {canReveal && ( { e.stopPropagation(); onRevealPath(node.path); }}> - {t(getRevealLabelKey())} + {t(getRevealLabelKey())} )} {isDir && (canCreateFile || canCreateFolder) && ( @@ -447,12 +447,12 @@ const FileRow: React.FC = ({ {canCreateFile && ( { e.stopPropagation(); onOpenDialog('createFile', node); }}> - {t('sidebarFilesTree.menu.newFile')} + {t('sidebarFilesTree.menu.newFile')} )} {canCreateFolder && ( { e.stopPropagation(); onOpenDialog('createFolder', node); }}> - {t('sidebarFilesTree.menu.newFolder')} + {t('sidebarFilesTree.menu.newFolder')} )} @@ -464,7 +464,7 @@ const FileRow: React.FC = ({ onClick={(e) => { e.stopPropagation(); onOpenDialog('delete', node); }} className="text-destructive focus:text-destructive" > - {t('sidebarFilesTree.menu.delete')} + {t('sidebarFilesTree.menu.delete')} )} @@ -476,6 +476,82 @@ const FileRow: React.FC = ({ ); }; +interface DialogsProps { + activeDialog: 'createFile' | 'createFolder' | 'rename' | 'delete' | null; + dialogData: { path: string; name?: string; type?: 'file' | 'directory' } | null; + dialogInputValue: string; + onDialogInputChange: (value: string) => void; + isDialogSubmitting: boolean; + onDialogSubmit: (e?: React.FormEvent) => Promise; + onClose: () => void; + inputRef: React.RefObject; +} + +const Dialogs: React.FC = ({ + activeDialog, + dialogData, + dialogInputValue, + onDialogInputChange, + isDialogSubmitting, + onDialogSubmit, + onClose, + inputRef, +}) => { + const { t } = useI18n(); + + return ( + !open && onClose()}> + + + + {activeDialog === 'createFile' && t('filesView.dialog.createFile.title')} + {activeDialog === 'createFolder' && t('filesView.dialog.createFolder.title')} + {activeDialog === 'rename' && t('filesView.dialog.rename.title')} + {activeDialog === 'delete' && t('filesView.dialog.delete.title')} + + + {activeDialog === 'createFile' && t('filesView.dialog.createFile.description', { path: dialogData?.path ?? t('filesView.dialog.rootFallback') })} + {activeDialog === 'createFolder' && t('filesView.dialog.createFolder.description', { path: dialogData?.path ?? t('filesView.dialog.rootFallback') })} + {activeDialog === 'rename' && t('filesView.dialog.rename.description', { name: dialogData?.name ?? '' })} + {activeDialog === 'delete' && t('filesView.dialog.delete.description', { name: dialogData?.name ?? '' })} + + + + {activeDialog !== 'delete' && ( +
+ onDialogInputChange(e.target.value)} + placeholder={activeDialog === 'rename' ? t('filesView.dialog.rename.placeholder') : t('filesView.dialog.namePlaceholder')} + onKeyDown={(e) => { + if (e.key === 'Enter') { + void onDialogSubmit(); + } + }} + ref={inputRef} + /> +
+ )} + + + + + +
+
+ ); +}; + interface FilesViewProps { mode?: 'full' | 'editor-only'; } @@ -602,18 +678,21 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { right: el.scrollLeft + el.clientWidth < el.scrollWidth - 2, }); }, []); + const updateEditorTabsOverflowRef = React.useRef(updateEditorTabsOverflow); + updateEditorTabsOverflowRef.current = updateEditorTabsOverflow; React.useEffect(() => { const el = editorTabsScrollRef.current; if (!el) return; - updateEditorTabsOverflow(); - el.addEventListener('scroll', updateEditorTabsOverflow, { passive: true }); - const ro = new ResizeObserver(updateEditorTabsOverflow); + const handler = () => updateEditorTabsOverflowRef.current(); + handler(); + el.addEventListener('scroll', handler, { passive: true }); + const ro = new ResizeObserver(handler); ro.observe(el); return () => { - el.removeEventListener('scroll', updateEditorTabsOverflow); + el.removeEventListener('scroll', handler); ro.disconnect(); }; - }, [updateEditorTabsOverflow, openFiles.length]); + }, [openFiles.length]); const [childrenByDir, setChildrenByDir] = React.useState>({}); const loadedDirsRef = React.useRef>(new Set()); @@ -633,6 +712,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { const [draftContent, setDraftContent] = React.useState(''); const [isSaving, setIsSaving] = React.useState(false); + const dialogInputRef = React.useRef(null); const autoSaveTimerRef = React.useRef | null>(null); const lastLoadedFileStatRef = React.useRef(null); const activeFileLoadIdRef = React.useRef(0); @@ -836,27 +916,22 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { }, [lineSelection, saveComment]); const mapDirectoryEntries = React.useCallback((dirPath: string, entries: Array<{ name: string; path: string; isDirectory: boolean }>): FileNode[] => { - const nodes = entries - .filter((entry) => entry && typeof entry.name === 'string' && entry.name.length > 0) - .filter((entry) => showHidden || !entry.name.startsWith('.')) - .filter((entry) => showGitignored || !shouldIgnoreEntryName(entry.name)) - .map((entry) => { - const name = entry.name; - const normalizedEntryPath = normalizePath(entry.path || ''); - const path = normalizedEntryPath - ? (isAbsolutePath(normalizedEntryPath) - ? normalizedEntryPath - : normalizePath(`${dirPath}/${normalizedEntryPath}`)) - : normalizePath(`${dirPath}/${name}`); - const type = entry.isDirectory ? 'directory' : 'file'; - const extension = type === 'file' && name.includes('.') ? name.split('.').pop()?.toLowerCase() : undefined; - return { - name, - path, - type, - extension, - }; - }); + const nodes: FileNode[] = []; + for (const entry of entries) { + if (!(entry && typeof entry.name === 'string' && entry.name.length > 0)) continue; + if (!showHidden && entry.name.startsWith('.')) continue; + if (!showGitignored && shouldIgnoreEntryName(entry.name)) continue; + const name = entry.name; + const normalizedEntryPath = normalizePath(entry.path || ''); + const path = normalizedEntryPath + ? (isAbsolutePath(normalizedEntryPath) + ? normalizedEntryPath + : normalizePath(`${dirPath}/${normalizedEntryPath}`)) + : normalizePath(`${dirPath}/${name}`); + const type = entry.isDirectory ? 'directory' : 'file'; + const extension = type === 'file' && name.includes('.') ? name.split('.').pop()?.toLowerCase() : undefined; + nodes.push({ name, path, type, extension }); + } return sortNodes(nodes); }, [showGitignored, showHidden]); @@ -1272,13 +1347,14 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { return null; }, [files]); - const displayedContent = React.useMemo(() => { - return fileContent.length > MAX_VIEW_CHARS + const displayedContent = React.useMemo(() => + fileContent.length > MAX_VIEW_CHARS ? `${fileContent.slice(0, MAX_VIEW_CHARS)}\n\n… truncated …` - : fileContent; - }, [fileContent]); + : fileContent, + [fileContent] + ); - const isDirty = React.useMemo(() => draftContent !== displayedContent, [draftContent, displayedContent]); + const isDirty = draftContent !== displayedContent; const saveDraft = React.useCallback(async () => { if (!selectedFile || !files.writeFile) { @@ -1540,11 +1616,13 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { expandPaths(root, pathsToExpand); } - for (const path of pathsToExpand) { + const loadPromises = pathsToExpand.map((path) => { if (!loadedDirsRef.current.has(path)) { - await loadDirectory(path); + return loadDirectory(path); } - } + return undefined; + }).filter(Boolean); + await Promise.all(loadPromises); }, [expandPaths, loadDirectory, root]); const getNextOpenFile = React.useCallback((path: string, filesList: FileNode[]) => { @@ -2293,7 +2371,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { const viewport = window.visualViewport; viewport?.addEventListener('resize', runNudge); - viewport?.addEventListener('scroll', runNudge); + viewport?.addEventListener('scroll', runNudge, { passive: true }); document.addEventListener('selectionchange', runNudge); return () => { @@ -2431,57 +2509,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { }; }, [files, isSelectedImage, isSelectedSvg, runtime.isDesktop, selectedFile?.path, selectedFileReadOptions, t]); - const renderDialogs = () => ( - !open && setActiveDialog(null)}> - - - - {activeDialog === 'createFile' && t('filesView.dialog.createFile.title')} - {activeDialog === 'createFolder' && t('filesView.dialog.createFolder.title')} - {activeDialog === 'rename' && t('filesView.dialog.rename.title')} - {activeDialog === 'delete' && t('filesView.dialog.delete.title')} - - - {activeDialog === 'createFile' && t('filesView.dialog.createFile.description', { path: dialogData?.path ?? t('filesView.dialog.rootFallback') })} - {activeDialog === 'createFolder' && t('filesView.dialog.createFolder.description', { path: dialogData?.path ?? t('filesView.dialog.rootFallback') })} - {activeDialog === 'rename' && t('filesView.dialog.rename.description', { name: dialogData?.name ?? '' })} - {activeDialog === 'delete' && t('filesView.dialog.delete.description', { name: dialogData?.name ?? '' })} - - - - {activeDialog !== 'delete' && ( -
- setDialogInputValue(e.target.value)} - placeholder={activeDialog === 'rename' ? t('filesView.dialog.rename.placeholder') : t('filesView.dialog.namePlaceholder')} - onKeyDown={(e) => { - if (e.key === 'Enter') { - void handleDialogSubmit(); - } - }} - autoFocus - /> -
- )} - - - - - -
-
- ); + const handleCloseDialog = React.useCallback(() => setActiveDialog(null), []); const blockWidgets = React.useMemo(() => { return buildCodeMirrorCommentWidgets({ @@ -2539,12 +2567,12 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { <> {isSaving ? ( - + {t('filesView.editor.saving')} ) : autoSaveEnabled && autoSaveStatus === 'saved' && !isDirty ? ( - + {t('filesView.editor.saved')} ) : isDirty ? ( @@ -2556,7 +2584,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { title={t(autoSaveEnabled ? 'filesView.editor.saveNowTitle' : 'filesView.editor.saveNowManualTitle', { shortcut: `${getModifierLabel()}+S` })} aria-label={t('filesView.editor.saveAria', { shortcut: `${getModifierLabel()}+S` })} > - + ) : null} @@ -2603,7 +2631,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { className="flex items-center gap-2" onClick={() => void loadOpenInApps(true)} > - + {t('filesView.editor.refreshApps')} ) : null} @@ -2617,7 +2645,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { size="sm" onClick={() => setWrapLines(!wrapLines)} className={cn( - 'h-6 w-6 p-0 transition-opacity hover:bg-transparent focus-visible:bg-transparent active:bg-transparent', + 'size-6 p-0 transition-opacity hover:bg-transparent focus-visible:bg-transparent active:bg-transparent', wrapLines ? 'text-foreground opacity-100' : 'text-muted-foreground opacity-65 hover:opacity-100' )} title={wrapLines ? t('filesView.editor.disableLineWrap') : t('filesView.editor.enableLineWrap')} @@ -2633,7 +2661,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { setIsSearchOpen(!isSearchOpen); event.currentTarget.blur(); }} - className="h-6 w-6 p-0 text-foreground opacity-100 transition-opacity hover:bg-transparent focus-visible:bg-transparent active:bg-transparent" + className="size-6 p-0 text-foreground opacity-100 transition-opacity hover:bg-transparent focus-visible:bg-transparent active:bg-transparent" title={t('filesView.editor.findInFile')} > @@ -2645,7 +2673,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { setIsGoToLineOpen((open) => !open); event.currentTarget.blur(); }} - className="h-6 w-6 p-0 text-foreground opacity-100 transition-opacity hover:bg-transparent focus-visible:bg-transparent active:bg-transparent" + className="size-6 p-0 text-foreground opacity-100 transition-opacity hover:bg-transparent focus-visible:bg-transparent active:bg-transparent" title={t('filesView.editor.goToLine')} > @@ -2688,7 +2716,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { variant="ghost" size="sm" onClick={() => saveJsonViewMode(jsonViewMode === 'tree' ? 'text' : 'tree')} - className="h-6 w-6 p-0 text-muted-foreground opacity-65 hover:bg-transparent hover:opacity-100 focus-visible:bg-transparent active:bg-transparent" + className="size-6 p-0 text-muted-foreground opacity-65 hover:bg-transparent hover:opacity-100 focus-visible:bg-transparent active:bg-transparent" title={jsonViewMode === 'tree' ? t('filesView.editor.switchToTextView') : t('filesView.editor.switchToTreeView')} > {jsonViewMode === 'tree' ? ( @@ -2717,14 +2745,14 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { toast.error(t('filesView.toast.copyFailed')); } }} - className="h-6 w-6 p-0 hover:bg-transparent focus-visible:bg-transparent active:bg-transparent" + className="size-6 p-0 hover:bg-transparent focus-visible:bg-transparent active:bg-transparent" title={t('filesView.editor.copyFileContents')} aria-label={t('filesView.editor.copyFileContents')} > {copiedContent ? ( - + ) : ( - + )} )} @@ -2747,14 +2775,14 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { toast.error(t('filesView.toast.copyFailed')); } }} - className="h-6 w-6 p-0 hover:bg-transparent focus-visible:bg-transparent active:bg-transparent" + className="size-6 p-0 hover:bg-transparent focus-visible:bg-transparent active:bg-transparent" title={t('filesView.editor.copyFilePathTitle', { path: displaySelectedPath })} aria-label={t('filesView.editor.copyFilePathTitle', { path: displaySelectedPath })} > {copiedPath ? ( - + ) : ( - + )} )} @@ -2767,11 +2795,11 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { const fn = files.downloadFile; if (fn) void fn(selectedFile.path); }} - className="h-6 w-6 p-0 hover:bg-transparent focus-visible:bg-transparent active:bg-transparent" + className="size-6 p-0 hover:bg-transparent focus-visible:bg-transparent active:bg-transparent" title={t('filesView.editor.saveFile')} aria-label={t('filesView.editor.saveFile')} > - + )} @@ -2780,25 +2808,25 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { variant="ghost" size="sm" onClick={() => setIsFullscreen(false)} - className="h-6 w-6 p-0 hover:bg-transparent focus-visible:bg-transparent active:bg-transparent" + className="size-6 p-0 hover:bg-transparent focus-visible:bg-transparent active:bg-transparent" title={t('filesView.editor.exitFullscreen')} aria-label={t('filesView.editor.exitFullscreen')} > - + ) : (!isMobile && mode === 'full' && ( ))} @@ -2845,9 +2873,9 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { type="button" onClick={() => setShowMobilePageContent(false)} aria-label={t('filesView.editor.back')} - className="inline-flex h-7 w-7 flex-shrink-0 items-center justify-center mr-1 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary" + className="inline-flex size-7 flex-shrink-0 items-center justify-center mr-1 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary" > - + )} @@ -2860,9 +2888,9 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { className="inline-flex min-w-0 max-w-full items-center gap-1 text-left typography-ui-label font-medium" aria-label={t('filesView.editor.openFilesAria')} > - + {selectedFile.name} - + @@ -2887,7 +2915,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { )} > - + {file.name} ); @@ -2942,7 +2970,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { : 'bg-transparent border-[var(--interactive-border)] text-[var(--surface-muted-foreground)] hover:bg-[var(--interactive-hover)] hover:text-[var(--surface-foreground)]' )} > - +
); @@ -3000,11 +3028,11 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { variant="ghost" size="sm" onClick={() => setIsFloatingToolbarOpen(true)} - className="h-8 w-8 rounded-lg border border-[var(--interactive-border)] bg-[var(--surface-elevated)] p-0 text-muted-foreground shadow-sm hover:text-foreground" + className="size-8 rounded-lg border border-[var(--interactive-border)] bg-[var(--surface-elevated)] p-0 text-muted-foreground shadow-sm hover:text-foreground" aria-label={t('filesView.editor.showControlsAria')} title={t('filesView.editor.controlsTitle')} > - + )} @@ -3017,7 +3045,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { ?
: (
- + {t('filesView.state.loading')}
) @@ -3195,7 +3223,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { {shouldMaskEditorForPendingNavigation && (
- + {t('filesView.state.openingFileAtChange')}
@@ -3217,7 +3245,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => {
- + = ({ mode = 'full' }) => { )}
@@ -3243,22 +3271,22 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { variant="ghost" size="sm" onClick={() => handleOpenDialog('createFile', { path: currentDirectory, type: 'directory' })} - className="h-8 w-8 p-0 flex-shrink-0" + className="size-8 p-0 flex-shrink-0" title={t('filesView.tree.actions.newFileTitle')} > - + -
@@ -3267,7 +3295,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => {
    {searching ? (
  • - + {t('filesView.tree.search.searching')}
  • ) : searchResults.length > 0 ? ( @@ -3319,7 +3347,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { ?
    : (
    - + Loading…
    ) @@ -3384,7 +3412,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { {shouldMaskEditorForPendingNavigation && (
    - + {t('filesView.state.openingFileAtChange')}
    @@ -3398,7 +3426,16 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { return (
    - {renderDialogs()} + {fullscreenViewer} {isMobile ? ( showMobilePageContent ? (