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
5 changes: 3 additions & 2 deletions cmd/dispatch/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ Commands:
completion <shell> Print shell completion (bash, zsh, fish, powershell)
doctor [--json] Print environment diagnostics (--json for machine-readable output)
stats [flags] Print session totals and breakdowns
search [query] [flags] Print matching sessions as JSON, CSV, IDs, or a table
search [query] [flags] Print matching sessions as JSON, CSV, IDs, paths, or a table
tags [--json] List tags in use with per-tag session counts
aliases [--json] List session aliases with orphan detection
alias <id> <name> Set, reassign, clear (--clear), or remove (--remove) a session alias
Expand Down Expand Up @@ -180,8 +180,9 @@ Stats flags:
Search flags:
--json Print results as JSON (default)
--ids Print one session ID per line
--paths Print one working directory per line
--table Print a readable table
--format json|csv|ids|table
--format json|csv|ids|paths|table
Choose the output format
--query <text> Text to match (also accepted as a positional argument)
--deep Search turns, checkpoints, files, and refs too
Expand Down
28 changes: 27 additions & 1 deletion cmd/dispatch/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const (
searchFormatIDs searchOutputFormat = "ids"
searchFormatTable searchOutputFormat = "table"
searchFormatCSV searchOutputFormat = "csv"
searchFormatPaths searchOutputFormat = "paths"
)

// searchOptions holds the parsed flags for the search command.
Expand Down Expand Up @@ -94,6 +95,9 @@ func runSearch(w io.Writer, args []string) error {
if opts.format == searchFormatCSV {
return writeSearchCSV(w, sessions)
}
if opts.format == searchFormatPaths {
return writeSearchPaths(w, sessions)
}

results := make([]searchSession, 0, len(sessions))
for _, s := range sessions {
Expand Down Expand Up @@ -170,6 +174,24 @@ func writeSearchCSV(w io.Writer, sessions []data.Session) error {
return cw.Error()
}

func writeSearchPaths(w io.Writer, sessions []data.Session) error {
seen := map[string]struct{}{}
for _, s := range sessions {
path := strings.TrimSpace(s.Cwd)
if path == "" {
continue
}
if _, ok := seen[path]; ok {
continue
}
seen[path] = struct{}{}
if _, err := fmt.Fprintln(w, path); err != nil {
return err
}
}
return nil
}

func shortSearchID(id string) string {
if len(id) <= 12 {
return id
Expand Down Expand Up @@ -235,6 +257,8 @@ func parseSearchArgs(args []string) (searchOptions, error) {
opts.format = searchFormatTable
case name == "--csv":
opts.format = searchFormatCSV
case name == "--paths":
opts.format = searchFormatPaths
case name == "--format":
v, ni, err := takeValue(i, "--format", inlineOrEmpty(inline, hasInline))
if err != nil {
Expand Down Expand Up @@ -405,8 +429,10 @@ func parseSearchFormat(v string) (searchOutputFormat, error) {
return searchFormatTable, nil
case string(searchFormatCSV):
return searchFormatCSV, nil
case string(searchFormatPaths):
return searchFormatPaths, nil
default:
return "", fmt.Errorf("invalid --format value %q (want json, ids, table, or csv)", v)
return "", fmt.Errorf("invalid --format value %q (want json, ids, table, csv, or paths)", v)
}
}

Expand Down
27 changes: 27 additions & 0 deletions cmd/dispatch/search_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ func TestParseSearchArgsIDFormats(t *testing.T) {
{name: "csv shortcut", args: []string{"search", "--csv"}},
{name: "format csv separate", args: []string{"search", "--format", "csv"}},
{name: "format csv inline", args: []string{"search", "--format=csv"}},
{name: "paths shortcut", args: []string{"search", "--paths"}},
{name: "format paths separate", args: []string{"search", "--format", "paths"}},
{name: "format paths inline", args: []string{"search", "--format=paths"}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
Expand All @@ -127,6 +130,9 @@ func TestParseSearchArgsIDFormats(t *testing.T) {
if strings.Contains(strings.Join(tc.args, " "), "csv") {
want = searchFormatCSV
}
if strings.Contains(strings.Join(tc.args, " "), "paths") {
want = searchFormatPaths
}
if opts.format != want {
t.Errorf("format = %q, want %s", opts.format, want)
}
Expand Down Expand Up @@ -360,6 +366,27 @@ func TestRunSearchCSVEmptyPrintsHeader(t *testing.T) {
}
}

func TestRunSearchPathsOutput(t *testing.T) {
sessions := []data.Session{
{ID: "session-a", Cwd: "/code/app"},
{ID: "session-b", Cwd: " "},
{ID: "session-c", Cwd: "/code/app"},
{ID: "session-d", Cwd: "/code/other"},
}
withSearchList(t, func(data.FilterOptions, data.SortOptions, int) ([]data.Session, error) {
return sessions, nil
})

var buf bytes.Buffer
if err := runSearch(&buf, []string{"search", "--paths"}); err != nil {
t.Fatalf("runSearch returned error: %v", err)
}

if got, want := buf.String(), "/code/app\n/code/other\n"; got != want {
t.Errorf("output = %q, want %q", got, want)
}
}

func TestRunSearchEmptyIsEmptyArray(t *testing.T) {
withSearchList(t, func(data.FilterOptions, data.SortOptions, int) ([]data.Session, error) {
return nil, nil
Expand Down