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.