Skip to content

Quick-restart previously spawned agents #78

Description

@TechDufus

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

  1. 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}..."
  2. No changes needed to prepareSpawn() - already handles restart vs new session correctly

  3. 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

  • s on ticket with prior spawn restarts immediately (no modal)
  • Brief notification shows "Restarting {agent}..."
  • Graceful fallback to selection modal on edge cases
  • No regression for fresh ticket spawns

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions