diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 360d245..57a2be3 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -1,11 +1,13 @@ package cli import ( + "context" "fmt" "os" "path/filepath" "sort" "strings" + "time" "github.com/sendbird/ccx/internal/clauderegistry" "github.com/sendbird/ccx/internal/extract" @@ -20,6 +22,7 @@ var Commands = []struct { Desc string }{ {"urls", "List URLs from the Claude session (interactive on TTY)"}, + {"refs", "List PR/Jira references with resolved status (interactive on TTY)"}, {"files", "List file paths touched by the session (interactive on TTY)"}, {"changes", "List file changes made by the session (interactive on TTY)"}, {"images", "List image paths from the session (interactive on TTY)"}, @@ -75,6 +78,8 @@ func runPlain(command, filePath, sessID, claudeDir string) error { switch command { case "urls": return printItems(extract.SessionURLs(filePath), "urls") + case "refs": + return printRefs(filePath) case "files": return printItems(extract.SessionFilePaths(filePath), "files") case "changes": @@ -99,6 +104,8 @@ func runInteractive(command, filePath, sessID, claudeDir string) (*RunResult, er switch command { case "urls": items = extractURLsWithContext(entries, sessID) + case "refs": + items = extractRefsWithContext(entries, sessID) case "files": items = extractFilesWithContext(entries, sessID) case "changes": @@ -145,6 +152,8 @@ func printHelp() { fmt.Fprintf(os.Stderr, " ccx urls Interactive URL picker\n") fmt.Fprintf(os.Stderr, " ccx urls --plain Plain tab-separated output\n") fmt.Fprintf(os.Stderr, " ccx urls | fzf Pipe to fzf (auto plain)\n") + fmt.Fprintf(os.Stderr, " ccx refs Interactive PR/Jira reference picker\n") + fmt.Fprintf(os.Stderr, " ccx refs --plain PR/Jira refs with resolved status (tab-separated)\n") fmt.Fprintf(os.Stderr, " ccx files Interactive file picker\n") fmt.Fprintf(os.Stderr, " ccx changes Interactive changed-files picker\n") fmt.Fprintf(os.Stderr, " ccx images Interactive image picker\n") @@ -193,6 +202,25 @@ func printItems(items []extract.Item, kind string) error { return nil } +func printRefs(filePath string) error { + refs := session.ExtractSessionRefsFromFile(filePath) + if len(refs) == 0 { + return fmt.Errorf("no PR/Jira references found in session") + } + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + refs = session.ResolveRefs(ctx, refs) + for _, r := range refs { + kind := strings.ToUpper(string(r.Kind)) + if len(kind) < 4 { + kind += strings.Repeat(" ", 4-len(kind)) + } + status := session.RefStatusText(r) + fmt.Fprintf(os.Stdout, "%s\t%s\t%s\t%s\n", kind, r.Label, status, r.URL) + } + return nil +} + func printChanges(items []extract.ChangeItem) error { if len(items) == 0 { return fmt.Errorf("no changes found in session") diff --git a/internal/cli/context.go b/internal/cli/context.go index 989e587..bdcb029 100644 --- a/internal/cli/context.go +++ b/internal/cli/context.go @@ -364,6 +364,20 @@ func extractURLsWithContext(entries []session.Entry, sessID string) []PickerItem return items } +// extractRefsWithContext returns only the session's PR and Jira URL references, +// reusing the URL extractor and filtering to the pr/jira categories. The refs +// picker resolves their status inline the same way the urls picker does. +func extractRefsWithContext(entries []session.Entry, sessID string) []PickerItem { + all := extractURLsWithContext(entries, sessID) + refs := make([]PickerItem, 0, len(all)) + for _, it := range all { + if it.Item.Category == "pr" || it.Item.Category == "jira" { + refs = append(refs, it) + } + } + return refs +} + func extractFilesWithContext(entries []session.Entry, sessID string) []PickerItem { index := make(map[string]int) var items []PickerItem diff --git a/internal/cli/picker.go b/internal/cli/picker.go index 2e266b5..aee07a9 100644 --- a/internal/cli/picker.go +++ b/internal/cli/picker.go @@ -108,7 +108,7 @@ func (m pickerModel) Init() tea.Cmd { // reuses session.ResolveRef so results are TTL-cached and share the bounded // concurrency used elsewhere. Non-urls kinds have no PR/Jira links → no-op. func (m pickerModel) resolveRefsCmd() tea.Cmd { - if m.kind != "urls" { + if m.kind != "urls" && m.kind != "refs" { return nil } var cmds []tea.Cmd @@ -807,6 +807,8 @@ func (m pickerModel) View() string { switch m.kind { case "urls": actions = "↵:jump o:open e:$EDITOR" + case "refs": + actions = "↵:jump o:open" case "files": actions = "↵:jump e:$EDITOR" case "changes": @@ -824,6 +826,8 @@ func (m pickerModel) View() string { switch m.kind { case "urls": filterHints = hint.Render("is:") + dim.Render("pr gh github jira slack other") + " " + hint.Render("role:") + dim.Render("user asst") + case "refs": + filterHints = hint.Render("is:") + dim.Render("pr jira") + " " + hint.Render("role:") + dim.Render("user asst") case "files": filterHints = hint.Render("is:") + dim.Render("read write edit glob grep tool") + " " + hint.Render("role:") + dim.Render("user asst") case "changes": diff --git a/main.go b/main.go index f177eed..41d3352 100644 --- a/main.go +++ b/main.go @@ -225,7 +225,7 @@ func main() { os.Exit(1) } os.Exit(0) - case "urls", "files", "changes", "images", "conversation", "info", "help": + case "urls", "refs", "files", "changes", "images", "conversation", "info", "help": subcmd := os.Args[1] fs := flag.NewFlagSet(subcmd, flag.ExitOnError) plain := fs.Bool("plain", false, "force plain text output (no interactive picker)")