From af25edd3f76713f16ab016ae43003dc47e21e52f Mon Sep 17 00:00:00 2001 From: kostazol Date: Sun, 24 May 2026 19:49:22 +0700 Subject: [PATCH] Fix mobile open file list behavior for deleted and long-named files (#1391) * Fix mobile open file list cleanup and long names - Remove deleted files from persisted open file tabs - Invalidate cached file content when stat/delete/rename affects paths - Keep mobile open-file close buttons visible for long filenames - Add marquee scrolling for overflowing file names * Fix bot comments --------- Co-authored-by: Konstantin Zolin --- .../ui/src/components/views/FilesView.tsx | 103 ++++++++++++++++-- .../ui/src/contexts/RuntimeAPIProvider.tsx | 36 ++++-- packages/ui/src/index.css | 36 ++++++ 3 files changed, 156 insertions(+), 19 deletions(-) diff --git a/packages/ui/src/components/views/FilesView.tsx b/packages/ui/src/components/views/FilesView.tsx index 01d06de8..a829a780 100644 --- a/packages/ui/src/components/views/FilesView.tsx +++ b/packages/ui/src/components/views/FilesView.tsx @@ -224,6 +224,47 @@ const FileStatusDot: React.FC<{ status: FileStatus }> = ({ status }) => { return ; }; +const ScrollingFileName: React.FC<{ name: string }> = ({ name }) => { + const containerRef = React.useRef(null); + const textRef = React.useRef(null); + const [overflowing, setOverflowing] = React.useState(false); + + React.useLayoutEffect(() => { + const container = containerRef.current; + const text = textRef.current; + if (!container || !text) { + return; + } + + const updateOverflow = () => { + setOverflowing(text.scrollWidth > container.clientWidth + 1); + }; + + updateOverflow(); + const resizeObserver = new ResizeObserver(updateOverflow); + resizeObserver.observe(container); + resizeObserver.observe(text); + + return () => { + resizeObserver.disconnect(); + }; + }, [name]); + + return ( + + + {overflowing ? ( + + {name} + + + ) : ( + {name} + )} + + ); +}; + const shouldIgnoreEntryName = (name: string): boolean => DEFAULT_IGNORED_DIR_NAMES.has(name); const shouldIgnorePath = (path: string): boolean => { @@ -237,6 +278,15 @@ const isDirectoryReadError = (error: unknown): boolean => { return normalized.includes('is a directory') || normalized.includes('eisdir'); }; +const isFileMissingError = (error: unknown): boolean => { + const message = error instanceof Error ? error.message : String(error ?? ''); + const normalized = message.toLowerCase(); + return normalized.includes('file not found') + || normalized.includes('enoent') + || normalized.includes('no such file') + || normalized.includes('does not exist'); +}; + const MAX_VIEW_CHARS = 200_000; const FILE_EDITOR_AUTO_SAVE_KEY = 'openchamber:files:auto-save-enabled'; @@ -1347,6 +1397,32 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { return null; }, [files]); + React.useEffect(() => { + if (!root || !files.statFile || openPaths.length === 0) { + return; + } + + let cancelled = false; + const paths = [...openPaths]; + + void Promise.all(paths.map(async (path) => { + try { + const stat = await files.statFile?.(path); + if (!cancelled && stat && !stat.isFile) { + removeOpenPathsByPrefix(root, path); + } + } catch (error) { + if (!cancelled && isFileMissingError(error)) { + removeOpenPathsByPrefix(root, path); + } + } + })); + + return () => { + cancelled = true; + }; + }, [files, openPaths, removeOpenPathsByPrefix, root]); + const displayedContent = React.useMemo(() => fileContent.length > MAX_VIEW_CHARS ? `${fileContent.slice(0, MAX_VIEW_CHARS)}\n\n… truncated …` @@ -1592,6 +1668,19 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { } return; } + if (isFileMissingError(error)) { + if (root) { + removeOpenPathsByPrefix(root, node.path); + } + setFileContent(''); + setDraftContent(''); + setFileError(null); + lastLoadedFileStatRef.current = null; + if (isMobile) { + setShowMobilePageContent(false); + } + return; + } setFileContent(''); setDraftContent(''); setFileError(error instanceof Error ? error.message : t('filesView.error.readFileFailed')); @@ -1602,7 +1691,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { setFileLoading(false); } }); - }, [expandPaths, isMobile, loadDirectory, mode, readFile, readFileStat, root, runtime.isDesktop, searchQuery, setSelectedPath, t]); + }, [expandPaths, isMobile, loadDirectory, mode, readFile, readFileStat, removeOpenPathsByPrefix, root, runtime.isDesktop, searchQuery, setSelectedPath, t]); const ensurePathVisible = React.useCallback(async (targetPath: string, includeTarget: boolean) => { if (!root) { @@ -2889,11 +2978,11 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { aria-label={t('filesView.editor.openFilesAria')} > - {selectedFile.name} + - + {openFiles.map((file) => { const isActive = selectedFile?.path === file.path; return ( @@ -2910,13 +2999,13 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { } }} className={cn( - 'flex items-center justify-between gap-2', + 'flex min-w-0 items-center justify-between gap-2 overflow-hidden', isActive && 'bg-[var(--interactive-selection)] text-[var(--interactive-selection-foreground)]' )} > - + - {file.name} +