Skip to content

(main): release the viewer-panel file watchers when the window closes - #308

Merged
jbr-sekoia merged 1 commit into
mainfrom
fix/file-watchers-released-on-close
Sep 18, 2026
Merged

jbr-sekoia merged 1 commit into
mainfrom
fix/file-watchers-released-on-close

Conversation

@jbr-sekoia

@jbr-sekoia jbr-sekoia commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator

The defect

The window-closed handler (main.js) releases the PTYs in activeSessions,
calls changesWatchers.closeAll() and drains subagentWatchers. It does not
touch fileWatchers, the registry behind watch-file / unwatch-file.

That registry is released only by an explicit unwatch-file, and a closing
window never sends one: on macOS the app survives the last window, so every
file a ViewerPanel had open keeps its fs.watch descriptor for the life of
the process, along with the debounce timer armed from inside it.

The fix

A closeAll-style helper next to the map, and one call in the closed
handler beside the two registries already drained there. The helper swallows
a close() that throws, which an fs.watch handle whose file is already gone
can do — matching git-changes-watch.js's disarm() rather than inventing a second
convention. 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 is
being 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-file behaviour. The issue stays open.

How it is pinned

test/window-close-releases-watchers.test.js:

  • a source-text assertion that the closed handler names all three
    registries — the house pattern for main.js handlers
    (test/read-file-for-panel-bounds.test.js states it, and
    test/git-changes-watch.test.js already pins the sibling's closeAll the
    same way);
  • the helper lifted out of the source and exercised: every watcher closed and
    the map emptied, and a watcher whose close() throws leaving the rest of
    the drain intact.

Mutation run: removing the closeAllFileWatchers() call from the closed
handler turns the file red at 1 fail / 4 tests; restoring it returns 4 pass.

Checks

  • npx eslint . — 0 errors, 333 warnings (unchanged from c939748).
  • npm test — batch 1 tests 1905 / pass 1902 / fail 1 / skipped 2, batch 2
    tests 120 / pass 119 / fail 0 / skipped 1. The single failure is the
    pre-existing environmental one, test/ipc-path-validator.test.js
    "allows files under ~/.claude/".

Refs #301.

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 devsuitup left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@jbr-sekoia
jbr-sekoia merged commit 3b1c2c0 into main Sep 18, 2026
10 checks passed
@jbr-sekoia
jbr-sekoia deleted the fix/file-watchers-released-on-close branch September 18, 2026 16:57
@jbr-sekoia jbr-sekoia mentioned this pull request Sep 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants