Repository navigation
feat(dispatch): add New Agent In… to spawn an agent into a chosen project - #860
Merged
Merged
Conversation
Filling empty Grid Dispatch lanes currently requires selecting an unrelated agent in the target project first, only to move the spawn target. The plan records the approved design: a Dispatch-only two-step picker (agent, then project) that reuses createDetachedDispatchAgent's explicit project override. Refs #852 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVRSHn7c7qnBoUQo1e2X4k
resolveDispatchSpawnTarget read the focused Grid Dispatch row's project binding inline. New Agent In needs the same answer to decide which projects it may offer for that lane, and two separate derivations could drift (the legacy projectTabId fold and row-length repair live in normalizeGridShape), letting the picker offer a project the resolver would refuse. Behavior is unchanged. The resolver's bound-row branch had no coverage, so its two rules (bound project over stale classic focus; active tab when it is one of several bound projects) are pinned before the move. Refs #852 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVRSHn7c7qnBoUQo1e2X4k
Filling an empty Grid Dispatch lane with an agent for another project meant first selecting some unrelated agent in that project, only to move the spawn target. New Agent In asks for the agent and then the project, and spawns through createDetachedDispatchAgent's existing explicit-project override, so the agent still fills the focused lane exactly like New Agent. It is Dispatch-only, honours the focused row's project binding, starts on the project plain New Agent would have used, and lists projects it cannot anchor as disabled with the reason. The dialog is a dedicated DialogContent surface rather than another intent on the placement overlay, and it derives nothing while closed. The catalog snapshot moves to 117 in this commit, per its own rule; its two test titles had drifted to 115 and now track the asserted count again. Refs #852 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVRSHn7c7qnBoUQo1e2X4k
This was referenced Sep 11, 2026
Closed
The picker's list handler took every Enter: it called preventDefault, which cancels the focused button's native click, and then committed the highlighted provider. Tab to Cancel and Enter therefore switched the agent's provider instead of cancelling, breaking the rule dialog-actions.tsx documents for focused buttons. Footer buttons now keep their own Enter; the list keeps highlight semantics for rows. Fixes #862 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVRSHn7c7qnBoUQo1e2X4k
Review of the new dialog found it copied the Switch Provider Enter bug: Tab to Cancel or Back and Enter spawned an agent. Footer buttons now own their Enter, and auto-repeated Enter is ignored so one long press cannot pick the agent and commit the default project. One-shot state resets on close instead of open, so an open that does not come from a user event never paints a stale project step. The cross-step focus effect is gone because Radix FocusScope already refocuses the container when a focused row unmounts. The empty project list now names the real cause (the row is bound only to closed projects, #863) and the control that fixes it. Comments and the plan state the anchor rule precisely: same project as New Agent, but the project's own checkout. Refs #852 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVRSHn7c7qnBoUQo1e2X4k
DialogActions already encoded "a focused button, link or textarea owns its own Enter", but list-style dialogs that handle Enter on DialogContent could not reach it and copied a footer-slot guard instead (New Agent In, Switch Provider). Exporting the rule as focusedControlOwnsEnter gives those dialogs, and the three more that #867 tracks, one definition to call. Behavior of DialogActions is unchanged; its Enter rule had no tests, so the confirm and Cancel-owns-Enter paths are pinned alongside the extraction. Refs #862 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVRSHn7c7qnBoUQo1e2X4k
A provider row could hold keyboard focus while the arrows moved the highlight elsewhere. Space is a native click on the focused button, so Tab to a row, ArrowDown, Space switched the agent to a provider other than the highlighted one. Rows are no longer tab stops: keyboard focus now rests only on the dialog surface, where arrows and Enter act on the highlight, or on the footer. The Enter guard uses the shared focusedControlOwnsEnter rule. Fixes #862 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVRSHn7c7qnBoUQo1e2X4k
The second review found the same focus-versus-highlight split in New Agent In: Tab to a project row, arrow to another, Space, and the agent was created in the focused project instead of the highlighted one (step one could pick a different agent the same way). Rows are no longer tab stops and their focus sync is gone, so keyboard selection is only ever the highlight. Eligibility is now an explicit `enabled` discriminant on the project model, so the dialog no longer re-derives it from the anchor and a future second disable reason cannot be committable. The reopen test now fails if the stale project step mounts even briefly, which the previous version could not detect. The dialog's keys are declared for the control reference, and the command description covers classic Dispatch rows as well as Grid lanes. Refs #852 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVRSHn7c7qnBoUQo1e2X4k
The bound-row branch of resolveDispatchSpawnTarget was already covered on the real persisted fixture in rowScopedRows.test.ts; the two resolver cases added with the selector extraction restated it on a hand-built state, and their comment wrongly claimed the branch had been untested. The selector's own tests stay. Also points a catalog comment at the asserted arithmetic instead of a number that had drifted to 103 + 4 = 107. Refs #852 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVRSHn7c7qnBoUQo1e2X4k
Juliusolsson05
added a commit
that referenced
this pull request
Sep 11, 2026
Brings #860 (New Agent In), #861 (Reader Mode from the render ledger) and #870 (Codex cyber-block native clone) into the branch before merge. The textual merge is clean; eight files changed on both sides, so the merged tree is re-verified with tsc and the full suite rather than trusting the pre-merge CI run. Refs #865 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LEwGeHkkhnDxU3X3chXX3F
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Filling empty Grid Dispatch lanes needed a detour. With the focused lane empty and its row unbound,
resolveDispatchSpawnTargetfiles a new agent under the classic Dispatch focus (the last agent you selected) oractiveTabId. To put a Codex agent for project C into an empty lane you had to select some unrelated agent in C purely to move the target, come back to the lane, then run New Agent… — once per lane.What this adds
New Agent In… (
new-agent-in, Dispatch only). A two-step dialog:AGENT_PROVIDER_CHOICES).A · titlelike the rest of Dispatch. Backspace goes back.Committing calls the existing
createDetachedDispatchAgent(selection, { tabId, anchorSessionId })— the same explicit-project path the Dispatch project header's "+" and the externalagents.createcapability use. The agent fills the focused lane, exactly like New Agent….Design decisions
surface: 'dispatch', also hidden under Tiled Tabs like New Agent…). In the grid a project is a tab one keystroke away and a detached agent spawned from the grid is not visible.resolveDispatchSpawnTargetintofocusedLaneBoundProjectTabIds, which both now read, so the picker and the resolver cannot disagree about what a row may hold. The resolver's behavior is unchanged; its bound-row rules are covered by the existingrowScopedRows.test.tson the real persisted fixture (an earlier version of this PR wrongly said the branch was untested and duplicated those cases — removed in round 2).DialogContent(Escape, focus trap, outside-click and app input ownership come from the primitive). Not a fifth mode ofNewAgentPlacementOverlay(already four intents; its "kind picked ⇒ placement step" invariant would need another branch) and not a palette sub-mode (a two-step flow would need its first answer in store state across palette modes).Review round (independent code review before merge)
Verdict was "ready with fixes"; everything below is fixed on this branch (commits
2e793b57,1b5bd639):preventDefault(cancelling the focused button's native click) and then committed the highlighted row, so Tab → Cancel/Back → Enter spawned an agent. Footer buttons now own their Enter, per the rulecomponents/ui/dialog-actions.tsxdocuments; rows keep highlight semantics. The dialog had copied this fromProviderSwitchPickerModal, where Tab → Cancel → Enter started a provider switch onmain— filed as bug(ui): Switch Provider keyboard activation can commit a provider the user did not pick #862 and fixed here with a regression test.closeTabnever clears row bindings in memory, which also makes plain New Agent… fail silently with the overlay stuck — predates this PR and is filed as bug(dispatch): closing a project tab leaves grid rows bound to it, so New Agent silently does nothing #863 (not fixed here).row sin test titles is this repo's existing convention for avoiding apostrophes in single-quoted titles (e.g.rowScopedRows.test.ts).Review round 2 (two orchestrated reviewers via the Agent Code orchestration MCP)
Reviewer A (Codex, adversarial behavior) and Reviewer B (Claude, integration/conventions/tests) both returned "ready with fixes". Fixed in
9e4414e3,c6f778fc,01c0fb44,599f7272:main: arrows moved the highlight while DOM focus stayed on a row, and Space is a native click on the focused button — no Enter handling can intercept it. Rows in both dialogs are no longer tab stops (tabIndex={-1}), so keyboard focus is only ever the dialog surface (arrows + Enter act on the highlight) or the footer (owns its own Enter/Space). bug(ui): Switch Provider keyboard activation can commit a provider the user did not pick #862 was widened to this path; structural regression tests in both dialogs (happy-dom cannot synthesize keyboard clicks).focusedControlOwnsEnterincomponents/ui/dialog-actions.tsx, used byDialogActions(behavior unchanged, now pinned by a newdialog-actions.renderer.test.tsx) and both list dialogs, replacing a copied footer-slot guard. Three more pre-existing dialogs with the pattern — Agent View Mode, Reorder Tabs, Pin Agents — are filed as bug(ui): Enter on a focused Cancel applies the change in Agent View Mode, Reorder Tabs and Pin Agents #867, not fixed here.controlInteractions.ts; the disabled-row test asserts the nativedisabledattribute; eligibility is an explicitenableddiscriminant on the project model (no longer re-derived from the anchor in three places); a stale103 + 4 = 107catalog comment fixed; the command description covers classic Dispatch rows as well as Grid lanes.Linked issues
Fixes #852
Fixes #862
Refs #863, #867
Verification
Local, Node 24:
npm run typecheck(control-sdk + workflow-mcp build +tsc -bon node and web) — exit 0 at every round, last at599f7272.enabled; the reopen DOM-watch test (red with reset-on-open, green with reset-on-close).DialogActions' pinned tests pass before and after the extraction (characterization).599f7272: renderer 27/27 (NewAgentInDialog,ProviderSwitchPickerModal,dialog-actions,dialog,control/documentation), unit 69/69 (newAgentInProjects,dispatchSelectors,rowScopedRows,catalog,featureReference).quality-gateandminimum-node-fixture-gatepassed on the first two pushes; the round-2 push (599f7272) is gated the same way before merge.maingained fix(workspace): light the status header for terminal-view agent panes #853/fix(dispatch): give every Grid Dispatch lane its own selector strip #859 meanwhile — no file overlap, clean merge.npm run test:contract,npm run check:keybindings— pass.catalog.test.ts(snapshot 116 → 117, fails before the command exists),taxonomy.test.ts,featureReference.test.ts,dispatchSelectors.test.ts,newAgentInProjects.test.ts,NewAgentInDialog.renderer.test.tsx.New tests:
newAgentInProjects.test.ts— tab order + global letters, bound-row filtering, anchor order (grid leaf before detached worktree row; skips a leaf with no live session), disabled-with-reason, initial highlight = plain New Agent's target, fallback when that target is not offered or not anchorable.NewAgentInDialog.renderer.test.tsx— Enter-Enter lands in New Agent's target project; keyboard and mouse two-step commits pass{kind, providerRuntime}+{tabId, anchorSessionId}; OpenCode Terminal stays a distinct runtime; Backspace goes back without creating; disabled project can't be chosen by click or arrows; repeated Enter spawns once; Cancel never creates; nothing derived while closed.dispatchSelectors.test.ts— the new selector (the resolver's bound-row rules stay covered byrowScopedRows.test.ts, which guarded the extraction).dialog-actions.renderer.test.tsx— the shared focused-control Enter rule andDialogActions' confirm / Cancel-owns-Enter behavior.Full local suite was not clean, for reasons unrelated to this diff (16 GB machine swapping with three other agent sessions running Vitest concurrently). Every failing file was checked against the base commit
48ea6912:store.test.ts"hydrates legacy standalone prompt templates…",store.performance.test.tshook,lazy-proserenderer test — 5s/10s timeoutsimageAttachment.test.ts"every fixture is traceable to … a real session"~/.claude/projects/…klay/that no longer exists on this machine; fails identically on the base commit.dictation/historyStore.test.ts,control/history/tasks.test.tscontrolHost.system.test.ts— 180s timeoutCI's
quality-gateis the authoritative run for this PR.Limitations / follow-ups
createDetachedDispatchAgentbehavior, not changed here.🤖 Generated with Claude Code
https://claude.ai/code/session_01SVRSHn7c7qnBoUQo1e2X4k