fix(files): refresh open file content after external changes (#967)
* fix(files): refresh open file content after external edits Previously, opening a file in the Files view and then editing it externally (e.g. via CLI or another editor) would show stale content. Even closing and reopening the file returned cached content — a full page reload was required. Root causes: 1. The in-memory readFile cache used path-only hits, with no metadata validation. External edits were invisible until the cache was evicted. 2. No polling mechanism existed to detect external changes to the open file. Fix: - Add mtimeMs to statFile across all runtimes (web, VS Code, desktop). - Cache layer (RuntimeAPIProvider): validate cache hits against current stat metadata (mtimeMs + size). On miss, use stat→read→stat to avoid TOCTOU. - UI layer (FilesView): poll the open file every 2s; on detected change, set loadedFilePath=null to trigger the existing load effect once (no double reload). Skip polling when tab is hidden or editor has unsaved changes. - After save, refresh the stat ref so the next poll doesn't see a spurious change from the save itself. Addresses review feedback from PR #827 (double reload + TOCTOU). * fix(files): address P2 review findings - readFreshFile retry now uses stat→read→stat to maintain TOCTOU protection during the retry path (not just the initial read). - Replace isDirty in polling effect deps with isDirtyRef to avoid unnecessary interval teardown/restart on every edit/save cycle.
This commit is contained in:
@@ -110,7 +110,7 @@ export const createWebFilesAPI = (): FilesAPI => ({
|
||||
};
|
||||
},
|
||||
|
||||
async statFile(path: string): Promise<{ path: string; isFile: boolean; size: number }> {
|
||||
async statFile(path: string): Promise<{ path: string; isFile: boolean; size: number; mtimeMs?: number }> {
|
||||
const target = normalizePath(path);
|
||||
const response = await fetch(`/api/fs/stat?path=${encodeURIComponent(target)}`);
|
||||
|
||||
@@ -124,6 +124,7 @@ export const createWebFilesAPI = (): FilesAPI => ({
|
||||
path: typeof (result as { path?: string }).path === 'string' ? normalizePath((result as { path: string }).path) : target,
|
||||
isFile: Boolean((result as { isFile?: boolean }).isFile),
|
||||
size: typeof (result as { size?: number }).size === 'number' ? (result as { size: number }).size : 0,
|
||||
mtimeMs: typeof (result as { mtimeMs?: number }).mtimeMs === 'number' ? (result as { mtimeMs: number }).mtimeMs : undefined,
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user