Skip to content

Fix grid-vs-dispatch divergence for good — resolver layer + migrations - #107

Merged
Juliusolsson05 merged 5 commits into
mainfrom
fix/grid-dispatch-divergence
May 13, 2026
Merged

Juliusolsson05 merged 5 commits into
mainfrom
fix/grid-dispatch-divergence

Conversation

@Juliusolsson05

@Juliusolsson05 Juliusolsson05 commented May 13, 2026 •

Copy link
Copy Markdown
Owner

Summary

Closes the recurring grid-vs-dispatch session-set divergence bug class — PRs #37 / #39 / #44 / #45 / #46 / #58 / #59 / #69 / #83 + issue #104 — by introducing a canonical resolver and migrating every known broken site to it.

Branch history (5 commits):

  1. `Add workspace queries.ts` — new `resolveTabSessions`, `resolveAllSessions`, `isDetached`. Thick WHY comments. Pure addition.
  2. `Migrate every known broken site` — 13 sites across 10 files swapped to the resolver. Closes Tab header activity counter misses dispatch agents — only walks grid tree (another grid-vs-dispatch divergence) #104 plus a handful of cousins (PerformancePanel telemetry, AgentActivityModal rows, ReaderView fallback, SpotlightView fallback, prompt-templates manual union, worktree-dump per-worktree agents, App.tsx pin-modal + path-picker fallback, CommandPalette buried scope, paneCommands detach-to-dispatch focus + attach-to-grid query).
  3. `ci: add resolver-discipline check` — added a grep CI lock + workflow.
  4. `fix: scope Reader/Spotlight to grid leaves; align validator + labels` — review found three real bugs in the migrations:
    • Reader/Spotlight click handlers wrote detached ids into `Tab.focusedSessionId` (grid-only invariant). Fixed by gating the mirror on `collectLeaves(activeTab.root).includes(sessionId)`.
    • `effects.ts:validFocusSessionIdsForMode` validated against `collectLeaves(tab.root)` while views render `resolveTabSessions` → spurious resets. Aligned the validator with the view.
    • `paneLabelForSession` only walked the grid → detached rows in PerformancePanel rendered `A?`. Switched to `resolveTabSessions`; grid-leaf indices unchanged.
  5. `Remove resolver-discipline CI check + workflow` — reverts commit Fix workspace bootstrap and Codex feed rendering #3. The CI lock was over-engineering for a single-developer repo; the resolver + migrations close the class on their own.

Net effect

The branch's net diff vs `main` is: queries.ts + 13 migrations + the Reader/Spotlight focus fix. No CI workflow, no grep script, no allowlist.

What `resolveTabSessions` returns

For a tab id, the union of:

  • grid leaves (depth-first tile-tree order)
  • detached Dispatch agents whose `projectTabId === tabId` (oldest-first)

