From 20aa0cc880bb031898c377b684b36ba4c5f10589 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sat, 15 Aug 2026 23:56:41 +0000 Subject: [PATCH] a deleted file stays open, says so, and can be saved back MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keeping the tab when an agent deletes the file was deliberate — closing it would let an `rm -rf` of a build directory take unsaved work with it — but it was only half the idea, and the missing half made it worse than useless: the tab looked identical to a live file, and you could not SAVE it, because the editor thought nothing had changed So the buffer was the only copy left, and the one action that would rescue it was locked until you typed a throwaway character to force the dirty flag. Reported from a real session. VS Code's answer, and the right one: a file that has left the disk is DIRTY, because the buffer now differs from what is there (nothing). markMissing missing: true + isDirty: true — save works immediately the tab name struck through, amber icon instead of the dirty dot markSaved clears `missing`, because saving PUT IT BACK — leaving the warning up would make the fix look like a failure reloadFile clears it too, for a file that reappears from elsewhere That makes "save recreates the file" an informed recovery rather than the accident it was: the tab says the file is gone before you press it. Not a bug, and checked rather than assumed: the report that a tab showed "my previous content on top of the agent's" was the file genuinely containing both. On disk it reads `- Sidecasadasrs assumed up` — `Sidecars` with `asadas` typed into the middle of it. The buffer had been saved over the deleted file, the agent then edited that file, and the editor was showing the truth. tsgo clean, frontend builds, 846 pass / 7 fail unchanged. Co-Authored-By: Claude Opus 5 --- .../src/apps/CodeEditor/CodeEditor.tsx | 9 ++++-- .../src/apps/CodeEditor/EditorTabs.tsx | 13 ++++++-- .../src/apps/CodeEditor/useEditorState.ts | 31 +++++++++++++++++-- 3 files changed, 45 insertions(+), 8 deletions(-) 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.