Skip to content

Stop remote streaming and chip switches from freezing the app - #357

Merged
frenchie4111 merged 4 commits into
mainfrom
fix-remote-streaming-freeze
Nov 3, 2026
Merged

frenchie4111 merged 4 commits into
mainfrom
fix-remote-streaming-freeze

Conversation

@frenchie4111

Copy link
Copy Markdown
Collaborator

Second 2.18.1 hotfix, following #356. The Cleanup flood is fixed, but a remote chat streaming still froze the app with a beach ball, at its worst when switching back to a local worktree.

Evidence (local perf.log / debug.log during the repro)

  • Local main stalled: eventloop-spike 4847 ms and 1710 ms. Renderer input took 1.8–5.4 s to paint with zero renderer long tasks, so the renderer itself wasn't busy.
  • Just before the stall: 58 worktree:listFiles calls in one second. This machine has 64 local chat tabs across 38 worktrees. Each chat tab lists files on mount with no in-flight dedupe, so nearly every local chat had mounted at the same moment.
  • The remote was healthy the whole time (0.1% CPU, lag 0–2 ms).

Causes and fixes

  1. The keep-everything-mounted loop rendered from the view-filtered list. In single-host mode, a chip switch unmounted every other host's worktrees, and switching back remounted them all at once. The loop now runs over the whole fleet (useFleetWorktrees), and visibility is keyed on (backendId, path). getMergedWorktrees keeps one cache per view mode, so reading both views on each render doesn't evict either one.
  2. App re-rendered on every streamed token from any host. It subscribed to every backend's whole AppState, both directly (the open-worktree prune) and through useMergedWorktrees. It now subscribes through cached projections via useAllBackendStatesSelector: the merge, and getFleetWorktreeLists. useSyncExternalStore skips the render when nothing it reads changed.
  3. Chat tabs dedupe in-flight file listings per worktree, so many chats mounting together make one call.

Tests

New cases in merge-backend-states.test.ts:

  • A jsonClaude-only change leaves the merge result identical, in both modes.
  • The two view-mode caches don't churn each other.
  • The fleet view keeps a hidden backend's worktrees.
  • getFleetWorktreeLists stays stable until some backend's list changes.

Typecheck, build and the full vitest suite pass. There is no React render test harness in the repo, so the "no re-render per token" claim is pinned at the projection layer. It has not been verified end to end yet; that needs a manual repro with a build from this branch.

🤖 Generated with Claude Code

frenchie4111 and others added 4 commits November 3, 2026 11:07
Two 2.18 fleet-view regressions behind the beach ball users see while a
remote chat streams:

- App subscribed to every backend's whole AppState (directly and through
  useMergedWorktrees), so each streamed token on any host re-rendered
  the root. It now subscribes through cached projections — the merge,
  and a per-backend worktree-list selector — and useSyncExternalStore
  bails when they're unchanged.
- The keep-everything-mounted loop rendered from the view-filtered list,
  so in single mode a chip switch unmounted every other host's worktrees
  and the return trip remounted them all. Switching back to Local
  remounted 38 worktrees / 64 chat tabs at once; 58 file listings in one
  second froze local main for ~5s. The loop now spans the whole fleet,
  with one merge cache per view mode so both stay reference-stable.

Chat tabs also dedupe their in-flight file listing per worktree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Move createSingleFlight to src/shared so the renderer reuses the tested
  helper instead of a second untested copy.
- The chat @-mention file cache and its in-flight dedupe are now per
  backend. With the whole fleet mounted, two hosts with the same paths
  would otherwise have shared one listing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Profiled during a repro: local main blocked 8.5s, 8.4s of it in
child_process.spawn under runGitRead. perf.log shows those reads were
changed-files / branch-commits for /home/mike/personal-assistant-agentjesus,
a worktree on the remote, run by the LOCAL backend. On macOS /home is an
autofs mount, so each spawn there sits ~1s on the main thread while the
automounter gives up. The same lines appear in every incident back to
the first report.

Cause: App resolved a worktree's owner from the view-filtered merge.
Switching the chip to Local drops the remote worktree from that map, so
selectedBackendId fell back to the active backend and the right column
(changed files, commits, watcher) queried the Mac for a remote path.
Every App routing map now comes from the fleet-wide merge — who owns a
path doesn't depend on what the sidebar shows.

Main also refuses worktree reads for paths it doesn't host (string check
against its worktrees, repo roots and pane keys — no filesystem access),
so any future misroute fails fast instead of freezing the app.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Register worktree-path handlers through onWorktreeRequest /
onWorktreeSignal (backed by a tested ownedPathsOnly wrapper) instead of
inline checks, and extend coverage to the ones that were missing: file
diffs, binary reads, writes, the per-file watcher, tools, editor:open,
and the agent session lookups and spawn-args (which probe a login shell
in that directory). Store-only handlers are left alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@frenchie4111
frenchie4111 merged commit c5af2c1 into main Nov 3, 2026
1 check passed
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.

1 participant