Repository navigation
feat(core): watch files open in the side panel - #57915
Conversation
There was a problem hiding this comment.
The server side works for the main case. In core, the new tests pass on Linux, including the real native test (in-place edit and temp + rename save). The server route tests and type checks for core, server, protocol and app also pass. I didn't run the Desktop or web app, so the files/model.tsx behaviour is only checked by reading the code.
Public API: additions only, so backwards compatible.
POST /api/experimental/fs/watchandDELETE /api/experimental/fs/watch(location query, body{ path }).POSTreturns 204, 404 or 503;DELETEreturns 204.- In
openapi.json:experimental.fs.watchandexperimental.fs.unwatch. - In the client:
file.watchandfile.unwatch. filesystem.changednow fires for watched files. The event's shape is unchanged.
One thing to fix: a watch whose stream has ended stays registered, so asking again only resets its expiry and never restarts it. Details are in the inline comment.
| ), | ||
| ) | ||
| }).pipe( | ||
| Effect.catchCauseIf( |
There was a problem hiding this comment.
When this stream ends, the entry stays in locations. This happens if the parent directory is removed (for example rm -rf src && mkdir src, a branch switch that drops the folder, or a build that wipes dist/), or if fs.watch hits ENOENT and the new Watcher path ends the stream. A later watch() then hits the existing branch (line 127) and only bumps expires, so the tab stays stale for up to 24h even though the client keeps asking.
I reproduced this with the native layer: watch src/a.ts, recreate src, call watch() again, write 10 times, and no events arrive.
One fix: drop the entry when the forked fiber exits without being interrupted, so the next request starts a fresh watch.
3407759 to
6a92cb2
Compare
There was a problem hiding this comment.
The new commits fix part of my earlier point. A file watch that ends or fails is now dropped and restarts on the next request, which covers a directory missing at start and watch errors.
The directory-recreated case I reported still fails on Linux. My earlier reproduction (recreate src/, watch again, write 10 times) still gets 0 events on this head. When the watched directory is deleted, fs.watch sends no error or close, only a rename event named after the directory itself. The native file watch in watcher.ts drops that event because the name isn't on its list, so the stream never ends and the new restart code never runs. Ending the subscription when an event names the watched directory, or when the directory is gone, would let the new restart code handle it.
The app side, which watches on sync and unwatches on tab close, reads correctly. Two small side effects worth knowing about:
- A link click syncs before its tab exists, so that watch can stay unclaimed until it expires after 24h.
- Closing a tab in one session stops the watch for another session open on the same directory, until that tab is selected again.
Tests: the core watch tests pass. typecheck passes in core, server and gui-extensions. The app type check has one error, in activity-chips.fixture.tsx, a file this PR doesn't touch. I didn't run the Desktop or web app.
Public API (backwards compatible):
- HTTP: experimental
POSTandDELETE /api/experimental/fs/watch(experimental.fs.watch/experimental.fs.unwatchin the OpenAPI spec). - Client:
file.watchandfile.unwatch. - Extension SDK: adds
Files.unwatch(path), andFiles.syncnow also starts a server watch.
There was a problem hiding this comment.
The two new commits change only tests. The promise client test now expects watch and unwatch, and the e2e mock server handles the new watch endpoints. bun test test/promise.test.ts passes. The public API is unchanged by these commits.
The open thread about the directory-recreate case isn't addressed by these commits.
There was a problem hiding this comment.
The new commit only regenerates the docs site's OpenAPI documents. They match packages/protocol/openapi.json, and the only addition is the experimental /api/experimental/fs/watch endpoints. The public API is unchanged by this commit.
The open thread about the directory-recreate case isn't addressed by this commit.
Issue for this PR
Closes #
Replaces #57426 (polling). Design: https://gist.github.com/jlongster/80526202aca5784bd3fbac9e554296f8
Type of change
What does this PR do?
Files open in the right sidebar go stale since #41096 stopped recursive project watching, so
filesystem.changednever fires for workspace files. Now the client asks the server to watch one file at a time.FileWatch, a global registry keyed by location + absolute path. It runs oneWatchertype: "file"watch per file (the parent directory, filtered by name, so atomic temp + rename saves are caught) and publishesfilesystem.changed(change, orunlinkwhen the file is gone) on the watch's location. Each watch expires 24h after the latest request; one sweep runs every 5 minutes; at most 256 watches per location, evicting the one closest to expiring. It's global so watches outlive idle location eviction. It follows thefs.filewatcherkill switch.fs.watchon a missing directory threw insideEffect.sync(a defect). It now logs and ends the stream.POST/DELETE /api/experimental/fs/watch { path }, location-scoped.POSTuses the same confinement asfs.read(404 otherwise) and returns 503 when watching is disabled. Client regenerated (file.watch,file.unwatch).sync(path)(called when a file tab opens and when a tab is selected, via the panelfocus()hook) also sendswatch. The file panel'sclose()hook sendsunwatch, so it only fires on a real tab close, not on session switches; this addsFiles.unwatchto the extension SDK. On everyserver.connected, the files open in the side panel are watched again. Errors are ignored (older servers return 404). The existingfilesystem.changedreload path is unchanged.Known limits: files outside the workspace aren't watched (their events would arrive on another location, which the file model doesn't listen to). A
DELETEfrom one window stops a watch another window still shows, until that tab is selected again or the stream reconnects.How did you verify your code works?
packages/core/test/filesystem/file-watch.test.ts: sharing + unwatch, 24h expiry and reset, the per-location cap, events on the right location, the disabled path, and a real native test (in-place edit and temp + rename save, macOS locally, skipped on CI like the other native watcher tests).packages/server/test/fs.test.ts: 404 outside the location or for a missing file, 503 when disabled, 204 forDELETE.bun typecheckin core, server, protocol, app, gui-extensions.Screenshots / recordings
Not run in the Desktop app.
Checklist
Added by the opencode bot (AI-generated).
Related: #52941, #57920
Public API: adds experimental POST/DELETE /api/experimental/fs/watch, client file.watch/file.unwatch, and extension SDK Files.unwatch; Files.sync now also watches (backwards compatible)