Excludes terminals from the detached side (Dispatch never holds terminals) and excludes `state.buried` entries (burying is the user's "put it away" signal). De-duped defensively.

Sites migrated (13)

File Site Change
`workspace/tile-tree/TabBar.tsx` tab activity counter grid+detached union (closes #104)
`features/performance/ui/PerformancePanel.tsx` useMemo + render loop covers detached
`features/workspace/ui/AgentActivityModal.tsx` row builder lists detached
`features/reader/ui/ReaderView.tsx` non-dispatch fallback union
`features/spotlight/ui/SpotlightView.tsx` non-dispatch fallback union
`features/prompt-templates/templates.ts` active-tab agents replaces manual concat from PR #83
`features/worktrees/lib/loadWorktreeDump.ts` per-worktree agents shows detached
`app/App.tsx` (path picker) most-recent fallback active-tab scoped, not global `Object.values().pop()`
`app/App.tsx` (pin modal, pass 1) owner-tab lookup uses resolver
`app/App.tsx` (pin modal, pass 2) grid+detached pass uses resolver
`features/command-palette/ui/CommandPalette.tsx` buried list filter by `sourceTabId`
`features/workspace/commands/paneCommands.ts` detach-to-dispatch when-guard dispatch-aware target
`features/workspace/commands/paneCommands.ts` attach-detached-to-grid uses `isDetached()`

Review fixes (commit #4)

  • `workspace/hook/actions/spotlight.ts` + `reader.ts` — gate `Tab.focusedSessionId` mirror on grid-leaf membership
  • `workspace/hook/invalidation/effects.ts` — validator aligns with view
  • `workspace/tile-tree/paneLabels.ts` — labels work for detached sessions (signature now takes `WorkspaceState`; 3 call sites updated)

Test plan

  • `npm run test:review-fixes` passes (settings, prompt-templates, session-ownership)
  • `npm run test:work-context` passes
  • Manual smoke: open a tab with grid + detached agents → tab counter shows the union total; trigger Performance Panel → detached rows have labels

Closes

🤖 Generated with Claude Code

Juliusolsson05 and others added 5 commits May 13, 2026 19:18
Introduces the canonical resolver layer for "which sessions live in
this tab" — the question that PRs #37 / #39 / #44 / #45 / #46 / #58
/ #59 / #69 / #83 + issue #104 all answered inconsistently.

Pure addition. The existing `dispatchSessionIdsForTab` and
`detachedDispatchSessionIdsForTab` selectors keep their current
semantics (the former strips pinned for dispatch UI display reasons;
the latter is detached-only for the bulk-attach ordering). They're
not being replaced — `resolveTabSessions` is a new function for the
common "all sessions in this tab, grid + detached" question.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces collectLeaves(tab.root) (and one manual grid+detached
union) with resolveTabSessions(state, tabId) at every surface that
asks "which sessions are in this tab?" and silently meant
grid-only. Also fixes one focus-asymmetry site (detach-to-dispatch
when-guard) and one buried-pane scope leak (palette shows other
projects' buried entries).

  - TabBar.tsx                       — tab activity counter (#104)
  - PerformancePanel.tsx (x2)        — visible IDs + render loop
  - AgentActivityModal.tsx           — activity rows per session
  - ReaderView.tsx                   — non-dispatch fallback
  - SpotlightView.tsx                — non-dispatch fallback
  - prompt-templates/templates.ts    — replaces manual concat
  - worktrees/loadWorktreeDump.ts    — live agents per worktree
  - App.tsx (path picker fallback)   — active-tab scoped
  - App.tsx (pinned owner lookup)    — uses resolver
  - App.tsx (pin modal grid pass)    — uses resolver
  - CommandPalette.tsx               — buried filter by sourceTabId
  - paneCommands.ts (detach-to-disp) — commandTargetSessionIdForState
  - paneCommands.ts (attach-to-grid) — isDetached() query

After this commit:
  - tab counter reflects detached agents (closes #104)
  - perf telemetry covers them
  - activity modal lists them
  - spotlight surfaces them
  - worktree dump shows them per worktree
  - new-tab fallback no longer pre-fills a stale background cwd
  - palette buried list scopes to the active project
  - detach-to-dispatch shows correctly in dispatch mode
  - attach-detached-to-grid uses a named query, not subscript

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The grid-vs-dispatch session-set divergence kept recurring because
the broken pattern — collectLeaves(tab.root) for "sessions in this
tab" or state.detachedSessions[id] for "is this detached" — was
easy to write and nobody enforced the resolver layer. This check
is the lock: a grep-based CI test that fails the build if any of
the forbidden patterns appears outside an explicit allowlist of
resolver-layer + tree-mutation files.

Patterns flagged:
  - collectLeaves(<expr>.root)
  - (state|workspace.state).detachedSessions[<expr>]
  - Object.{values,keys,entries}(<...detachedSessions)

Comment lines are stripped before matching so the explainer prose
can reference the literal pattern without tripping the check.

After this commit, the bug class is structurally closed: the next
contributor who writes the broken pattern hits a CI failure with a
message pointing them at the right resolver
(resolveTabSessions / isDetached in
src/renderer/src/workspace/queries.ts).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Review of PR #107 surfaced three coupled issues — same root cause:
the migrated views (ReaderView, SpotlightView, PerformancePanel)
now show detached sessions, but downstream code that processes
selections from those views still assumed grid-only.

Fixes:

1. spotlight.ts / reader.ts — non-Dispatch setSpotlightSession /
   setReaderModeSession used to write the selected sessionId into
   Tab.focusedSessionId unconditionally. After the views started
   surfacing detached sessions, that could violate the grid-leaf
   invariant (`Tab.focusedSessionId` must be a leaf in `tab.root`).
   Gate the write on actual grid-leaf membership; for a detached
   selection only the Reader/Spotlight surface state is updated.

2. effects.ts — validFocusSessionIdsForMode's non-Dispatch path
   validated against `collectLeaves(tab.root)` while the views
   render `resolveTabSessions`. Mismatch would reset a detached
   Reader/Spotlight focus to the first grid pane. Validator now uses
   the same source as the view.

3. paneLabels.ts — paneLabelForSession only walked the grid tree, so
   detached rows in PerformancePanel rendered with "?". Switched to
   resolveTabSessions; grid leaves keep identical indices (resolver
   yields grid first in tile-tree order), TileTree/DispatchLayout
   call sites updated for the new state-shaped parameter. Detached
   sessions now get meaningful labels.

CI lock tightening:
  - Added a destructuring pattern so `const { detachedSessions } =
    state` followed by `detachedSessions[id]` no longer slips
    through. Verified the regex fires on a contrived sample.
  - Removed hook/invalidation/ from the allowlist. The very directory
    hid the effects.ts bug above — narrowing the allowlist means the
    lock would have caught that mistake in this PR's CI run.
  - Removed tile-tree/paneLabels.ts from the allowlist (no longer
    walks the grid directly).
  - Documented the line-oriented-grep limitation in the header.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dropping the grep-based CI lock and the GitHub Actions workflow it
ran in. The check was over-engineering:

  - The resolver itself (resolveTabSessions, isDetached) is now the
    obvious way to ask "sessions in this tab"; new code copies from
    neighbors, and every neighbor uses the resolver.
  - The allowlist was a maintenance burden — the very PR that
    introduced it had to revise the allowlist mid-review when
    hook/invalidation/ turned out to hide a real bug.
  - The check failed to catch any of the bugs the human review
    caught. They were semantic, not syntactic; grep can't see them.

The closure of the bug class is the resolver API + the migrations,
not the CI grep. Keeping both was belt-and-suspenders for a single-
developer repo that doesn't need either.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Juliusolsson05 Juliusolsson05 changed the title Fix grid-vs-dispatch divergence for good — resolver layer, migrations, CI lock Fix grid-vs-dispatch divergence for good — resolver layer + migrations May 13, 2026
@Juliusolsson05
Juliusolsson05 merged commit 4bd39dd into main May 13, 2026
@Juliusolsson05
Juliusolsson05 deleted the fix/grid-dispatch-divergence branch July 7, 2026 21:51
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.

Tab header activity counter misses dispatch agents — only walks grid tree (another grid-vs-dispatch divergence)

1 participant