Skip to content

fix(dispatch): give every Grid Dispatch lane its own selector strip - #859

Merged
Juliusolsson05 merged 6 commits into
mainfrom
fix/grid-dispatch-first-lane-strip
Sep 11, 2026
Merged

Juliusolsson05 merged 6 commits into
mainfrom
fix/grid-dispatch-first-lane-strip

Conversation

@Juliusolsson05

@Juliusolsson05 Juliusolsson05 commented Sep 10, 2026 •

Copy link
Copy Markdown
Owner

Problem

In Grid Dispatch, the first (leftmost) lane of every row had no compact index strip. Every other lane had one. Changing the agent in the first lane took two gestures (click into the lane to move focus, then pick from the row's index), while every other lane took one click on its strip. The first lane is also the widest by default, so it's where agents get swapped most.

How it got that way, corrected after review:

Change

  • Every Grid Dispatch lane now renders its DispatchMiniList strip, including the first lane of each row. A strip always writes its own lane and focuses it, through the waking selectTiledLaneSession path. The index is unchanged and still follows focus.
  • Single-lane rows get a strip too. A strip that appeared only when a row gained a second lane would shove the existing lane's content 46px sideways mid-rearrangement. One rule for every lane also keeps the chip column in the same place, which is what makes it scannable.
  • Empty-lane hint:
    • It is one sentence in every lane: "Pick an agent from the strip, or press ⌥↓ for the top of the index".
    • It is not shown at all when the row offers no agents, for example a row bound to projects that currently have none. There the strip is empty and ⌥↓ does nothing.
    • The gate uses rowScopedRows, the same filter the strip and ⌥↓ use.
  • DispatchMiniList's gridRow prop type now names projectTabIds, the field rowScopedRows actually filters on, instead of the legacy projectTabId.

Design notes

  • Why not revert the index to lane-0-only. The follow-focus index was a deliberate choice (clicking a row's index means "I am working in this row now"). A new test pins that premise. If the index ever stops following focus, that test fails first, and the first lane's strip should be reconsidered.
  • Why the old bound-row hint branch was deleted rather than "fixed". 524671b2 withheld "for the top of the index" from project-bound rows because ⌥↓ then walked the global list. 29ecd829 later made ⌥↓ walk the focused row's own filtered list (tiledRowScopedRows), which makes the promise true in bound rows. The branch read the legacy projectTabId, which normalizeGridShape folds away, so it was dead and already fell through to the correct copy.

Review

Two orchestrated reviewers ran through the Agent Code orchestration MCP, both on Codex: one on correctness and behavior, one on tests, comments and conventions. A Claude reviewer was attempted twice but never received its prompt (#854), so the second seat moved to Codex. Verdicts were Yes and With fixes. There were no Critical findings. Everything valid is resolved on this branch:

Finding Resolution
Regression history attributed to the wrong commit (both reviewers) Corrected in the layout's WHY comment, the plan, this body, and #850 (318ad66f)
Strip-click test only used flat lane 0, where laneIndex, column and start coincide Now a table over lanes 0, 2 and 3 with focus elsewhere (19dbccd1)
Strip gridRow wiring untested New test: each strip gets its own row's binding
Expand test only proved a handler was present Now asserts toggleDispatchRowExpandedParent(rowIndex, parentId) per strip
Row-index test comment overclaimed Reworded; new test pins that the index fills the focused lane of its row
Strip lookup could silently match more than one stripSelecting requires exactly one match
Empty-lane hint promised a pick in rows with no agents (pre-existing) Hint gated on the row offering an agent (6ee33cc0); test was RED first

Mutation-checked: I temporarily applied each realistic miswiring to the layout and confirmed the matching test fails:

  • the strip writes column or start instead of its own lane;
  • every strip is handed grid.rows[0];
  • expand is hardcoded to row 0;
  • selectIntoRow always targets start.

Not changed (known limitation, follow-up): the fixed 46px strip plus the percentage-only LANE_MIN_FRACTION (0.08 of the lane region) can leave a lane that was dragged to its minimum with no visible content. This already applied to every other lane; this PR extends it to the first lane. The fix is a minimum-content-width rule for lanes and belongs in its own change.

Tests

gridDispatchLayout.renderer.test.tsx renders the real TiledDispatchLayout over the recorded dispatch-global-d23 fixture.

  • Every lane has a strip. 2×2 → 4 strips, [3, 1] → 4. This replaces the two tests that pinned "first lane has no strip". It failed on main with 2.
  • Each strip writes its own lane and focuses it, wherever focus is. Lanes 0, 2 and 3. The lane-0 case failed on main, where lane 0 had no strip.
  • Each strip gets its own row's project binding.
  • Expand addresses the strip's own row.
  • A row's index fills the focused lane of that row.
  • No hint is shown when the row offers no agents. This case uses an unbound row as the contrast. It failed before 6ee33cc0.

Verification

Node 24 (.nvmrc):

  • Dispatch renderer suite: 6 files, 44 tests pass.
  • tsc -b: exit 0 (node and web projects).
  • CI on the first push (0ef2b17e): quality-gate (full suite, coverage, package verification) and minimum-node-fixture-gate both passed.
  • Full npm test locally: could not complete. macOS killed it for low memory (load 99–169, swap about 12 of 13 GB, alongside other agent sessions and the running app).
    • vitest related on the changed components passed: 5 files, 30 tests.
    • Of the files that failed during that partial run, the ones outside this change's import graph were re-run one worker at a time. 4 of 7 pass.
    • store.test.ts and lazy-prose still time out, but only under this machine's load; both pass in CI.
    • imageAttachment.test.ts fails a machine-local provenance check because a cited ~/.claude session file was deleted. CI skips that check.

I didn't launch the app, per the repo rule.

Risk

Low. The change is purely presentational: the first lane of each row is 46px narrower, like every other lane already was, and a hint is hidden in rows that offer nothing. There are no state shape, persistence, keybinding or command changes.

Unrelated CI note: main at 48ea6912 went red in "Enforce coverage baseline" on src/remote-client/src/WebSocketSessionFeed.integration.test.ts. The same test passed in that job's "Test system" step, and it passed on this PR's first run, so it looks timing-sensitive under coverage instrumentation.

Fixes #850

🤖 Generated with Claude Code

https://claude.ai/code/session_01J8PfoJxJ1cvJ9akXsWSVZB

Juliusolsson05 and others added 6 commits September 10, 2026 15:58
…r strip

Refs #850

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8PfoJxJ1cvJ9akXsWSVZB
Since #687 a row's index fills whichever lane of the row is focused, but
the first lane of each row still skipped its compact index strip on the
old grounds that the index was its selector. That left the leftmost lane
as the only one that could not be swapped in one click: the user had to
click into it to move focus, then pick from the index.

Every lane, single-lane rows included, now carries a strip addressed to
that lane, and the index keeps following focus. A strip that appeared
only once a row gained a second lane would shift the existing lane's
content sideways mid-rearrangement, so the rule has no exceptions.

The empty-lane hint loses its first-lane branch and a dead branch keyed
on the legacy projectTabId, which normalizeGridShape folds away on read.
That branch already fell through to the correct copy, because ⌥↓ has
walked the focused row's own list since 29ecd82, so "the top of the
index" holds in project-bound rows too. DispatchMiniList's prop type now
names projectTabIds, the field it actually filters on.

Refs #850

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8PfoJxJ1cvJ9akXsWSVZB
The first draft planned to fix the hint's legacy projectTabId read so
bound rows would stop promising "the top of the index". That premise
predates 29ecd82, which made ⌥↓ row-scoped and the promise true, so the
plan now records deleting the dead branch instead and drops the hint test.

Refs #850

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8PfoJxJ1cvJ9akXsWSVZB
…expand target

Review of #859 showed the strip tests could not tell right wiring from
wrong. The only strip clicked sat in flat lane 0, where laneIndex, column
and start are all 0. Nothing checked which row's binding a strip was
handed, and the expand test passed for any handler, a no-op included.

The strip-click test is now a table over lanes 0, 2 and 3 with focus
elsewhere. New tests check that each strip gets its own row's binding and
that expand addresses its own row. Another pins that a row's index fills
the focused lane of that row, which is the premise that makes every lane
need a strip. Each was checked by mutating the layout: column, start,
grid.rows[0], a row-0 expand and a bare start in selectIntoRow all fail.
Strip lookup now requires exactly one match.

Refs #850

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8PfoJxJ1cvJ9akXsWSVZB
The comment, plan and PR said #687 kept the first lane strip-less. It did
the opposite. 524671b (#687) made the index follow focus and gave every
lane a strip for that reason. 1eb9a47 (#691, a lane-wake fix) then
removed the first lane's strip as a side change, on the grounds that the
index was its selector, which had already stopped being true. Both PR
reviewers caught it with git show. The record now points at the change
that actually regressed, and the plan records the review and its
resolutions.

Refs #850

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8PfoJxJ1cvJ9akXsWSVZB
…ents

A focused empty lane advertised "pick from the strip, or press ⌥↓" even in
a row bound to projects that currently have no agents. There the strip is
empty and ⌥↓ returns without doing anything, so both halves of the hint
promised a pick that could not happen. The hint is now gated on the row
offering at least one agent. That is checked through rowScopedRows, the
same filter the strip and ⌥↓ use, so the hint and the gestures it names
cannot drift apart. The test was RED before the change, and it has an
unbound contrast case so "never show a hint" cannot pass it.

Pre-existing, raised in review of #859 because the PR rewrote this hint.

Refs #850

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8PfoJxJ1cvJ9akXsWSVZB
@Juliusolsson05

Copy link
Copy Markdown
Owner Author

Review round complete. Two orchestrated Codex reviewers ran: correctness gave Yes, tests and conventions gave With fixes, and neither found anything Critical. All valid findings are resolved in 19dbccd1 (strip tests pinned to lane, row binding and expand target, each checked by mutation), 318ad66f (corrected #687/#691 regression history) and 6ee33cc0 (no empty-lane hint in rows that offer no agents). One finding is not adopted: the narrow-lane content limitation predates this PR and applies to every lane, so it's recorded in the description as a follow-up. The description has the full table.

@Juliusolsson05
Juliusolsson05 merged commit ad9ddc4 into main Sep 11, 2026
3 of 4 checks passed
@Juliusolsson05
Juliusolsson05 deleted the fix/grid-dispatch-first-lane-strip branch September 11, 2026 00:29
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.

bug(dispatch): Grid Dispatch first lanes lost their selector strip when the index began following focus

1 participant