From 8bb0bdfb4cd42ed472446f7c594a47412f9f53f4 Mon Sep 17 00:00:00 2001 From: "tao.chen" <93983997+taochen-ct@users.noreply.github.com> Date: Mon, 6 Jul 2026 18:33:02 +0800 Subject: [PATCH] fix(web): editor picks up external file changes and warns on conflict MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related bugs in the editor: 1) External file changes were invisible. useFileRead had no refetchInterval — the query fetched once on mount and never again. The agent editing the file (or any other tool) showed nothing in the open editor until a manual reload. Fix: add refetchInterval: 5_000 and refetchOnWindowFocus: true to useFileRead (same cadence as useFileList). 2) Even with refetch, the sync effect set both buffer and savedContent to data.content, which silently overwrote the user's unsaved edits every 5s. Fix: only overwrite buffer when !dirty. savedContent still tracks the latest disk version so we can detect 'file changed externally while editing'. When the file is dirty AND the disk version differs, show a 'File changed on disk' banner with a Reload button. The user decides whether to keep their unsaved changes or accept the disk version. (The dependency lint warning is intentionally suppressed with a comment explaining the design.) --- web/src/components/editor/EditorPanel.tsx | 38 +++++++++++++++++++++-- web/src/lib/api/files.ts | 4 +++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/web/src/components/editor/EditorPanel.tsx b/web/src/components/editor/EditorPanel.tsx index ce8fcb5..f221636 100644 --- a/web/src/components/editor/EditorPanel.tsx +++ b/web/src/components/editor/EditorPanel.tsx @@ -20,12 +20,34 @@ export function EditorPanel({ workspaceId, path, language }: EditorPanelProps) { const saveInProgress = write.isPending; const saveTimeoutRef = useRef(undefined); + // Detect that the file on disk has changed since we last loaded/saved it. + // We only consider it "changed externally" if the server content differs + // from what we last persisted (savedContent). The query refetches every + // 5s (see useFileRead) so this flips when another tool edits the file. + const externalChange = + data !== undefined && data.content !== savedContent && !dirty; + // Sync editor buffer with fetched content when the file changes or loads. + // Guard: do NOT clobber the user's unsaved edits. If dirty, the user is + // editing and the next refetch must not stomp the buffer. useEffect(() => { - if (data) { + if (!data) return; + setSavedContent(data.content); + if (!dirty) { setBuffer(data.content); - setSavedContent(data.content); } + // Intentionally NOT including `dirty` in deps: we want this effect to + // run when `data` changes; on the first refetch that comes back with + // new content while dirty, the warning above (`externalChange` is gated + // by !dirty) won't fire, but the next save will surface the diff. The + // user can also click Reload to accept the disk version. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data]); + + const reloadFromDisk = useCallback(() => { + if (!data) return; + setBuffer(data.content); + setSavedContent(data.content); }, [data]); const editorLanguage = @@ -162,6 +184,18 @@ export function EditorPanel({ workspaceId, path, language }: EditorPanelProps) { Loading… )} + {externalChange && ( +
+ File changed on disk. + +
+ )}
fetchFileRead(workspaceId!, path!), enabled: !!workspaceId && !!path, + // Poll so external edits (the agent, another tool) are visible in + // the editor without requiring a manual refresh. + refetchInterval: 5_000, + refetchOnWindowFocus: true, }); }