fix(files): stop file viewer reload loop from sub-ms mtime jitter (#1489) (#2297)

* fix(files): guard file polling races

- Ignore sub-millisecond mtime jitter on a same-size file so an unchanged
  open file no longer loops through reload and flickers.
- Swap externally changed text content into the open editor in place
  instead of clearing the loaded path and showing the load spinner.
- Read content only after metadata changed, confirm it with a second
  read, and skip the swap when the file is unchanged, the buffer is
  dirty, or a newer local write landed.
- Keep the stat baseline unchanged when a poll cannot observe content so
  a failed read is retried rather than treated as unchanged.
- Fall back to a full reload for images, PDFs, binaries, and files above
  the content-poll byte limit, and when a poll returns binary content.
- Serialize polls and dispose the poller on unmount, file switch, and
  directory change.
- Add a `fresh` file read option that bypasses the content cache and the
  HTTP cache.

* fix(files): invalidate stale polls after diagram saves

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
Ibrahim Khan
2026-09-05 18:31:35 +03:00
committed by GitHub
co-authored by Bohdan Triapitsyn
parent a005215458
commit 7ea24e3c50
10 changed files with 397 additions and 31 deletions
+9
View File
@@ -77,6 +77,15 @@ describe('createWebFilesAPI', () => {
cache: 'default',
headers: { 'x-opencode-directory': '/worktree-a' },
});
runtimeFetchMock.mockResolvedValueOnce(new Response('fresh content'));
await api.readFile?.('/worktree-b/file.txt', { directory: '/worktree-a', fresh: true });
expect(runtimeFetchMock).toHaveBeenLastCalledWith('/api/fs/read', {
query: new URLSearchParams({ path: '/worktree-b/file.txt' }),
cache: 'no-store',
headers: { 'x-opencode-directory': '/worktree-a' },
});
});
it('sends the workspace directory header for downloads', async () => {
+1 -1
View File
@@ -197,7 +197,7 @@ export const createWebFilesAPI = ({ getDirectory }: WebFilesAPIOptions): FilesAP
}
const response = await runtimeFetch('/api/fs/read', {
query: params,
cache: options?.optional ? 'no-store' : 'default',
cache: options?.optional || options?.fresh ? 'no-store' : 'default',
headers: directoryHeaders(getDirectory, options?.directory),
});