diff --git a/README.md b/README.md index bf2de97..01270db 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/cmd/dispatch/main.go b/cmd/dispatch/main.go index 8931a16..26ab82b 100644 --- a/cmd/dispatch/main.go +++ b/cmd/dispatch/main.go @@ -204,7 +204,8 @@ Config commands: Notes commands: notes [list] [--json] List notes attached to current sessions notes get Print one session note - notes set Set one session note + notes set Set one session note + notes set --stdin Read one session note from stdin notes clear Clear one session note Export flags: diff --git a/cmd/dispatch/notes.go b/cmd/dispatch/notes.go index 3694f85..125ef04 100644 --- a/cmd/dispatch/notes.go +++ b/cmd/dispatch/notes.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "io" + "os" "sort" "strings" @@ -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"` @@ -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) @@ -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") diff --git a/cmd/dispatch/notes_test.go b/cmd/dispatch/notes_test.go index 1d2c0d6..4ef3272 100644 --- a/cmd/dispatch/notes_test.go +++ b/cmd/dispatch/notes_test.go @@ -110,6 +110,29 @@ 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") }) @@ -117,6 +140,7 @@ func TestRunNotesErrors(t *testing.T) { {"notes", "bogus"}, {"notes", "get"}, {"notes", "set", "ses-1"}, + {"notes", "set", "ses-1", "--stdin", "extra"}, {"notes", "clear"}, {"notes", "list", "extra"}, } {