Skip to content
Open
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,13 +288,15 @@ Manage session notes from the command line without editing `config.json` directl
```sh
dispatch notes
dispatch notes --json
dispatch notes --csv
dispatch notes --markdown
dispatch notes get 0a1b2c3d
dispatch notes set 0a1b2c3d "follow up after review"
printf 'line one\nline two\n' | dispatch notes set 0a1b2c3d --stdin
dispatch notes clear 0a1b2c3d
```

`dispatch notes` lists notes for sessions that still exist in the session store. `set`, `get`, and `clear` operate on one session ID and use the same notes shown in the TUI preview. Use `--stdin` to set a multiline note without shell quoting.
`dispatch notes` lists notes for sessions that still exist in the session store. `set`, `get`, and `clear` operate on one session ID and use the same notes shown in the TUI preview. Use `--stdin` to set a multiline note without shell quoting. Add `--json`, `--csv`, or `--markdown` to the list to reformat it for scripts, spreadsheets, or docs.

### Named Views

Expand Down
2 changes: 1 addition & 1 deletion cmd/dispatch/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ Config commands:
config import [--in F] Read config JSON from stdin or a file

Notes commands:
notes [list] [--json] List notes attached to current sessions
notes [list] [--json|--csv|--markdown] List notes attached to current sessions
notes get <id> Print one session note
notes set <id> <text...> Set one session note
notes set <id> --stdin Read one session note from stdin
Expand Down
2 changes: 1 addition & 1 deletion cmd/dispatch/man.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ var manCommands = []manEntry{
{"alias <id> <name>", "Set, reassign, clear (--clear), or remove (--remove) a session alias."},
{"views [list] [--json|--csv]", "List configured named views."},
{"views use <name|default>", "Set or clear the active named view."},
{"notes [command]", "List, get, set, or clear session notes."},
{"notes [list] [--json|--csv|--markdown]", "List, get, set, or clear session notes."},
{"config <get|set|unset|list|edit|path|validate|schema|export|import>", "Read, change, validate, document, or transfer preferences."},
{"export <id> [flags]", "Export a session as Markdown, JSON, HTML, or plain text."},
{"info <id> [--json] [--refs]", "Print a concise session summary."},
Expand Down
28 changes: 25 additions & 3 deletions cmd/dispatch/notes.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func runNotes(w io.Writer, args []string) error {
if len(rest) > 0 {
rest = rest[1:]
}
if len(rest) == 0 || rest[0] == "list" || rest[0] == "--json" || rest[0] == "--csv" {
if len(rest) == 0 || rest[0] == "list" || rest[0] == "--json" || rest[0] == "--csv" || rest[0] == "--markdown" {
return runNotesList(w, rest)
}

Expand All @@ -60,6 +60,7 @@ func runNotes(w io.Writer, args []string) error {
func runNotesList(w io.Writer, args []string) error {
jsonOut := false
csvOut := false
markdownOut := false
tag := ""
if len(args) > 0 && args[0] == "list" {
args = args[1:]
Expand All @@ -72,6 +73,8 @@ func runNotesList(w io.Writer, args []string) error {
jsonOut = true
case "--csv":
csvOut = true
case "--markdown":
markdownOut = true
case "--tag":
if hasInline {
tag = inline
Expand All @@ -86,8 +89,8 @@ func runNotesList(w io.Writer, args []string) error {
return fmt.Errorf("notes list does not take arguments, got %q", arg)
}
}
if jsonOut && csvOut {
return fmt.Errorf("--json and --csv cannot be combined")
if (jsonOut && csvOut) || (jsonOut && markdownOut) || (csvOut && markdownOut) {
return fmt.Errorf("--json, --csv, and --markdown cannot be combined")
}
tag = normalizeNotesTag(tag)

Expand All @@ -111,6 +114,10 @@ func runNotesList(w io.Writer, args []string) error {
if csvOut {
return writeNotesCSV(w, report)
}
if markdownOut {
writeNotesMarkdown(w, report)
return nil
}
writeNotesText(w, report)
return nil
}
Expand Down Expand Up @@ -285,3 +292,18 @@ func writeNotesCSV(w io.Writer, report notesReport) error {
cw.Flush()
return cw.Error()
}

func writeNotesMarkdown(w io.Writer, report notesReport) {
fmt.Fprintln(w, "# Dispatch notes")
fmt.Fprintln(w)
if report.TotalNotes == 0 {
fmt.Fprintln(w, "No notes found.")
return
}
fmt.Fprintf(w, "Notes: %d\n\n", report.TotalNotes)
fmt.Fprintln(w, "| ID | Summary | Note |")
fmt.Fprintln(w, "|---|---|---|")
for _, entry := range report.Notes {
fmt.Fprintf(w, "| %s | %s | %s |\n", markdownCell(entry.ID), markdownCell(entry.Summary), markdownCell(entry.Note))
}
}
65 changes: 65 additions & 0 deletions cmd/dispatch/notes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,71 @@ func TestRunNotesListCSV(t *testing.T) {
}
}

func TestRunNotesListMarkdown(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", "--markdown"}); err != nil {
t.Fatalf("runNotes markdown: %v", err)
}
out := buf.String()
for _, want := range []string{
"# Dispatch notes",
"Notes: 2",
"| ID | Summary | Note |",
"|---|---|---|",
"| a | Auth fix | follow up |",
"| b | Build command | ready to ship |",
} {
if !strings.Contains(out, want) {
t.Errorf("Markdown output missing %q:\n%s", want, out)
}
}
if strings.Contains(out, "orphan note") {
t.Fatalf("orphan note should not appear:\n%s", out)
}
}

func TestRunNotesListMarkdownEmpty(t *testing.T) {
cfg := config.Default()
withConfigSeams(t, cfg)
withNotesList(t, func(data.FilterOptions) ([]data.Session, error) { return notedSessions(), nil })

var buf bytes.Buffer
if err := runNotes(&buf, []string{"notes", "list", "--markdown"}); err != nil {
t.Fatalf("runNotes markdown empty: %v", err)
}
out := buf.String()
if !strings.Contains(out, "# Dispatch notes") || !strings.Contains(out, "No notes found.") {
t.Fatalf("empty markdown output = %q", out)
}
if strings.Contains(out, "| ID |") {
t.Fatalf("empty markdown should not print a table:\n%s", out)
}
}

func TestRunNotesListMarkdownEscapes(t *testing.T) {
cfg := config.Default()
cfg.SessionNotes = map[string]string{"a": "line one\nline two | piped"}
withConfigSeams(t, cfg)
withNotesList(t, func(data.FilterOptions) ([]data.Session, error) {
return []data.Session{{ID: "a", Summary: "Sum | mary"}}, nil
})

var buf bytes.Buffer
if err := runNotes(&buf, []string{"notes", "--markdown"}); err != nil {
t.Fatalf("runNotes markdown escapes: %v", err)
}
out := buf.String()
if strings.Contains(out, "line one\nline two") {
t.Fatalf("newline not escaped in note cell:\n%s", out)
}
if !strings.Contains(out, `\|`) {
t.Fatalf("pipe not escaped in cells:\n%s", out)
}
}

func TestRunNotesGetSetClear(t *testing.T) {
cfg := withConfigSeams(t, config.Default())

Expand Down
Loading