feat(watcher): forward FS events as workspace/didChangeWatchedFiles
LSP servers maintain their own workspace index built at initialize time and rely on the client to push file-system events. Previously the daemon only synced the single file being queried, so externally created/changed files (codegen, build scripts, git checkout, the agent's own writes from the perspective of other open files) left the server's index stale until manual /lsp-destroy. Each ClientEntry now lazily owns a WorkspaceWatcher (chokidar + picomatch) that translates FS events into workspace/didChangeWatchedFiles batches. Patterns come from the server via client/registerCapability (no speculative watching). Ignores layer a tiny baseline (.git, .DS_Store) over the repo's root .gitignore, with a fallback list for non-git workspaces. Events debounce 50ms quiet / 500ms max wait. Notable: gopls registers absolute-path globs (/abs/root/**/*.go) rather than relative ones, so compileWatchers() matches each event against both relative and absolute path forms. Caught by the integration test; unit regression test added. Rollback: PI_LSP_DISABLE_WATCHERS=1 disables all watcher creation. - src/client.ts: honor register/unregisterCapability for workspace/didChangeWatchedFiles; advertise dynamicRegistration; expose getFileWatchers/onWatchersChanged/sendNotification - src/watcher.ts: new WorkspaceWatcher with layered ignores, debounce+batch, Created+Deleted coalescing, dual-form glob matching - src/daemon.ts: per-entry watcher lifecycle, PI_LSP_DISABLE_WATCHERS, LSP_DEBUG-gated pattern/event logging - test/unit/watcher.test.ts: 11 tests against real chokidar + temp dir - test/integration/watcher-gopls.test.ts: end-to-end against gopls - AGENTS.md: new "Workspace File Watching" section - flake.nix: add go (required by gopls integration test)
This commit is contained in:
35
AGENTS.md
35
AGENTS.md
@@ -41,6 +41,38 @@ The daemon tracks opened files per-entry in a `Map<uri, mtimeMs>`. On each reque
|
||||
|
||||
A per-entry `serializer` promise chain prevents concurrent syncs from racing.
|
||||
|
||||
### Workspace File Watching
|
||||
|
||||
Each `ClientEntry` lazily owns a `WorkspaceWatcher` (`src/watcher.ts`,
|
||||
chokidar + picomatch) that translates filesystem events into
|
||||
`workspace/didChangeWatchedFiles` notifications. This keeps the server's
|
||||
workspace index fresh when files are created/changed/deleted **outside** of
|
||||
LSP tool calls (build scripts, codegen, `git checkout`, the agent's own
|
||||
file writes).
|
||||
|
||||
Non-obvious bits:
|
||||
|
||||
- **Patterns come from the server.** We honor `client/registerCapability`
|
||||
for `workspace/didChangeWatchedFiles` and store the registrations on the
|
||||
`LspClient`. **Don't re-stub those handlers**; they look harmless but
|
||||
break the entire feature. If a server doesn't register, we don't watch.
|
||||
- **Servers send mixed pattern forms.** Gopls registers absolute-path
|
||||
globs (`/abs/root/**/*.go`); others send relative (`**/*.ts`) or
|
||||
`RelativePattern` objects. `compileWatchers()` tries both relative and
|
||||
absolute matching against each event so we accept all forms.
|
||||
- **Ignore layering.** Always-ignore baseline (`.git/`, `.DS_Store`) +
|
||||
root `.gitignore` parsed via the `ignore` package + a small fallback
|
||||
for non-git workspaces. Nested gitignores aren't supported yet.
|
||||
- **Debounce.** 50ms quiet period, capped at 500ms max wait so sustained
|
||||
event streams (branch switches) still flush in bounded time.
|
||||
- **Watcher and mtime-sync coexist.** When the agent edits a file we'll
|
||||
emit `didChangeWatchedFiles` *and* the next request's `syncFile` will
|
||||
send a `didChange`. Servers treat the two as orthogonal (workspace
|
||||
index vs. editor buffer) and dedupe internally. This matches VS Code.
|
||||
- **Rollback.** `PI_LSP_DISABLE_WATCHERS=1` short-circuits all watcher
|
||||
creation — if something goes wrong in a real workspace, this restores
|
||||
the prior "only the queried file is synced" behavior.
|
||||
|
||||
### Extension vs Daemon Responsibilities
|
||||
|
||||
| Concern | Where |
|
||||
@@ -60,7 +92,8 @@ cli.ts — CLI for testing/debugging (daemon-aware or --no-daemon)
|
||||
daemon.ts — Entrypoint that starts the daemon process
|
||||
|
||||
src/
|
||||
client.ts — LspClient: spawns a language server, JSON-RPC handshake, file sync
|
||||
client.ts — LspClient: spawns a language server, JSON-RPC handshake, file sync, file-watcher registrations
|
||||
watcher.ts — WorkspaceWatcher: chokidar + picomatch → workspace/didChangeWatchedFiles batches
|
||||
commands.ts — CLI command dispatcher (maps command names → LSP methods)
|
||||
config.ts — Per-repo `.pi-lsp.json` loader: walk-up + merge with built-ins, mtime cache
|
||||
daemonClient.ts — High-level helpers (daemonRequest, daemonDiagnostics, etc.)
|
||||
|
||||
Reference in New Issue
Block a user