diff --git a/src/workspaces/officerdev/src/apps/CodeEditor/CodeEditor.tsx b/src/workspaces/officerdev/src/apps/CodeEditor/CodeEditor.tsx index 9ca79226..28a8449a 100644 --- a/src/workspaces/officerdev/src/apps/CodeEditor/CodeEditor.tsx +++ b/src/workspaces/officerdev/src/apps/CodeEditor/CodeEditor.tsx @@ -45,6 +45,7 @@ export const CodeEditorView = ({ closeFile, setContent, reloadFile, + markMissing, markSaved, getActiveFile, searchForFile, @@ -185,10 +186,12 @@ export const CodeEditorView = ({ if (file.isDirty) continue; try { const res = await readFileRef.current(file.path); - if (!alive || res.content === file.content) continue; - reloadFile(file.path, res.content); + if (!alive) continue; + if (res.content !== file.content) reloadFile(file.path, res.content); } catch { - // Gone, or unreadable. Leave the tab exactly as it is — see above. + // Gone, or unreadable. The tab stays — see above — but it says so now, instead of looking + // identical to a file that is still there. + if (alive) markMissing(file.path); } } })(); diff --git a/src/workspaces/officerdev/src/apps/CodeEditor/EditorTabs.tsx b/src/workspaces/officerdev/src/apps/CodeEditor/EditorTabs.tsx index 11f0431e..a547530d 100644 --- a/src/workspaces/officerdev/src/apps/CodeEditor/EditorTabs.tsx +++ b/src/workspaces/officerdev/src/apps/CodeEditor/EditorTabs.tsx @@ -1,5 +1,5 @@ import { Link } from 'react-router'; -import { X } from 'lucide-react'; +import { X, FileX } from 'lucide-react'; import { getIcon } from 'material-file-icons'; import type { OpenFile } from './useEditorState'; @@ -38,8 +38,15 @@ export const EditorTabs = ({ files, activePath, onSelect, onClose, theme, search const label = ( <> - {file.name} - {file.isDirty && } + {/* Struck through when the file is gone from disk. The tab stays open on purpose — the buffer + is the only copy left — and this is what makes that legible instead of looking like an + ordinary tab that happens to have vanished from the tree. */} + {file.name} + {file.missing ? ( + + ) : ( + file.isDirty && + )} ); const labelClass = 'flex items-center gap-1.5 text-sm cursor-pointer whitespace-nowrap'; diff --git a/src/workspaces/officerdev/src/apps/CodeEditor/useEditorState.ts b/src/workspaces/officerdev/src/apps/CodeEditor/useEditorState.ts index ced0b763..27f3b4ea 100644 --- a/src/workspaces/officerdev/src/apps/CodeEditor/useEditorState.ts +++ b/src/workspaces/officerdev/src/apps/CodeEditor/useEditorState.ts @@ -7,6 +7,8 @@ export type OpenFile = { content: string; originalContent: string; isDirty: boolean; + /** Open, but no longer on disk. The buffer is the only copy left — saving puts it back. */ + missing?: boolean; }; /** @@ -85,13 +87,37 @@ export const useEditorState = (urlState = false) => { */ const reloadFile = (path: string, content: string) => { setFiles((prev) => - prev.map((f) => (f.path === path ? { ...f, content, originalContent: content, isDirty: false } : f)), + prev.map((f) => + f.path === path ? { ...f, content, originalContent: content, isDirty: false, missing: false } : f, + ), ); }; + /** + * The file is open but no longer on disk — an agent deleted it, or a task moved it. + * + * The tab deliberately stays: closing it would let an `rm -rf` of a build directory take unsaved work + * with it. But it was indistinguishable from a live file, so the buffer looked authoritative when it + * was the only copy left. + * + * Marked DIRTY as well as missing, and that is the useful half: the content differs from what is on + * disk (which is nothing), so Cmd+S writes it back and the file returns. Dirty also stops the refresh + * loop retrying a read that will keep failing. + */ + const markMissing = (path: string) => { + setFiles((prev) => prev.map((f) => (f.path === path ? { ...f, missing: true, isDirty: true } : f))); + }; + + /** + * Our own write landed. Clears `missing` too: saving a deleted file PUTS IT BACK, so the tab must stop + * saying it is gone — otherwise the one action that fixes the situation leaves the warning on screen + * and reads as having failed. + */ const markSaved = (path: string, content: string) => { setFiles((prev) => - prev.map((f) => (f.path === path ? { ...f, originalContent: content, content, isDirty: false } : f)), + prev.map((f) => + f.path === path ? { ...f, originalContent: content, content, isDirty: false, missing: false } : f, + ), ); }; @@ -105,6 +131,7 @@ export const useEditorState = (urlState = false) => { closeFile, setContent, reloadFile, + markMissing, markSaved, getActiveFile, // Null when this editor does not own the address bar — rows and tabs then stay buttons.