diff --git a/cmd/dispatch/notes.go b/cmd/dispatch/notes.go index cf31a03..8064858 100644 --- a/cmd/dispatch/notes.go +++ b/cmd/dispatch/notes.go @@ -60,15 +60,28 @@ func runNotes(w io.Writer, args []string) error { func runNotesList(w io.Writer, args []string) error { jsonOut := false csvOut := false + tag := "" if len(args) > 0 && args[0] == "list" { args = args[1:] } - for _, arg := range args { - switch arg { + for i := 0; i < len(args); i++ { + arg := args[i] + name, inline, hasInline := splitFlag(arg) + switch name { case "--json": jsonOut = true case "--csv": csvOut = true + case "--tag": + if hasInline { + tag = inline + continue + } + if i+1 >= len(args) { + return fmt.Errorf("--tag requires a value") + } + tag = args[i+1] + i++ default: return fmt.Errorf("notes list does not take arguments, got %q", arg) } @@ -76,6 +89,7 @@ func runNotesList(w io.Writer, args []string) error { if jsonOut && csvOut { return fmt.Errorf("--json and --csv cannot be combined") } + tag = normalizeNotesTag(tag) cfg, err := configLoadFn() if err != nil { @@ -85,6 +99,9 @@ func runNotesList(w io.Writer, args []string) error { if err != nil { return err } + if tag != "" { + sessions = filterNotesSessionsByTag(cfg, sessions, tag) + } report := buildNotesReport(cfg, sessions) if jsonOut { enc := json.NewEncoder(w) @@ -98,6 +115,27 @@ func runNotesList(w io.Writer, args []string) error { return nil } +func normalizeNotesTag(tag string) string { + parts := config.ParseTags(tag) + if len(parts) == 0 { + return "" + } + return parts[0] +} + +func filterNotesSessionsByTag(cfg *config.Config, sessions []data.Session, tag string) []data.Session { + if cfg == nil || tag == "" { + return sessions + } + filtered := make([]data.Session, 0, len(sessions)) + for _, s := range sessions { + if cfg.HasTag(s.ID, tag) { + filtered = append(filtered, s) + } + } + return filtered +} + func runNotesGet(w io.Writer, args []string) error { if len(args) != 1 { return fmt.Errorf("notes get requires a session ID") diff --git a/cmd/dispatch/notes_test.go b/cmd/dispatch/notes_test.go index 6e2fd35..1ae13a5 100644 --- a/cmd/dispatch/notes_test.go +++ b/cmd/dispatch/notes_test.go @@ -33,6 +33,10 @@ func notedConfig() *config.Config { "b": "ready to ship", "z": "orphan note", } + cfg.SessionTags = map[string][]string{ + "a": {"bug", "work"}, + "b": {"docs"}, + } return cfg } @@ -82,6 +86,40 @@ func TestRunNotesListJSON(t *testing.T) { } } +func TestRunNotesListTagFilterText(t *testing.T) { + withConfigSeams(t, notedConfig()) + withNotesList(t, func(data.FilterOptions) ([]data.Session, error) { return notedSessions(), nil }) + + var buf bytes.Buffer + if err := runNotes(&buf, []string{"notes", "list", "--tag", "bug"}); err != nil { + t.Fatalf("runNotes tag filter: %v", err) + } + out := buf.String() + if !strings.Contains(out, "follow up") { + t.Fatalf("tagged note missing:\n%s", out) + } + if strings.Contains(out, "ready to ship") { + t.Fatalf("untagged note should be filtered out:\n%s", out) + } +} + +func TestRunNotesListTagFilterJSON(t *testing.T) { + withConfigSeams(t, notedConfig()) + withNotesList(t, func(data.FilterOptions) ([]data.Session, error) { return notedSessions(), nil }) + + var buf bytes.Buffer + if err := runNotes(&buf, []string{"notes", "--json", "--tag=missing"}); err != nil { + t.Fatalf("runNotes json tag filter: %v", err) + } + var report notesReport + if err := json.Unmarshal(buf.Bytes(), &report); err != nil { + t.Fatalf("invalid JSON: %v\n%s", err, buf.String()) + } + if report.TotalNotes != 0 || len(report.Notes) != 0 { + t.Fatalf("report = %+v, want no notes", report) + } +} + func TestRunNotesListCSV(t *testing.T) { withConfigSeams(t, notedConfig()) withNotesList(t, func(data.FilterOptions) ([]data.Session, error) { return notedSessions(), nil }) @@ -166,6 +204,7 @@ func TestRunNotesErrors(t *testing.T) { {"notes", "set", "ses-1", "--stdin", "extra"}, {"notes", "clear"}, {"notes", "list", "extra"}, + {"notes", "list", "--tag"}, {"notes", "list", "--json", "--csv"}, } { if err := runNotes(&bytes.Buffer{}, args); err == nil {