feat(convoy): Add --tree flag to show convoy + child status tree

Shows convoys with their tracked issues in a tree format:
🚚 hq-cv-abc: Convoy Name (3/5)
├── ✓ gt-123: Closed issue
├── ▶ gt-456: In progress issue
└── ○ gt-789: Pending issue

Makes convoy progress visible at a glance without running 'gt convoy status' on each one.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
dementus
2026-01-02 17:16:39 -08:00
committed by Steve Yegge
parent 8517ff0650
commit 0bcd8acecb

View File

@@ -65,6 +65,7 @@ var (
convoyListJSON bool convoyListJSON bool
convoyListStatus string convoyListStatus string
convoyListAll bool convoyListAll bool
convoyListTree bool
convoyInteractive bool convoyInteractive bool
) )
@@ -144,6 +145,7 @@ Examples:
gt convoy list # Open convoys only (default) gt convoy list # Open convoys only (default)
gt convoy list --all # All convoys (open + closed) gt convoy list --all # All convoys (open + closed)
gt convoy list --status=closed # Recently landed gt convoy list --status=closed # Recently landed
gt convoy list --tree # Show convoy + child status tree
gt convoy list --json`, gt convoy list --json`,
RunE: runConvoyList, RunE: runConvoyList,
} }
@@ -187,6 +189,7 @@ func init() {
convoyListCmd.Flags().BoolVar(&convoyListJSON, "json", false, "Output as JSON") convoyListCmd.Flags().BoolVar(&convoyListJSON, "json", false, "Output as JSON")
convoyListCmd.Flags().StringVar(&convoyListStatus, "status", "", "Filter by status (open, closed)") convoyListCmd.Flags().StringVar(&convoyListStatus, "status", "", "Filter by status (open, closed)")
convoyListCmd.Flags().BoolVar(&convoyListAll, "all", false, "Show all convoys (open and closed)") convoyListCmd.Flags().BoolVar(&convoyListAll, "all", false, "Show all convoys (open and closed)")
convoyListCmd.Flags().BoolVar(&convoyListTree, "tree", false, "Show convoy + child status tree")
// Interactive TUI flag (on parent command) // Interactive TUI flag (on parent command)
convoyCmd.Flags().BoolVarP(&convoyInteractive, "interactive", "i", false, "Interactive tree view") convoyCmd.Flags().BoolVarP(&convoyInteractive, "interactive", "i", false, "Interactive tree view")
@@ -729,6 +732,11 @@ func runConvoyList(cmd *cobra.Command, args []string) error {
return nil return nil
} }
// Tree view: show convoys with their child issues
if convoyListTree {
return printConvoyTree(townBeads, convoys)
}
fmt.Printf("%s\n\n", style.Bold.Render("Convoys")) fmt.Printf("%s\n\n", style.Bold.Render("Convoys"))
for i, c := range convoys { for i, c := range convoys {
status := formatConvoyStatus(c.Status) status := formatConvoyStatus(c.Status)
@@ -739,6 +747,61 @@ func runConvoyList(cmd *cobra.Command, args []string) error {
return nil return nil
} }
// printConvoyTree displays convoys with their child issues in a tree format.
func printConvoyTree(townBeads string, convoys []struct {
ID string `json:"id"`
Title string `json:"title"`
Status string `json:"status"`
CreatedAt string `json:"created_at"`
}) error {
for _, c := range convoys {
// Get tracked issues for this convoy
tracked := getTrackedIssues(townBeads, c.ID)
// Count completed
completed := 0
for _, t := range tracked {
if t.Status == "closed" {
completed++
}
}
// Print convoy header with progress
total := len(tracked)
progress := ""
if total > 0 {
progress = fmt.Sprintf(" (%d/%d)", completed, total)
}
fmt.Printf("🚚 %s: %s%s\n", c.ID, c.Title, progress)
// Print tracked issues as tree children
for i, t := range tracked {
// Determine tree connector
isLast := i == len(tracked)-1
connector := "├──"
if isLast {
connector = "└──"
}
// Status symbol: ✓ closed, ▶ in_progress/hooked, ○ other
status := "○"
switch t.Status {
case "closed":
status = "✓"
case "in_progress", "hooked":
status = "▶"
}
fmt.Printf("%s %s %s: %s\n", connector, status, t.ID, t.Title)
}
// Add blank line between convoys
fmt.Println()
}
return nil
}
func formatConvoyStatus(status string) string { func formatConvoyStatus(status string) string {
switch status { switch status {
case "open": case "open":