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
78 changes: 69 additions & 9 deletions cmd/dispatch/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,22 @@ type infoRefs struct {
Issues []string `json:"issues"`
}

type infoOutputFormat string

const (
infoFormatText infoOutputFormat = "text"
infoFormatJSON infoOutputFormat = "json"
infoFormatMarkdown infoOutputFormat = "markdown"
)

// runInfo prints a concise summary of a single session as text, or as JSON
// with --json. The --refs flag adds linked reference values.
func runInfo(w io.Writer, args []string) error {
if w == nil {
w = io.Discard
}

id, asJSON, includeRefs, err := parseInfoArgs(args)
id, format, includeRefs, err := parseInfoArgs(args)
if err != nil {
return err
}
Expand All @@ -76,41 +84,54 @@ func runInfo(w io.Writer, args []string) error {
if includeRefs {
addInfoRefs(&info, detail.Refs)
}
if asJSON {
switch format {
case infoFormatJSON:
return writeInfoJSON(w, info)
case infoFormatMarkdown:
return writeInfoMarkdown(w, info)
default:
return writeInfoText(w, info)
}
return writeInfoText(w, info)
}

// parseInfoArgs extracts the session ID and flags from the info
// subcommand arguments. args[0] is expected to be "info".
func parseInfoArgs(args []string) (id string, asJSON, includeRefs bool, err error) {
func parseInfoArgs(args []string) (id string, format infoOutputFormat, includeRefs bool, err error) {
rest := args
if len(rest) > 0 {
rest = rest[1:] // drop the "info" token
}

format = infoFormatText
var positionals []string
for _, arg := range rest {
switch {
case arg == "--json":
asJSON = true
if format == infoFormatMarkdown {
return "", "", false, errors.New("--json and --markdown cannot be combined")
}
format = infoFormatJSON
case arg == "--markdown":
if format == infoFormatJSON {
return "", "", false, errors.New("--json and --markdown cannot be combined")
}
format = infoFormatMarkdown
case arg == "--refs":
includeRefs = true
case strings.HasPrefix(arg, "-"):
return "", false, false, fmt.Errorf("unknown flag: %s", arg)
return "", "", false, fmt.Errorf("unknown flag: %s", arg)
default:
positionals = append(positionals, arg)
}
}

switch len(positionals) {
case 0:
return "", false, false, errors.New("info requires a session ID")
return "", "", false, errors.New("info requires a session ID")
case 1:
return positionals[0], asJSON, includeRefs, nil
return positionals[0], format, includeRefs, nil
default:
return "", false, false, fmt.Errorf("info accepts a single session ID, got %d arguments", len(positionals))
return "", "", false, fmt.Errorf("info accepts a single session ID, got %d arguments", len(positionals))
}
}

Expand Down Expand Up @@ -223,6 +244,45 @@ func writeInfoText(w io.Writer, info sessionInfo) error {
return err
}

func writeInfoMarkdown(w io.Writer, info sessionInfo) error {
var b strings.Builder
fmt.Fprintf(&b, "## Session `%s`\n\n", markdownCell(info.ID))

writeRow := func(label, value string) {
if value != "" {
fmt.Fprintf(&b, "| %s | %s |\n", label, markdownCell(value))
}
}

fmt.Fprintln(&b, "| Field | Value |")
fmt.Fprintln(&b, "|---|---|")
writeRow("Summary", info.Summary)
writeRow("Repository", info.Repository)
writeRow("Branch", info.Branch)
writeRow("Directory", info.Directory)
writeRow("Host", info.HostType)
writeRow("Alias", info.Alias)
if len(info.Tags) > 0 {
writeRow("Tags", strings.Join(info.Tags, ", "))
}
writeRow("Note", oneLine(info.Note))
writeRow("Created", info.CreatedAt)
writeRow("Updated", info.UpdatedAt)
writeRow("Last active", info.LastActiveAt)
fmt.Fprintf(&b, "| Turns | %d |\n", info.Turns)
fmt.Fprintf(&b, "| Files | %d |\n", info.Files)
fmt.Fprintf(&b, "| Checkpoints | %d |\n", info.Checkpoints)
writeRow("Refs", formatRefCounts(info))
if info.Refs != nil {
writeRow("Commits", formatRefList(info.Refs.Commits))
writeRow("PRs", formatRefList(info.Refs.PRs))
writeRow("Issues", formatRefList(info.Refs.Issues))
}

_, err := io.WriteString(w, b.String())
return err
}

// formatRefCounts renders the reference breakdown as a compact summary such as
// "3 commits, 1 pr, 0 issues", using singular labels when a count is one.
func formatRefCounts(info sessionInfo) string {
Expand Down
43 changes: 34 additions & 9 deletions cmd/dispatch/info_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,23 +57,25 @@ func TestParseInfoArgs(t *testing.T) {
name string
args []string
wantID string
wantJSON bool
wantFmt infoOutputFormat
wantRefs bool
wantErr bool
}{
{name: "id only", args: []string{"info", "abc"}, wantID: "abc"},
{name: "id with json", args: []string{"info", "abc", "--json"}, wantID: "abc", wantJSON: true},
{name: "json before id", args: []string{"info", "--json", "abc"}, wantID: "abc", wantJSON: true},
{name: "id with refs", args: []string{"info", "abc", "--refs"}, wantID: "abc", wantRefs: true},
{name: "json with refs", args: []string{"info", "--json", "--refs", "abc"}, wantID: "abc", wantJSON: true, wantRefs: true},
{name: "id only", args: []string{"info", "abc"}, wantID: "abc", wantFmt: infoFormatText},
{name: "id with json", args: []string{"info", "abc", "--json"}, wantID: "abc", wantFmt: infoFormatJSON},
{name: "json before id", args: []string{"info", "--json", "abc"}, wantID: "abc", wantFmt: infoFormatJSON},
{name: "id with markdown", args: []string{"info", "abc", "--markdown"}, wantID: "abc", wantFmt: infoFormatMarkdown},
{name: "id with refs", args: []string{"info", "abc", "--refs"}, wantID: "abc", wantFmt: infoFormatText, wantRefs: true},
{name: "json with refs", args: []string{"info", "--json", "--refs", "abc"}, wantID: "abc", wantFmt: infoFormatJSON, wantRefs: true},
{name: "missing id", args: []string{"info"}, wantErr: true},
{name: "two ids", args: []string{"info", "a", "b"}, wantErr: true},
{name: "unknown flag", args: []string{"info", "abc", "--nope"}, wantErr: true},
{name: "json markdown conflict", args: []string{"info", "abc", "--json", "--markdown"}, wantErr: true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
id, asJSON, includeRefs, err := parseInfoArgs(tt.args)
id, format, includeRefs, err := parseInfoArgs(tt.args)
if tt.wantErr {
if err == nil {
t.Fatal("expected an error")
Expand All @@ -86,8 +88,8 @@ func TestParseInfoArgs(t *testing.T) {
if id != tt.wantID {
t.Errorf("id = %q, want %q", id, tt.wantID)
}
if asJSON != tt.wantJSON {
t.Errorf("asJSON = %v, want %v", asJSON, tt.wantJSON)
if format != tt.wantFmt {
t.Errorf("format = %q, want %q", format, tt.wantFmt)
}
if includeRefs != tt.wantRefs {
t.Errorf("includeRefs = %v, want %v", includeRefs, tt.wantRefs)
Expand Down Expand Up @@ -270,6 +272,29 @@ func TestRunInfo_JSONWithRefs(t *testing.T) {
}
}

func TestRunInfo_MarkdownWithRefs(t *testing.T) {
withInfoDetail(t, func(string) (*data.SessionDetail, error) {
return infoSampleDetail(), nil
})

var buf bytes.Buffer
if err := runInfo(&buf, []string{"info", "ses-info-1", "--markdown", "--refs"}); err != nil {
t.Fatalf("unexpected error: %v", err)
}
out := buf.String()
for _, want := range []string{
"## Session `ses-info-1`",
"| Summary | Fix the widget |",
"| Repository | jongio/dispatch |",
"| Turns | 5 |",
"| PRs | 42, 43 |",
} {
if !strings.Contains(out, want) {
t.Errorf("markdown output missing %q, got:\n%s", want, out)
}
}
}

func TestRunInfo_NotFound(t *testing.T) {
withInfoDetail(t, func(string) (*data.SessionDetail, error) {
return nil, nil // loader returns (nil, nil) when the ID is unknown
Expand Down