feat(convoy): Add numbered shortcuts and interactive TUI (gt-fo0qa)

- Add numbered prefixes to `gt convoy list` output (1. 2. 3. ...)
- Support numeric shortcuts in `gt convoy status <n>`
- Add `-i/--interactive` flag for expandable tree view TUI
- New internal/tui/convoy package with bubbletea-based UI
  - j/k navigation, enter to expand/collapse
  - 1-9 to jump directly to convoy

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gastown/crew/jack
2025-12-31 12:58:56 -08:00
committed by Steve Yegge
parent 1c82d51408
commit 7d6ea09efe
4 changed files with 628 additions and 10 deletions

View File

@@ -9,10 +9,13 @@ import (
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
tea "github.com/charmbracelet/bubbletea"
"github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/style"
"github.com/steveyegge/gastown/internal/tui/convoy"
"github.com/steveyegge/gastown/internal/workspace"
)
@@ -25,19 +28,25 @@ func generateShortID() string {
// Convoy command flags
var (
convoyMolecule string
convoyNotify string
convoyStatusJSON bool
convoyListJSON bool
convoyListStatus string
convoyListAll bool
convoyMolecule string
convoyNotify string
convoyStatusJSON bool
convoyListJSON bool
convoyListStatus string
convoyListAll bool
convoyInteractive bool
)
var convoyCmd = &cobra.Command{
Use: "convoy",
GroupID: GroupWork,
Short: "Track batches of work across rigs",
RunE: requireSubcommand,
RunE: func(cmd *cobra.Command, args []string) error {
if convoyInteractive {
return runConvoyTUI()
}
return requireSubcommand(cmd, args)
},
Long: `Manage convoys - the primary unit for tracking batched work.
A convoy is a persistent tracking unit that monitors related issues across
@@ -134,6 +143,9 @@ func init() {
convoyListCmd.Flags().StringVar(&convoyListStatus, "status", "", "Filter by status (open, closed)")
convoyListCmd.Flags().BoolVar(&convoyListAll, "all", false, "Show all convoys (open and closed)")
// Interactive TUI flag (on parent command)
convoyCmd.Flags().BoolVarP(&convoyInteractive, "interactive", "i", false, "Interactive tree view")
// Add subcommands
convoyCmd.AddCommand(convoyCreateCmd)
convoyCmd.AddCommand(convoyStatusCmd)
@@ -329,6 +341,15 @@ func runConvoyStatus(cmd *cobra.Command, args []string) error {
convoyID := args[0]
// Check if it's a numeric shortcut (e.g., "1" instead of "hq-cv-xyz")
if n, err := strconv.Atoi(convoyID); err == nil && n > 0 {
resolved, err := resolveConvoyNumber(townBeads, n)
if err != nil {
return err
}
convoyID = resolved
}
// Get convoy details
showArgs := []string{"show", convoyID, "--json"}
showCmd := exec.Command("bd", showArgs...)
@@ -518,11 +539,11 @@ func runConvoyList(cmd *cobra.Command, args []string) error {
}
fmt.Printf("%s\n\n", style.Bold.Render("Convoys"))
for _, c := range convoys {
for i, c := range convoys {
status := formatConvoyStatus(c.Status)
fmt.Printf(" 🚚 %s: %s %s\n", c.ID, c.Title, status)
fmt.Printf(" %d. 🚚 %s: %s %s\n", i+1, c.ID, c.Title, status)
}
fmt.Printf("\nUse 'gt convoy status <id>' for detailed view.\n")
fmt.Printf("\nUse 'gt convoy status <id>' or 'gt convoy status <n>' for detailed view.\n")
return nil
}
@@ -703,3 +724,44 @@ func getIssueDetails(issueID string) *issueDetails {
IssueType: issues[0].IssueType,
}
}
// runConvoyTUI launches the interactive convoy TUI.
func runConvoyTUI() error {
townBeads, err := getTownBeadsDir()
if err != nil {
return err
}
m := convoy.New(townBeads)
p := tea.NewProgram(m, tea.WithAltScreen())
_, err = p.Run()
return err
}
// resolveConvoyNumber converts a numeric shortcut (1, 2, 3...) to a convoy ID.
// Numbers correspond to the order shown in 'gt convoy list'.
func resolveConvoyNumber(townBeads string, n int) (string, error) {
// Get convoy list (same query as runConvoyList)
listArgs := []string{"list", "--type=convoy", "--json"}
listCmd := exec.Command("bd", listArgs...)
listCmd.Dir = townBeads
var stdout bytes.Buffer
listCmd.Stdout = &stdout
if err := listCmd.Run(); err != nil {
return "", fmt.Errorf("listing convoys: %w", err)
}
var convoys []struct {
ID string `json:"id"`
}
if err := json.Unmarshal(stdout.Bytes(), &convoys); err != nil {
return "", fmt.Errorf("parsing convoy list: %w", err)
}
if n < 1 || n > len(convoys) {
return "", fmt.Errorf("convoy %d not found (have %d convoys)", n, len(convoys))
}
return convoys[n-1].ID, nil
}