diff --git a/internal/cmd/convoy.go b/internal/cmd/convoy.go index 7a2de354..de401b7b 100644 --- a/internal/cmd/convoy.go +++ b/internal/cmd/convoy.go @@ -1260,6 +1260,7 @@ func runConvoyList(cmd *cobra.Command, args []string) error { convoys = filtered } + if convoyListJSON { enc := json.NewEncoder(os.Stdout) enc.SetIndent("", " ") @@ -1394,59 +1395,38 @@ func printConvoyTreeFromItems(townBeads string, convoys []convoyListItem) error 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() +// getEpicTitles fetches titles for the given epic IDs. +func getEpicTitles(epicIDs []string) map[string]string { + result := make(map[string]string) + if len(epicIDs) == 0 { + return result } - return nil + // Use bd show to get epic details (handles routing automatically) + args := append([]string{"show"}, epicIDs...) + args = append(args, "--json") + + showCmd := exec.Command("bd", args...) + var stdout bytes.Buffer + showCmd.Stdout = &stdout + + if err := showCmd.Run(); err != nil { + return result + } + + var issues []struct { + ID string `json:"id"` + Title string `json:"title"` + } + if err := json.Unmarshal(stdout.Bytes(), &issues); err != nil { + return result + } + + for _, issue := range issues { + result[issue.ID] = issue.Title + } + + return result } func formatConvoyStatus(status string) string { @@ -1462,6 +1442,61 @@ func formatConvoyStatus(status string) string { } } +// getConvoyParentEpics returns a map from convoy ID to parent epic ID. +// Convoys link to epics via child_of dependency type. +// Uses a single batched query for efficiency. +func getConvoyParentEpics(townBeads string, convoyIDs []string) map[string]string { + result := make(map[string]string) + if len(convoyIDs) == 0 { + return result + } + + dbPath := filepath.Join(townBeads, "beads.db") + + // Build IN clause with properly escaped IDs + var quotedIDs []string + for _, id := range convoyIDs { + safeID := strings.ReplaceAll(id, "'", "''") + quotedIDs = append(quotedIDs, fmt.Sprintf("'%s'", safeID)) + } + inClause := strings.Join(quotedIDs, ", ") + + // Query child_of dependencies for all convoys at once + query := fmt.Sprintf( + `SELECT issue_id, depends_on_id FROM dependencies WHERE issue_id IN (%s) AND type = 'child_of'`, + inClause) + + queryCmd := exec.Command("sqlite3", "-json", dbPath, query) + var stdout bytes.Buffer + queryCmd.Stdout = &stdout + + if err := queryCmd.Run(); err != nil { + return result + } + + var deps []struct { + IssueID string `json:"issue_id"` + DependsOnID string `json:"depends_on_id"` + } + if err := json.Unmarshal(stdout.Bytes(), &deps); err != nil { + return result + } + + for _, dep := range deps { + epicID := dep.DependsOnID + // Handle external reference format: external:rig:issue-id + if strings.HasPrefix(epicID, "external:") { + parts := strings.SplitN(epicID, ":", 3) + if len(parts) == 3 { + epicID = parts[2] // Extract the actual issue ID + } + } + result[dep.IssueID] = epicID + } + + return result +} + // trackedIssueInfo holds info about an issue being tracked by a convoy. type trackedIssueInfo struct { ID string `json:"id"`