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 (
+
+ {name}
+ {overflowing ? (
+
+ {name}
+ {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}
+