Skip to content

fix(dispatch): two #37 follow-ups — undo loses detached agents; rehydrate stale focus - #39

Merged
Juliusolsson05 merged 2 commits into
mainfrom
fix/dispatch-detached-followups
May 1, 2026
Merged

Juliusolsson05 merged 2 commits into
mainfrom
fix/dispatch-detached-followups

Conversation

@Juliusolsson05

Copy link
Copy Markdown
Owner

Summary

Two bugs surfaced by review of PR #37 (dispatch detached sessions). Both are session-id lifecycle holes in the new detached model. Small, targeted, no behavior change beyond closing the holes.

Commit map

  1. `319a615` — High: `undo close` lost detached dispatch agents.
  2. `69c47cd` — Medium: rehydrate didn't remap `dispatchMode.focusedSessionId`.

Bug 1 — `undo close` silently dropped detached agents

`closeTab` killed both grid panes and detached agents associated with the tab (their `projectTabId` makes the tab the natural lifetime owner), but the undo entry only captured grid leaf metas. Cmd+Shift+T restored the tile tree and silently lost every parked dispatch agent — big trust hit because the user reasonably expects "undo close" to mean "give me back the entire tab."

Fix:

  • `lib/undoClose.ts`: extend `ClosedTab` with optional `detachedEntries` (`SessionMeta` for respawn + the original `detachedAt` so the dispatch row's age display doesn't snap back to "just now"). Optional because pre-feature undo entries don't carry it.
  • `actions/tab.ts`: capture detached entries at close time. Defensive flatMap skips records missing a SessionMeta.
  • `actions/undoClose.ts`: in the tab branch, after rebuilding the tile tree, re-spawn each detached entry and rebuild `DetachedSessionRecord` entries pointing at the restored tab's NEW id. Same restore-what-we-can policy as the existing tile-tree spawn loop. `SessionMeta` lands in `state.sessions` through `sessionActions.spawn`'s own setState, so this code only merges `DetachedSessionRecord` entries.

Bug 2 — rehydrate kept stale dispatch focus

`rehydrate.ts` spread `persisted.dispatchMode` directly while every other persisted SessionId (tab leaves, buried, detached) went through `idMap`. After restart, `dispatchMode.focusedSessionId` still pointed at a pre-restart sessionId, so `selectActiveRow` silently fell back to grid focus or first row. No crash, but commands that target dispatch focus operated on the wrong row.

Fix: pass `focusedSessionId` through `idMap.get(...)`, fall back to `undefined` if the old id didn't respawn. The dispatch UI's first-row fallback handles `undefined` cleanly.

Verification

  • `tsc -p tsconfig.web.json --noEmit` clean for all four touched files (24 unrelated errors persist on main, all in debug-panel/feed-row files unaffected by this PR).
  • No build run end-to-end; per the prior dictation-alias issue, building from a worktree off main currently exits non-zero on the main process bundle until fix: bump claude-code-headless for stale-flow watchdog #38 lands or a sibling `agent-voice-dictation` exists.

Test plan

  • Reviewer: in Dispatch, spawn 2-3 detached agents, close the tab → Cmd+Shift+T → tile tree comes back AND detached agents are back in the dispatch list with their original detachedAt-derived ages preserved.
  • Reviewer: in Dispatch with a detached agent focused, quit + reopen the app → that detached agent (or a sensible fallback row) is focused, not silently displaced to grid focus or first row.

🤖 Generated with Claude Code

Juliusolsson05 and others added 2 commits May 1, 2026 12:48
closeTab in actions/tab.ts kills both grid panes AND detached dispatch
agents associated with the project tab (the detached records carry
projectTabId, so the tab is the natural lifetime owner). The undo path
restored only the grid panes — detachedEntries weren't captured, so
Cmd+Shift+T silently dropped every parked dispatch agent the user had
in that project. Big trust hit: the user thought "undo close" meant
"give me back the entire tab" but actually got "tile-tree only,
everything else is gone."

Fix:

- lib/undoClose.ts: extend ClosedTab with optional detachedEntries,
  each carrying SessionMeta (for respawn) + the original detachedAt
  (so the dispatch row's age display doesn't snap to "just now" on
  undo — a 4-hour-old detached agent restored in the same minute
  should still read as 4 hours old). Optional because tab closes
  before the detached-sessions feature shipped, or tabs that simply
  had no detached agents, won't carry the field.

- actions/tab.ts: capture detachedEntries at close time. Defensive
  flatMap skips any record whose SessionMeta has gone missing — should
  never happen, but an undo entry referencing a non-existent meta
  would throw at restore time.

- actions/undoClose.ts: in the tab branch, after rebuilding the tile
  tree, re-spawn each detached entry and rebuild DetachedSessionRecord
  entries pointing at the restored tab's NEW id (the old tab id is
  gone). Same restore-what-we-can policy as the tile-tree spawn loop:
  failed spawns are skipped silently. SessionMeta lands in
  state.sessions through sessionActions.spawn's own setState, so the
  setState here only needs to merge the DetachedSessionRecord entries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dispatchMode.focusedSessionId is a SessionId that, like every other
persisted SessionId in the workspace (tab leaves, buried records,
detached records), needs to be remapped through the rehydrate idMap.
Without this remap the field carries a pre-restart sessionId past
restart, so the dispatch UI's selectActiveRow silently falls back to
grid focus or first row, and any command that targets dispatch focus
operates on the wrong visible row.

Fall back to undefined when the old id failed to respawn (or wasn't in
the idMap for any reason). Better to clear the focus than to pretend a
dead id is still selectable — the dispatch UI's first-row fallback
handles undefined cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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