diff --git a/README.md b/README.md index cd1e2a2..94d2024 100644 --- a/README.md +++ b/README.md @@ -354,7 +354,7 @@ dispatch info 0a1b2c3d --json dispatch info 0a1b2c3d --refs ``` -The summary covers the session's repository, branch, working directory, host type, turn and file counts, timestamps, and linked ref counts. Use `--refs` to include linked commit, PR, and issue values, or `--json` for scripting. The session ID accepts the same prefix shorthand as `open`. +The summary covers the session's repository, branch, working directory, host type, saved alias, tags, note, turn and file counts, timestamps, and linked ref counts. Use `--refs` to include linked commit, PR, and issue values, or `--json` for scripting. The session ID accepts the same prefix shorthand as `open`. ### Aliases diff --git a/cmd/dispatch/info.go b/cmd/dispatch/info.go index 857b79a..75a3163 100644 --- a/cmd/dispatch/info.go +++ b/cmd/dispatch/info.go @@ -7,6 +7,7 @@ import ( "io" "strings" + "github.com/jongio/dispatch/internal/config" "github.com/jongio/dispatch/internal/data" ) @@ -28,6 +29,9 @@ type sessionInfo struct { CreatedAt string `json:"created_at,omitempty"` UpdatedAt string `json:"updated_at,omitempty"` LastActiveAt string `json:"last_active_at,omitempty"` + Alias string `json:"alias,omitempty"` + Tags []string `json:"tags,omitempty"` + Note string `json:"note,omitempty"` Turns int `json:"turns"` Files int `json:"files"` Checkpoints int `json:"checkpoints"` @@ -64,6 +68,11 @@ func runInfo(w io.Writer, args []string) error { } info := buildSessionInfo(detail) + cfg, err := configLoadFn() + if err != nil { + return fmt.Errorf("loading config: %w", err) + } + addInfoAnnotations(&info, cfg) if includeRefs { addInfoRefs(&info, detail.Refs) } @@ -136,6 +145,17 @@ func buildSessionInfo(detail *data.SessionDetail) sessionInfo { return info } +func addInfoAnnotations(info *sessionInfo, cfg *config.Config) { + if info == nil || cfg == nil { + return + } + info.Alias = cfg.AliasFor(info.ID) + if tags := cfg.TagsFor(info.ID); len(tags) > 0 { + info.Tags = append([]string(nil), tags...) + } + info.Note = cfg.SessionNotes[info.ID] +} + func addInfoRefs(info *sessionInfo, refs []data.SessionRef) { info.Refs = &infoRefs{ Commits: []string{}, @@ -180,6 +200,11 @@ func writeInfoText(w io.Writer, info sessionInfo) error { writeField("Branch:", info.Branch) writeField("Directory:", info.Directory) writeField("Host:", info.HostType) + writeField("Alias:", info.Alias) + if len(info.Tags) > 0 { + writeField("Tags:", strings.Join(info.Tags, ", ")) + } + writeField("Note:", oneLine(info.Note)) writeField("Created:", info.CreatedAt) writeField("Updated:", info.UpdatedAt) writeField("Last active:", info.LastActiveAt) @@ -215,6 +240,10 @@ func pluralize(n int, singular, plural string) string { return fmt.Sprintf("%d %s", n, plural) } +func oneLine(value string) string { + return strings.Join(strings.Fields(value), " ") +} + func formatRefList(values []string) string { if len(values) == 0 { return "-" diff --git a/cmd/dispatch/info_test.go b/cmd/dispatch/info_test.go index 83f80c4..71dc5cd 100644 --- a/cmd/dispatch/info_test.go +++ b/cmd/dispatch/info_test.go @@ -8,6 +8,7 @@ import ( "strings" "testing" + "github.com/jongio/dispatch/internal/config" "github.com/jongio/dispatch/internal/data" "github.com/jongio/dispatch/internal/update" ) @@ -17,7 +18,12 @@ func withInfoDetail(t *testing.T, fn func(string) (*data.SessionDetail, error)) t.Helper() prev := infoGetDetailFn infoGetDetailFn = fn - t.Cleanup(func() { infoGetDetailFn = prev }) + prevConfig := configLoadFn + configLoadFn = func() (*config.Config, error) { return &config.Config{}, nil } + t.Cleanup(func() { + infoGetDetailFn = prev + configLoadFn = prevConfig + }) } func infoSampleDetail() *data.SessionDetail { @@ -156,6 +162,47 @@ func TestRunInfo_TextWithRefs(t *testing.T) { } } +func TestRunInfo_IncludesAnnotations(t *testing.T) { + withInfoDetail(t, func(string) (*data.SessionDetail, error) { + return infoSampleDetail(), nil + }) + prevConfig := configLoadFn + configLoadFn = func() (*config.Config, error) { + return &config.Config{ + SessionAliases: map[string]string{"ses-info-1": "widget"}, + SessionTags: map[string][]string{"ses-info-1": {"work", "ui"}}, + SessionNotes: map[string]string{"ses-info-1": "follow up\nwith tests"}, + }, nil + } + t.Cleanup(func() { configLoadFn = prevConfig }) + + var text bytes.Buffer + if err := runInfo(&text, []string{"info", "ses-info-1"}); err != nil { + t.Fatalf("unexpected error: %v", err) + } + for _, want := range []string{ + "Alias: widget", + "Tags: work, ui", + "Note: follow up with tests", + } { + if !strings.Contains(text.String(), want) { + t.Errorf("text output missing %q, got:\n%s", want, text.String()) + } + } + + var jsonOut bytes.Buffer + if err := runInfo(&jsonOut, []string{"info", "ses-info-1", "--json"}); err != nil { + t.Fatalf("unexpected JSON error: %v", err) + } + var got sessionInfo + if err := json.Unmarshal(jsonOut.Bytes(), &got); err != nil { + t.Fatalf("output is not valid JSON: %v\n%s", err, jsonOut.String()) + } + if got.Alias != "widget" || strings.Join(got.Tags, ",") != "work,ui" || got.Note != "follow up\nwith tests" { + t.Errorf("annotations = alias %q tags %+v note %q", got.Alias, got.Tags, got.Note) + } +} + func TestRunInfo_TextOmitsEmptyFields(t *testing.T) { withInfoDetail(t, func(string) (*data.SessionDetail, error) { return &data.SessionDetail{Session: data.Session{ID: "bare"}}, nil