(main): release the viewer-panel file watchers when the window closes - #308
Conversation
The window-`closed` handler drains the PTYs, the Changes watches and the subagent watches, but `fileWatchers` was released only by an explicit `unwatch-file`, which a closing window never sends. Every file a ViewerPanel had open kept its fs.watch descriptor for the life of the process. Drain the registry alongside the other two, swallowing a close() that throws so a handle whose file is already gone cannot strand the rest of the teardown. Refs #301 — whether one registry could serve all three callers is left open.
devsuitup
left a comment
There was a problem hiding this comment.
Reviewed 9281fca. closeAllFileWatchers() sits beside the map, is called between changesWatchers.closeAll() and the subagentWatchers drain, and its per-watcher try/catch means a throwing close() cannot strand the steps after it; fileWatchers is module-level and emptied, so a macOS activate starts clean. The source-text assertions read the real main.js; the behavioural tests re-evaluate a regex-sliced copy of the helper — the house pattern, sound at this shape. CI green, no trailers.
One gap for parity with the sibling: the watch-file handler's debounce is a let closed over inside the handler (main.js:1010-1018) and nothing in closeAllFileWatchers can clear it. Harmless today — the callback checks mainWindow && !mainWindow.isDestroyed() and mainWindow is nulled synchronously in the same handler — but subagentWatchers' teardown explicitly clears its timers, and this "release" does not. Store the handle with the watcher and clearTimeout it in the drain; one test line.
Approving; the timer is a follow-up-sized change, before or after merge as you prefer.
The defect
The window-
closedhandler (main.js) releases the PTYs inactiveSessions,calls
changesWatchers.closeAll()and drainssubagentWatchers. It does nottouch
fileWatchers, the registry behindwatch-file/unwatch-file.That registry is released only by an explicit
unwatch-file, and a closingwindow never sends one: on macOS the app survives the last window, so every
file a ViewerPanel had open keeps its
fs.watchdescriptor for the life ofthe process, along with the debounce timer armed from inside it.
The fix
A
closeAll-style helper next to the map, and one call in theclosedhandler beside the two registries already drained there. The helper swallows
a
close()that throws, which anfs.watchhandle whose file is already gonecan do — matching
git-changes-watch.js'sdisarm()rather than inventing a secondconvention. A throw here would strand the subagent teardown that follows.
Deliberately out of scope
#301 also asks whether a single registry could serve all three callers
(
fileWatchers,changesWatchers,subagentWatchers). That unification isbeing folded into a larger piece and is not touched here: this PR changes
nothing about what any watcher does while armed, and nothing about
watch-file/unwatch-filebehaviour. The issue stays open.How it is pinned
test/window-close-releases-watchers.test.js:closedhandler names all threeregistries — the house pattern for
main.jshandlers(
test/read-file-for-panel-bounds.test.jsstates it, andtest/git-changes-watch.test.jsalready pins the sibling'scloseAllthesame way);
the map emptied, and a watcher whose
close()throws leaving the rest ofthe drain intact.
Mutation run: removing the
closeAllFileWatchers()call from theclosedhandler turns the file red at 1 fail / 4 tests; restoring it returns 4 pass.
Checks
npx eslint .— 0 errors, 333 warnings (unchanged fromc939748).npm test— batch 1tests 1905 / pass 1902 / fail 1 / skipped 2, batch 2tests 120 / pass 119 / fail 0 / skipped 1. The single failure is thepre-existing environmental one,
test/ipc-path-validator.test.js"allows files under ~/.claude/".
Refs #301.