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:
2026-05-19 23:43:32 -04:00
parent e143e05758
commit 77876264ee
9 changed files with 869 additions and 5 deletions

View File

@@ -21,12 +21,16 @@
"test:integration": "NODE_OPTIONS='--import=tsx' node --test test/integration/**/*.ts"
},
"dependencies": {
"chokidar": "^5.0.0",
"ignore": "^7.0.5",
"picomatch": "^4.0.4",
"vscode-jsonrpc": "^8.2.1",
"vscode-languageserver-protocol": "^3.17.5"
},
"devDependencies": {
"@mariozechner/pi-coding-agent": "^0.72.0",
"@types/node": "^22.10.0",
"@types/picomatch": "^4.0.3",
"oxlint": "^1.62.0",
"tsx": "^4.19.2",
"typebox": "^1.1.37",