Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 50 additions & 29 deletions cmd/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cmd
import (
"fmt"
"os"
"sort"
"strings"
"text/tabwriter"
"time"
Expand Down Expand Up @@ -41,14 +42,15 @@ func init() {

// agentStatusEntry holds one row of the agent status output.
type agentStatusEntry struct {
Worktree string `json:"worktree"`
SessionID string `json:"session_id"`
Status string `json:"status"`
Size string `json:"size"`
Model string `json:"model"`
InputTokens string `json:"input_tokens"`
OutputTokens string `json:"output_tokens"`
LastActive string `json:"last_active"`
Worktree string `json:"worktree"`
SessionID string `json:"session_id"`
Status string `json:"status"`
Size string `json:"size"`
Model string `json:"model"`
InputTokens string `json:"input_tokens"`
OutputTokens string `json:"output_tokens"`
LastActive string `json:"last_active"`
lastActiveEpoch int64 // unexported, for sorting only
}

func runAgentStatus(cmd *cobra.Command, args []string) error {
Expand Down Expand Up @@ -97,18 +99,24 @@ func runAgentStatus(cmd *cobra.Command, args []string) error {
lastActive := time.Unix(s.Modified, 0)

entry := agentStatusEntry{
Worktree: ui.ShortenHome(wt.Path, home),
SessionID: s.ID,
Status: status,
Size: s.SizeStr,
Model: session.ShortenModel(model),
InputTokens: session.FormatTokenCount(tokens.InputTokens),
OutputTokens: session.FormatTokenCount(tokens.OutputTokens),
LastActive: session.FormatAge(lastActive),
Worktree: ui.ShortenHome(wt.Path, home),
SessionID: s.ID,
Status: status,
Size: s.SizeStr,
Model: session.ShortenModel(model),
InputTokens: session.FormatTokenCount(tokens.InputTokens),
OutputTokens: session.FormatTokenCount(tokens.OutputTokens),
LastActive: session.FormatAge(lastActive),
lastActiveEpoch: s.Modified,
}
entries = append(entries, entry)
}

// Sort by last active (most recent first)
sort.Slice(entries, func(i, j int) bool {
return entries[i].lastActiveEpoch > entries[j].lastActiveEpoch
})

if jsonFlag {
printJSON(entries)
return nil
Expand All @@ -127,28 +135,33 @@ func runAgentStatus(cmd *cobra.Command, args []string) error {
ui.SectionHeader("Agent Sessions")
fmt.Println()

// Compute max worktree name width for alignment
maxWT := len("WORKTREE")
for _, e := range entries {
name := worktreeDisplayName(e.Worktree)
if len(name) > maxWT {
maxWT = len(name)
}
}

// Use tabwriter only for plain-text columns, then append colored status after
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "WORKTREE\tSTATUS\tSIZE\tMODEL\tTOKENS (IN/OUT)\tLAST ACTIVE")
fmt.Fprintln(w, "--------\t------\t----\t-----\t---------------\t-----------")
fmt.Fprintf(w, "%-7s %-*s %-7s %-6s %-12s %s\n", "STATUS", maxWT, "WORKTREE", "SIZE", "MODEL", "TOKENS(I/O)", "LAST ACTIVE")
fmt.Fprintf(w, "%-7s %-*s %-7s %-6s %-12s %s\n", "───────", maxWT, strings.Repeat("─", maxWT), "───────", "──────", "────────────", "───────────")

for _, e := range entries {
statusStr := e.Status
statusStr := fmt.Sprintf("%-7s", e.Status)
if e.Status == "running" {
statusStr = ui.GreenText("running")
statusStr = ui.GreenText(statusStr)
} else {
statusStr = ui.DimText("stopped")
statusStr = ui.DimText(statusStr)
}

tokenStr := fmt.Sprintf("%s/%s", e.InputTokens, e.OutputTokens)
name := worktreeDisplayName(e.Worktree)

// Shorten worktree path for display
wtDisplay := e.Worktree
if parts := strings.Split(wtDisplay, "/"); len(parts) > 2 {
wtDisplay = "~/" + strings.Join(parts[len(parts)-2:], "/")
}

fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
wtDisplay, statusStr, e.Size, e.Model, tokenStr, e.LastActive)
fmt.Fprintf(w, "%s %-*s %-7s %-6s %-12s %s\n",
statusStr, maxWT, name, e.Size, e.Model, tokenStr, ui.DimText(e.LastActive))
}
w.Flush()

Expand All @@ -164,3 +177,11 @@ func runAgentStatus(cmd *cobra.Command, args []string) error {

return nil
}

// worktreeDisplayName extracts the last path component (worktree dir name) for display.
func worktreeDisplayName(path string) string {
if parts := strings.Split(path, "/"); len(parts) > 0 {
return parts[len(parts)-1]
}
return path
}
34 changes: 27 additions & 7 deletions cmd/resume.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ var (
resumeSession int
resumeList bool
resumeNoITerm bool
resumeModel string
)

// resumeWorktree handles the core resume logic for a matched worktree.
Expand Down Expand Up @@ -90,7 +91,11 @@ func resumeWorktree(wt worktree.Worktree, cmdName string, t terminal.Terminal) e
if resumeNoITerm {
fmt.Println()
fmt.Println(ui.BoldText("Resume command:"))
fmt.Printf(" cd %s && %s --resume %s\n", wt.Path, cfg.ClaudeBin, s.ID)
modelFlag := ""
if resumeModel != "" {
modelFlag = fmt.Sprintf(" --model %s", resumeModel)
}
fmt.Printf(" cd %s && %s%s --resume %s\n", wt.Path, cfg.ClaudeBin, modelFlag, s.ID)
fmt.Println()
fmt.Println(ui.DimText(fmt.Sprintf("Worktree: %s", shortPath)))
fmt.Println(ui.DimText(fmt.Sprintf("Session: %s (%s)", s.ModHuman, s.SizeStr)))
Expand All @@ -104,9 +109,12 @@ func resumeWorktree(wt worktree.Worktree, cmdName string, t terminal.Terminal) e
fmt.Printf(" Path: %s\n", ui.DimText(shortPath))
fmt.Printf(" Session: %s\n", ui.DimText(s.ID))
fmt.Printf(" Modified: %s\n", ui.DimText(fmt.Sprintf("%s (%s)", s.ModHuman, s.SizeStr)))
if resumeModel != "" {
fmt.Printf(" Model: %s\n", ui.CyanText(resumeModel))
}
fmt.Println()

if err := t.OpenTabWithResume(wt.Path, s.ID, cfg.ClaudeBin); err != nil {
if err := t.OpenTabWithResume(wt.Path, s.ID, cfg.ClaudeBin, resumeModel); err != nil {
return fmt.Errorf("opening %s tab: %w", t.Name(), err)
}

Expand All @@ -130,10 +138,14 @@ func openNewSession(wt worktree.Worktree, t terminal.Terminal) error {
if resumeNoITerm {
fmt.Println()
fmt.Println(ui.BoldText("Start command:"))
modelFlag := ""
if resumeModel != "" {
modelFlag = fmt.Sprintf(" --model %s", resumeModel)
}
if initialPrompt != "" {
fmt.Printf(" cd %s && %s %q\n", wt.Path, cfg.ClaudeBin, initialPrompt)
fmt.Printf(" cd %s && %s%s %q\n", wt.Path, cfg.ClaudeBin, modelFlag, initialPrompt)
} else {
fmt.Printf(" cd %s && %s\n", wt.Path, cfg.ClaudeBin)
fmt.Printf(" cd %s && %s%s\n", wt.Path, cfg.ClaudeBin, modelFlag)
}
fmt.Println()
fmt.Println(ui.DimText(fmt.Sprintf("Worktree: %s", shortPath)))
Expand All @@ -144,13 +156,20 @@ func openNewSession(wt worktree.Worktree, t terminal.Terminal) error {
fmt.Println(ui.BoldText(fmt.Sprintf("%s in new %s tab", action, t.Name())))
fmt.Printf(" Worktree: %s\n", ui.CyanText(wt.Name))
fmt.Printf(" Path: %s\n", ui.DimText(shortPath))
if resumeModel != "" {
fmt.Printf(" Model: %s\n", ui.CyanText(resumeModel))
}
fmt.Println()

var err error
if initialPrompt != "" {
err = t.OpenTabWithClaude(wt.Path, initialPrompt, cfg.ClaudeBin)
err = t.OpenTabWithClaude(wt.Path, initialPrompt, cfg.ClaudeBin, resumeModel)
} else {
err = t.OpenTab(wt.Path, cfg.ClaudeBin)
cmd := cfg.ClaudeBin
if resumeModel != "" {
cmd += fmt.Sprintf(" --model %s", resumeModel)
}
err = t.OpenTab(wt.Path, cmd)
}
if err != nil {
return fmt.Errorf("opening %s tab: %w", t.Name(), err)
Expand Down Expand Up @@ -222,11 +241,12 @@ func findWorktreeByName(term string) (*worktree.Worktree, error) {
return &matches[0], nil
}

// addResumeFlags adds the shared --session, --list, --no-iterm flags to a cobra command.
// addResumeFlags adds the shared --session, --list, --no-iterm, --model flags to a cobra command.
func addResumeFlags(cmd *cobra.Command) {
cmd.Flags().IntVarP(&resumeSession, "session", "s", 0, "Resume Nth session instead of most recent (1-based)")
cmd.Flags().BoolVarP(&resumeList, "list", "l", false, "List available sessions without resuming")
cmd.Flags().BoolVar(&resumeNoITerm, "no-terminal", false, "Print the resume command instead of opening terminal")
cmd.Flags().StringVarP(&resumeModel, "model", "m", "", "Claude model to use (e.g., sonnet, opus, haiku)")
}

// runReviewResume handles `zen review resume <pr-number>`.
Expand Down
18 changes: 16 additions & 2 deletions cmd/review.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,14 @@ var reviewDeleteCmd = &cobra.Command{
var (
reviewRepo string
reviewNoITerm bool
reviewModel string
reviewDeleteForce bool
)

func init() {
reviewCmd.Flags().StringVar(&reviewRepo, "repo", "", "Repository short name from config (auto-detected if omitted)")
reviewCmd.Flags().BoolVar(&reviewNoITerm, "no-terminal", false, "Create worktree only, don't open terminal tab")
reviewCmd.Flags().StringVarP(&reviewModel, "model", "m", "", "Claude model to use (e.g., sonnet, opus, haiku)")
addResumeFlags(reviewResumeCmd)
reviewDeleteCmd.Flags().BoolVarP(&reviewDeleteForce, "force", "f", false, "Skip confirmation")
reviewCmd.AddCommand(reviewResumeCmd)
Expand Down Expand Up @@ -104,6 +106,10 @@ func runReview(cmd *cobra.Command, args []string) error {
// If worktree already exists, resume it
if _, err := os.Stat(worktreePath); err == nil {
ui.LogInfo(fmt.Sprintf("Worktree already exists, resuming PR #%d...", prNumber))
// Pass model through to resume path
if reviewModel != "" {
resumeModel = reviewModel
}
return openReviewTab(worktreePath, worktreeName)
}

Expand Down Expand Up @@ -174,10 +180,18 @@ func runReview(cmd *cobra.Command, args []string) error {
fmt.Printf(" PR: #%d — %s\n", prNumber, details.Title)
fmt.Printf(" Author: %s\n", details.Author)

if reviewModel != "" {
fmt.Printf(" Model: %s\n", ui.CyanText(reviewModel))
}

if reviewNoITerm {
fmt.Println()
fmt.Println(ui.BoldText("Open manually:"))
fmt.Printf(" cd %s && %s \"/review-pr\"\n", worktreePath, cfg.ClaudeBin)
modelFlag := ""
if reviewModel != "" {
modelFlag = fmt.Sprintf(" --model %s", reviewModel)
}
fmt.Printf(" cd %s && %s%s \"/review-pr\"\n", worktreePath, cfg.ClaudeBin, modelFlag)
return nil
}

Expand All @@ -187,7 +201,7 @@ func runReview(cmd *cobra.Command, args []string) error {
return err
}

if err := term.OpenTabWithClaude(worktreePath, "/review-pr", cfg.ClaudeBin); err != nil {
if err := term.OpenTabWithClaude(worktreePath, "/review-pr", cfg.ClaudeBin, reviewModel); err != nil {
return fmt.Errorf("opening %s tab: %w", term.Name(), err)
}

Expand Down
Loading