fix(web): editor picks up external file changes and warns on conflict
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.)
This commit is contained in:
@@ -20,12 +20,34 @@ export function EditorPanel({ workspaceId, path, language }: EditorPanelProps) {
|
||||
const saveInProgress = write.isPending;
|
||||
const saveTimeoutRef = useRef<number | undefined>(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…
|
||||
</div>
|
||||
)}
|
||||
{externalChange && (
|
||||
<div className="flex shrink-0 items-center justify-between gap-2 border-b border-amber-500/30 bg-amber-500/10 px-3 py-1.5 text-xs text-amber-700 dark:text-amber-300">
|
||||
<span>File changed on disk.</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={reloadFromDisk}
|
||||
className="rounded border border-amber-500/40 px-2 py-0.5 hover:bg-amber-500/20"
|
||||
>
|
||||
Reload
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="min-h-0 flex-1">
|
||||
<Editor
|
||||
key={path}
|
||||
|
||||
@@ -87,6 +87,10 @@ export function useFileRead(
|
||||
queryKey: fileReadQueryKey(workspaceId ?? "", path ?? ""),
|
||||
queryFn: () => 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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user