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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,10 +282,11 @@ dispatch notes
dispatch notes --json
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.
`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.

### Named Views

Expand Down
3 changes: 2 additions & 1 deletion cmd/dispatch/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,8 @@ Config commands:
Notes commands:
notes [list] [--json] List notes attached to current sessions
notes get <id> Print one session note
notes set <id> <text> Set one session note
notes set <id> <text...> Set one session note
notes set <id> --stdin Read one session note from stdin
notes clear <id> Clear one session note

Export flags:
Expand Down
38 changes: 36 additions & 2 deletions cmd/dispatch/notes.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"io"
"os"
"sort"
"strings"

Expand All @@ -15,6 +16,10 @@ import (
// variable so tests can substitute a fixed set of sessions.
var notesListSessionsFn = defaultStatsListSessions

// notesStdin is the input stream used by `notes set --stdin`. Tests swap it
// for an in-memory reader.
var notesStdin io.Reader = os.Stdin

type noteEntry struct {
ID string `json:"id"`
Summary string `json:"summary"`
Expand Down Expand Up @@ -96,14 +101,17 @@ func runNotesGet(w io.Writer, args []string) error {
}

func runNotesSet(w io.Writer, args []string) error {
if len(args) < 2 {
if len(args) < 1 {
return fmt.Errorf("notes set requires a session ID and note text")
}
sessionID := args[0]
note := strings.Join(args[1:], " ")
if strings.TrimSpace(sessionID) == "" {
return fmt.Errorf("notes set requires a session ID")
}
note, err := parseNoteText(args[1:])
if err != nil {
return err
}
cfg, err := configLoadFn()
if err != nil {
return fmt.Errorf("loading config: %w", err)
Expand All @@ -116,6 +124,32 @@ func runNotesSet(w io.Writer, args []string) error {
return nil
}

func parseNoteText(args []string) (string, error) {
if len(args) == 0 {
return "", fmt.Errorf("notes set requires a session ID and note text")
}
readStdin := false
var textParts []string
for _, arg := range args {
if arg == "--stdin" {
readStdin = true
continue
}
textParts = append(textParts, arg)
}
if readStdin {
if len(textParts) > 0 {
return "", fmt.Errorf("notes set --stdin cannot be combined with note text")
}
b, err := io.ReadAll(notesStdin)
if err != nil {
return "", fmt.Errorf("reading note from stdin: %w", err)
}
return string(b), nil
}
return strings.Join(textParts, " "), nil
}

func runNotesClear(w io.Writer, args []string) error {
if len(args) != 1 {
return fmt.Errorf("notes clear requires a session ID")
Expand Down
24 changes: 24 additions & 0 deletions cmd/dispatch/notes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,13 +110,37 @@ func TestRunNotesGetSetClear(t *testing.T) {
}
}

func TestRunNotesSetFromStdin(t *testing.T) {
cfg := withConfigSeams(t, config.Default())
prev := notesStdin
notesStdin = strings.NewReader("line one\nline two\n")
t.Cleanup(func() { notesStdin = prev })

var buf bytes.Buffer
if err := runNotes(&buf, []string{"notes", "set", "ses-1", "--stdin"}); err != nil {
t.Fatalf("set note from stdin: %v", err)
}
if got := cfg.SessionNotes["ses-1"]; got != "line one\nline two\n" {
t.Fatalf("stored note = %q", got)
}

buf.Reset()
if err := runNotes(&buf, []string{"notes", "get", "ses-1"}); err != nil {
t.Fatalf("get note: %v", err)
}
if buf.String() != "line one\nline two\n\n" {
t.Fatalf("get output = %q", buf.String())
}
}

func TestRunNotesErrors(t *testing.T) {
withConfigSeams(t, config.Default())
withNotesList(t, func(data.FilterOptions) ([]data.Session, error) { return nil, errors.New("boom") })
for _, args := range [][]string{
{"notes", "bogus"},
{"notes", "get"},
{"notes", "set", "ses-1"},
{"notes", "set", "ses-1", "--stdin", "extra"},
{"notes", "clear"},
{"notes", "list", "extra"},
} {
Expand Down