Repository navigation
feat(dispatch): detached sessions — stop dispatch from poisoning grid layout - #37
Merged
Merged
Conversation
…pawn Decouple session creation from grid placement so spawning ten command-center agents in Dispatch Mode no longer leaves ten unmanageable panes in the tile tree when the user toggles back to grid. ## Model Adds DetachedSessionRecord and a parallel state bucket: detachedSessions: Record<SessionId, DetachedSessionRecord> Invariant: a live session is in exactly one of: - a leaf in tabs[].root (grid placement) - detachedSessions (intentionally not placed in the grid) Tab.focusedSessionId stays grid-only — it has a hard "must be a leaf in tab.root" invariant used by reader, spotlight, resize, split, duplicate, and bury commands. Dispatch selection rides on a new mode-local field: dispatchMode.focusedSessionId …so exiting Dispatch never leaves the grid in an impossible focus state. ## Persistence - persistence.ts: PersistedWorkspace.detachedSessions added (optional for backwards compat with pre-feature workspace.json files). - useAutoSave.ts: writes detachedSessions on every snapshot. - rehydrate.ts: remaps detached sessionIds through the same idMap as tile leaves and buried records — renderer SessionIds are per-launch routing ids, so persisted records have to follow the old->new map or later lifecycle actions update the wrong key. ## Dispatch UI - dispatchSelectors.ts: buildDispatchGroups now emits a row per grid leaf AND per detached session for the relevant tab, tagged with placement: 'grid' | 'detached' so future UI/commands can distinguish them. - DispatchLayout.tsx: selectActiveRow prefers dispatch focus, falls back to grid focus, then first row. - useKeybinds.ts: Cmd+1..9 and Option+ArrowUp/Down route through focusDispatchSession instead of focusSessionInTab. ## Spawn - pane.ts:createDetachedDispatchAgent — spawns a Claude/Codex session and appends a detached record without touching tab.root. - NewAgentPlacementOverlay.tsx: in Dispatch Mode, becomes a kind picker (no placement step) and commits via createDetachedDispatchAgent. Race- protected via a committingRef so rapid Enter cannot spawn duplicates. - App.tsx: overlay is now also rendered when DispatchLayout is the active surface. - paneCommands.ts: New Agent command no longer blocked in Dispatch. ## Lifecycle - pane.ts:closeSession — handles the detached path: kill backend, drop runtime + uuid/screen refs, delete from sessions + detachedSessions, clear dispatch focus if it pointed at the closed session. - session.ts:close — same cleanup pattern for the generic kill path. - session.ts:reloadAgentSessions — remaps detached sessionIds and dispatchMode.focusedSessionId through the post-reload idMap, so a reloaded detached agent keeps its dispatch selection. - tab.ts:closeTab — kills both grid AND detached sessions for the closed project tab; clears dispatch focus if it landed in the killed set. Still missing (follow-up commit in this same PR): "Attach Detached To Grid" command, "Detach Grid To Dispatch" reverse, edge-case cleanup for tabs whose only sessions are detached. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the two missing commands that complete the detached-sessions model: a detached agent can be promoted into the grid through the existing placement overlay, and a focused grid agent can be moved out to the dispatch detached bucket without being killed. ## Attach Detached → Grid - pane.ts:attachDetachedToGrid — synchronous setState that removes the session from detachedSessions and inserts it into the active tab's tile tree at the chosen PlacementTarget. No spawn round-trip; the process is already running and the renderer runtime entry already exists, so the UI seam is invisible to the user beyond the layout change. - The target tab need not equal the detached record's projectTabId — projectTabId was always affinity, not ownership. Pinning a project-A detached agent into project-B's grid is the whole point of having a placement step. - Stale-anchor guard: if the chosen split-leaf target's anchor was closed between picker open and Enter, the action no-ops cleanly rather than silently dropping into the wrong tree. ## Detach Grid → Dispatch - pane.ts:detachFocusedToDispatch — removes the focused leaf via the existing closeLeaf primitive without killing the session, and adds a detached record. If Dispatch Mode is active, focuses the freshly detached agent so the user sees the result of the action. - Refuses (with toast) for terminals (the right-hand project terminal slot already serves that role) and for the only-leaf-in-tab case (closeLeaf would return null and tab.root cannot represent an empty tree — the user should add another pane first or close the tab). ## Overlay attach mode - NewAgentPlacementOverlay accepts attachDetachedSessionId. When set, the kind picker is skipped (the session already exists with a known kind) and the overlay opens directly on the placement-target step. Enter calls attachDetachedToGrid instead of commitNewAgentPlacement. - App.tsx subscribes to dispatchAttachIntent in the uiShell store; the overlay is open when either newAgentPlacementOpen OR dispatchAttachIntent is set, and the close handler clears both so the next open starts fresh. ## Store + command surface - uiShell: dispatchAttachIntent: SessionId | null + open/close actions. - CommandContext.ui: openDispatchAttach passed through CommandPalette. - paneCommands: attach-detached-to-grid (gated on dispatch-focused detached session) and detach-to-dispatch (gated on focused non-terminal grid leaf). ## commandTargetSessionId selector Adds src/renderer/src/workspace/hook/selectors/commandTargetSessionId.ts as a documented helper for "which session is the user currently commanding?" — Dispatch focus first, grid focus second. Currently unused; staged for migrating close/kill/provider-replace/copy-assistant call sites that should target detached sessions when Dispatch is on screen. Kept out of the workspace getter so call sites that import this file are explicitly declaring "I work on detached AND grid sessions" in the diff. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures the model, invariants, file map, and the deliberate follow-ups that aren't in this PR (closeFocused dispatch-targeting migration, attach-into-empty-tab, future tmux dispatch terminology rename in plan/2026-04-13). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three real type errors in the baseline detached-sessions commit that the original Session 2 worktree never caught (the project-reference ordering blocked the typecheck there). All three now clean under \`npx tsc -p tsconfig.web.json --noEmit\`: - useKeybinds.ts:dispatchRows — buildDispatchGroups was tightened to one (state) arg when detached-sessions landed; the call site here still passed workspace.runtimes as a second arg. Drop it. - NewAgentPlacementOverlay.tsx:arrow handler — anchorSessionId is string | null and was passed straight to placementTargetIdForArrow which expects string. The component's render guards already prevent the null path at render time, but the keydown handler runs at the document level so the null branch is reachable in transient states. Add an explicit early-return. - NewAgentPlacementOverlay.tsx:dispatch-mode Enter — option.kind is SessionKind, but createDetachedDispatchAgent only accepts 'claude' | 'codex'. The kindOptions array is already filtered to exclude terminal in dispatch mode at runtime, but the typechecker cannot infer that from the runtime filter. Re-narrow at the call site. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2 tasks
Juliusolsson05
added a commit
that referenced
this pull request
May 1, 2026
…owups fix(dispatch): two #37 follow-ups — undo loses detached agents; rehydrate stale focus
This was referenced May 11, 2026
This was referenced Sep 25, 2026
Merged
Merged
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.
Summary
Decouple session creation from grid placement so spawning ten command-center agents in Dispatch Mode no longer leaves ten unmanageable panes in the tile tree when the user toggles back to grid.
What it does
Why
Before this PR, Dispatch Mode was a different rendering of the same tile tree. Creating an agent in Dispatch silently inserted it into the active tab's tree; spawning ten then exiting Dispatch left ten unmanageable panes. The detached bucket fixes this by making session ownership independent of grid placement.
Commit map
Known limitations / deliberate follow-ups (not in this PR)
Test plan
🤖 Generated with Claude Code