diff --git a/cmd/dispatch/main.go b/cmd/dispatch/main.go index d689ff5..ec84c09 100644 --- a/cmd/dispatch/main.go +++ b/cmd/dispatch/main.go @@ -126,7 +126,7 @@ Commands: completion 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 Set, reassign, clear (--clear), or remove (--remove) a session alias @@ -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 to match (also accepted as a positional argument) --deep Search turns, checkpoints, files, and refs too diff --git a/cmd/dispatch/search.go b/cmd/dispatch/search.go index 4e6c591..8e05c4d 100644 --- a/cmd/dispatch/search.go +++ b/cmd/dispatch/search.go @@ -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. @@ -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 { @@ -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 @@ -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 { @@ -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) } } diff --git a/cmd/dispatch/search_test.go b/cmd/dispatch/search_test.go index 3462671..28f9697 100644 --- a/cmd/dispatch/search_test.go +++ b/cmd/dispatch/search_test.go @@ -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) { @@ -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) } @@ -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