Add interactive resume picker for stopped/idle sessions · Entire

Add interactive resume picker for stopped/idle sessions

103fcd7→main· Soph·1mo ago·12 files·+838 added/-240 removed

entire resume with no argument now opens an interactive picker of resumable sessions across all worktrees, so you don't have to remember which branch you left work on. Picking a session checks out its branch and prints the command to continue the agent; if the branch is already checked out in another worktree, it points you there instead of failing a checkout. entire resume <branch> is unchanged.

Details:

Tests cover the picker filtering/sorting, labels, branch derivation, internal-ref exclusion, worktree-clash detection, and keep-existing-log behavior. Obsolete overwrite-prompt tests removed; timestamp resume integration tests updated to keep-by-default semantics.

Sessions

Changes


Code

//go:build integration && unix

package integration

import (
    "fmt"
    "os"
    "path/filepath"
    "strings"
    "testing"
    "time"
)

func (env *TestEnv) RunResumeInteractive(branchName string, respond func(ptyFile *os.File) string) (string, error) {
    env.T.Helper()
    return env.RunCommandInteractive([]string{"resume", branchName}, respond)
}

func TestResume_LocalLogNewerTimestamp_UserConfirmsOverwrite(t *testing.T) {
    t.Parallel()
    env := NewFeatureBranchEnv(t)

// Create a session with a specific timestamp
    session := env.NewSession()
    if err := env.SimulateUserPromptSubmit(session.ID); err != nil {
        t.Fatalf("SimulateUserPromptSubmit failed: %v", err)
    }

content := "def hello; end"
    env.WriteFile("hello.rb", content)
    
    session.CreateTranscript(
        "Create hello method",
        []FileChange{{Path: "hello.rb", Content: content}},
    )
    if err := env.SimulateStop(session.ID, session.TranscriptPath); err != nil {
        t.Fatalf("SimulateStop failed: %v", err)
    }

// Commit the session's changes (manual-commit requires user to commit)
    env.GitCommitWithShadowHooks("Create hello method", "hello.rb")

featureBranch := env.GetCurrentBranch()

// Create a local log with a NEWER timestamp than the checkpoint
    if err := os.MkdirAll(env.ClaudeProjectDir, 0o755); err != nil {
        t.Fatalf("failed to create Claude project dir: %v", err)
    }
    existingLog := filepath.Join(env.ClaudeProjectDir, session.ID+".jsonl")
    futureTimestamp := time.Now().Add(24 * time.Hour).UTC().Format(time.RFC3339)
    newerContent := fmt.Sprintf(`{"type":"human","timestamp":"%s","message":{"content":"newer local work"}}`, futureTimestamp)
    if err := os.WriteFile(existingLog, []byte(newerContent), 0o644); err != nil {
        t.Fatalf("failed to write existing log: %v", err)
    }

// Switch to main
    env.GitCheckoutBranch(masterBranch)

// Resume interactively and confirm the overwrite
    output, err := env.RunResumeInteractive(featureBranch, func(ptyFile *os.File) string {
        out, promptErr := WaitForPromptAndRespond(ptyFile, "[y/N]", "y\n", 10*time.Second)
        if promptErr != nil {
            t.Logf("Warning: %v", promptErr)
        }
        return out
    })
    if err != nil {
        t.Fatalf("resume with user confirmation failed: %v\nOutput: %s", err, output)
    }

// Verify local log was overwritten with checkpoint content
    data, err := os.ReadFile(existingLog)
    if err != nil {
        t.Fatalf("failed to read log: %v", err)
    }
    if strings.Contains(string(data), "newer local work") {
        t.Errorf("local log should have been overwritten after user confirmed, but still has newer content: %s", string(data))
    }
    if !strings.Contains(string(data), "Create hello method") {
        t.Errorf("restored log should contain checkpoint transcript, got: %s", string(data))
    }
}

// Other relevant tests...

Example Outputs