feat(agents): reopen a closed agent instead of only deleting it - #503
Open
snehithareddy28 wants to merge 1 commit into
Open
feat(agents): reopen a closed agent instead of only deleting it#503snehithareddy28 wants to merge 1 commit into
snehithareddy28 wants to merge 1 commit into
Conversation
Closing an agent's tab retains it — the store says so outright ("Retained
+ flagged, NOT deleted") and Command Center → Archived lists it — but the
only control on an archived row was the ✕ that forgets it permanently. The
retained record had no way back onto the floor, so the one deliberate way
to close an agent was effectively one-way. "Restore team" does not help:
it is fed by restorableAgents, which only collects agents whose terminal
died unexpectedly, and a deliberately closed agent never enters that list.
Archived rows gain a reopen button. It puts the agent back with its own id,
its own cwd and its prior CLI session resumed, which is what makes memory.md,
the inbox and the registry entry reattach by id. addAgent already un-archives
by id, so the row leaves the list as the card returns to the floor.
The respawn recipe is the subtle part and it already existed inside
useRestoreTeam, so rather than writing a second one that could drift, it
moves to hooks/respawnAgent.ts and both callers share it: re-enter the
isolated worktree when it is still on disk (probed, never assumed) and fall
back to the base repo when it has been pruned, never re-isolate, resume the
prior session, and treat an id whose terminal is already running as
already-live rather than a failure. The module is structural and
dependency-injected, so the sequence is unit-testable without Electron.
A reopen that cannot work now says why on the row that failed, and the ✕
finally states that it deletes permanently.
Seven tests in test/respawn-agent.test.cjs cover the recipe: own id, resume
on, isolate off, durable role preserved, worktree re-entered, worktree gone
→ base repo, already-live, no saved command, and a throwing spawn reported
rather than propagated.
Closes chaitanyagiri#447
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
What & why
Closes #447. Closing an agent from the strip retains it —
store.tssays so outright ("Retained + flagged, NOT deleted"), and Command Center → Archived → show closed agents lists it. But the only control on an archived row was the ✕ that forgets it permanently, so the retained record had no way back onto the floor: the one deliberate way to close an agent was effectively one-way."Restore team" does not cover this, and it is easy to assume it does. That button is fed by
restorableAgents, which only collects agents whose terminal died unexpectedly (crash / quit recovery). An agent you closed on purpose never enters that list.Archived rows now have a reopen button. It brings the agent back with its own id, its own cwd and its prior CLI session resumed — which is what makes
memory.md, the inbox and the registry entry reattach by id rather than coming back as a stranger.addAgentalready un-archives by id ("an id is active xor archived"), so the row leaves the list as the card returns to the floor.The recipe is shared, not copied. The subtle part of a respawn already existed inside
useRestoreTeam, so rather than writing a second copy that could drift, it moves tohooks/respawnAgent.tsand both entry points call it:git worktree addwould conflict on the existing path and branch, and a fresh worktree would strand the uncommitted work in the old one;already-liverather than a phantom failure.The module is structural and dependency-injected (no store, no
window, no config imports), so the sequence is unit-testable without Electron — the same shapequeueDelivery.tsuses for its gate.Two smaller UX repairs on the same row: a reopen that cannot work now says why, on the row that failed, instead of looking inert; and the ✕ finally carries a title saying it deletes permanently.
Type of change
Evidence
Before
After
Notes for review:
reopenbutton before the ✕. While a reopen is in flight the button readsreopening…and every row's button is disabled, so a second click cannot spawn a duplicate. A failure renders as one coral line under that row (Couldn't reopen: …). Nothing else in the panel moves; the row becomes a small column so the error has somewhere to go. Strings are new keys incommandCenterand are translated in en, zh-CN and ar.useRestoreTeam's behaviour is unchanged — same flags, same per-agent tallies (restored/already live/failed — …), same roster-order insertion, same worktree-gone warning. It is ~60 lines shorter because the per-agent body is now one call.gitIsReporeturns false and the agent reopens on its base repo withworktree gone — using base repoon its card.already-liveputs the card back rather than reporting an error: the roster and a live PTY had disagreed, and restoring the card is the honest repair.test/respawn-agent.test.cjs, with the two main-process calls faked. They fail onmainbecause the module does not exist there.npm run typecheckand the fullnpm run test:focusedsuite (841/841) pass locally.🤖 Generated with Claude Code