From dd2520f2fcc83099d376e36bc16442fb14ba7fda Mon Sep 17 00:00:00 2001 From: Jon Gallant <2163001+jongio@users.noreply.github.com> Date: Sun, 26 Jul 2026 10:53:08 -0700 Subject: [PATCH] Filter notes by tag Closes #373 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f892a21-0f5d-41e7-aebb-afbda057615d --- cmd/dispatch/notes.go | 42 ++++++++++++++++++++++++++++++++++++-- cmd/dispatch/notes_test.go | 39 +++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/cmd/dispatch/notes.go b/cmd/dispatch/notes.go index 125ef04..85ce1fa 100644 --- a/cmd/dispatch/notes.go +++ b/cmd/dispatch/notes.go @@ -58,17 +58,31 @@ func runNotes(w io.Writer, args []string) error { func runNotesList(w io.Writer, args []string) error { jsonOut := 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 "--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) } } + tag = normalizeNotesTag(tag) cfg, err := configLoadFn() if err != nil { @@ -78,6 +92,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) @@ -88,6 +105,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 4ef3272..2c0be87 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 TestRunNotesGetSetClear(t *testing.T) { cfg := withConfigSeams(t, config.Default()) @@ -143,6 +181,7 @@ func TestRunNotesErrors(t *testing.T) { {"notes", "set", "ses-1", "--stdin", "extra"}, {"notes", "clear"}, {"notes", "list", "extra"}, + {"notes", "list", "--tag"}, } { if err := runNotes(&bytes.Buffer{}, args); err == nil { t.Fatalf("expected error for args %v", args)