Parent: #77
Problem
When an agent exits (completes task, crashes, or user stops it), restarting requires going through the full spawn flow even though the ticket already stores everything needed to restart.
Current State (verified via code inspection)
Ticket already persists all restart data (internal/board/board.go:69-98):
AgentType - "opencode", "claude", or "aider"
WorktreePath - absolute path to worktree
BranchName / BaseBranch - git context
AgentSpawnedAt - timestamp of first spawn (used for --continue detection)
AgentSessionID - OpenCode session ID for resume
AgentPort - OpenCode API port
Restart logic already exists in prepareSpawn() (ui/model.go:2376-2417):
isNewSession := ticket.AgentSpawnedAt == nil
switch agentType {
case "claude":
if !isNewSession {
args = append(args, "--continue") // Resume session
}
case "opencode":
if !isNewSession && sessionID != "" {
args = append(args, "--session", sessionID)
} else if !isNewSession {
args = append(args, "--continue")
}
}
The pain is UX, not capability - pressing s enters ModeSpawning which shows agent selection even when ticket already has AgentType set.
Proposed Solution: Smart s key
Modify spawnAgent() to skip agent selection when ticket already has spawn history:
func (m *Model) spawnAgent() (tea.Model, tea.Cmd) {
ticket := m.selectedTicket()
// Existing validations...
// NEW: Quick restart path
if ticket.AgentType != "" && ticket.AgentSpawnedAt != nil {
agentCfg, exists := m.config.Agents[ticket.AgentType]
if exists {
// Validate worktree still exists (if used)
if ticket.UseWorktree && ticket.WorktreePath != "" {
if _, err := os.Stat(ticket.WorktreePath); os.IsNotExist(err) {
m.notify("Worktree deleted — spawning fresh")
// Fall through to normal spawn to recreate
} else {
// QUICK RESTART: Skip modal, go directly to prepareSpawn
m.mode = ModeSpawning
m.spawningTicketID = ticket.ID
m.spawningAgent = ticket.AgentType
m.notify(fmt.Sprintf("Restarting %s...", ticket.AgentType))
return m, tea.Batch(m.spinner.Tick, m.prepareSpawn(ticket, proj, agentCfg))
}
} else if !ticket.UseWorktree {
// No worktree needed, quick restart
m.mode = ModeSpawning
m.spawningTicketID = ticket.ID
m.spawningAgent = ticket.AgentType
m.notify(fmt.Sprintf("Restarting %s...", ticket.AgentType))
return m, tea.Batch(m.spinner.Tick, m.prepareSpawn(ticket, proj, agentCfg))
}
} else {
m.notify(fmt.Sprintf("Agent '%s' no longer configured", ticket.AgentType))
// Fall through to normal spawn
}
}
// Existing: show agent selection modal for fresh spawns
// ...
}
Implementation Checklist
Changes to internal/ui/model.go
-
Modify spawnAgent() (~line 2255):
- Check if
ticket.AgentType != "" AND ticket.AgentSpawnedAt != nil
- Validate agent type still exists in config
- Validate worktree exists (if
UseWorktree && WorktreePath != "")
- If all valid → skip modal, go directly to
prepareSpawn()
- Show brief notification: "Restarting {agent}..."
-
No changes needed to prepareSpawn() - already handles restart vs new session correctly
-
No changes needed to Ticket struct - already has all required fields
Edge Cases
| Scenario |
Behavior |
Ticket has no prior spawn (AgentType == "") |
Normal flow (show selection) |
| Agent type removed from config |
Notify user, fall through to selection |
| Worktree was deleted |
Notify user, fall through to normal spawn (recreates worktree) |
| Ticket not in "In Progress" |
Existing validation blocks spawn |
| Agent already running for ticket |
Existing validation blocks spawn |
| Different agent already running in main repo |
Existing validation blocks spawn |
Testing
# Manual test cases:
1. Spawn agent on ticket → exit → press 's' → should restart immediately
2. Spawn agent → delete worktree manually → press 's' → should notify and recreate
3. Spawn agent → remove agent from config → press 's' → should notify and show selection
4. Fresh ticket → press 's' → should show selection (existing behavior)
Alternative Considered: Separate r Keybinding
Rejected because:
- Adds cognitive load (which key to press?)
s for "spawn" already implies "start agent" - semantically covers restart
- Smart detection is invisible UX improvement
Files to Modify
internal/ui/model.go - spawnAgent() function only (~15-20 lines)
Acceptance Criteria
Parent: #77
Problem
When an agent exits (completes task, crashes, or user stops it), restarting requires going through the full spawn flow even though the ticket already stores everything needed to restart.
Current State (verified via code inspection)
Ticket already persists all restart data (
internal/board/board.go:69-98):AgentType- "opencode", "claude", or "aider"WorktreePath- absolute path to worktreeBranchName/BaseBranch- git contextAgentSpawnedAt- timestamp of first spawn (used for--continuedetection)AgentSessionID- OpenCode session ID for resumeAgentPort- OpenCode API portRestart logic already exists in
prepareSpawn()(ui/model.go:2376-2417):The pain is UX, not capability - pressing
sentersModeSpawningwhich shows agent selection even when ticket already hasAgentTypeset.Proposed Solution: Smart
skeyModify
spawnAgent()to skip agent selection when ticket already has spawn history:Implementation Checklist
Changes to
internal/ui/model.goModify
spawnAgent()(~line 2255):ticket.AgentType != ""ANDticket.AgentSpawnedAt != nilUseWorktree && WorktreePath != "")prepareSpawn()No changes needed to
prepareSpawn()- already handles restart vs new session correctlyNo changes needed to Ticket struct - already has all required fields
Edge Cases
AgentType == "")Testing
Alternative Considered: Separate
rKeybindingRejected because:
sfor "spawn" already implies "start agent" - semantically covers restartFiles to Modify
internal/ui/model.go-spawnAgent()function only (~15-20 lines)Acceptance Criteria
son ticket with prior spawn restarts immediately (no modal)