From aae70ca838b8918e9b9c9b42d6beb63a26de841e Mon Sep 17 00:00:00 2001 From: Jon Gallant <2163001+jongio@users.noreply.github.com> Date: Sat, 25 Jul 2026 10:53:46 -0700 Subject: [PATCH] Add command output to search Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e02217cd-ffed-496e-b8ea-848a781cda64 --- cmd/dispatch/main.go | 5 +++-- cmd/dispatch/search.go | 28 ++++++++++++++++++++++++- cmd/dispatch/search_test.go | 42 +++++++++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/cmd/dispatch/main.go b/cmd/dispatch/main.go index d689ff5..8871b07 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, commands, 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 + --commands Print one resume command per line --table Print a readable table - --format json|csv|ids|table + --format json|csv|ids|commands|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..0eed2e5 100644 --- a/cmd/dispatch/search.go +++ b/cmd/dispatch/search.go @@ -10,6 +10,7 @@ import ( "strings" "text/tabwriter" + "github.com/jongio/dispatch/internal/config" "github.com/jongio/dispatch/internal/data" ) @@ -33,6 +34,7 @@ const ( searchFormatIDs searchOutputFormat = "ids" searchFormatTable searchOutputFormat = "table" searchFormatCSV searchOutputFormat = "csv" + searchFormatCommands searchOutputFormat = "commands" ) // searchOptions holds the parsed flags for the search command. @@ -94,6 +96,13 @@ func runSearch(w io.Writer, args []string) error { if opts.format == searchFormatCSV { return writeSearchCSV(w, sessions) } + if opts.format == searchFormatCommands { + cfg, err := configLoadFn() + if err != nil { + return fmt.Errorf("loading config: %w", err) + } + return writeSearchCommands(w, sessions, cfg) + } results := make([]searchSession, 0, len(sessions)) for _, s := range sessions { @@ -170,6 +179,19 @@ func writeSearchCSV(w io.Writer, sessions []data.Session) error { return cw.Error() } +func writeSearchCommands(w io.Writer, sessions []data.Session, cfg *config.Config) error { + for _, s := range sessions { + cmdStr, err := openResumeCmdFn(s.ID, openResumeConfig(cfg, &s)) + if err != nil { + return err + } + if _, err := fmt.Fprintln(w, cmdStr); 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 == "--commands": + opts.format = searchFormatCommands 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(searchFormatCommands): + return searchFormatCommands, 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 commands)", v) } } diff --git a/cmd/dispatch/search_test.go b/cmd/dispatch/search_test.go index 3462671..1306e7f 100644 --- a/cmd/dispatch/search_test.go +++ b/cmd/dispatch/search_test.go @@ -10,6 +10,7 @@ import ( "github.com/jongio/dispatch/internal/config" "github.com/jongio/dispatch/internal/data" + "github.com/jongio/dispatch/internal/platform" ) // withSearchList swaps the search command's session loader for a test double @@ -113,6 +114,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: "commands shortcut", args: []string{"search", "--commands"}}, + {name: "format commands separate", args: []string{"search", "--format", "commands"}}, + {name: "format commands inline", args: []string{"search", "--format=commands"}}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { @@ -127,6 +131,9 @@ func TestParseSearchArgsIDFormats(t *testing.T) { if strings.Contains(strings.Join(tc.args, " "), "csv") { want = searchFormatCSV } + if strings.Contains(strings.Join(tc.args, " "), "commands") { + want = searchFormatCommands + } if opts.format != want { t.Errorf("format = %q, want %s", opts.format, want) } @@ -360,6 +367,41 @@ func TestRunSearchCSVEmptyPrintsHeader(t *testing.T) { } } +func TestRunSearchCommandsOutput(t *testing.T) { + sessions := []data.Session{ + {ID: "session-a", Cwd: "/code/app"}, + {ID: "session-b", Cwd: "/code/other"}, + } + withSearchList(t, func(data.FilterOptions, data.SortOptions, int) ([]data.Session, error) { + return sessions, nil + }) + prevConfig := configLoadFn + configLoadFn = func() (*config.Config, error) { + cfg := config.Default() + cfg.Model = "gpt-test" + return cfg, nil + } + prevResume := openResumeCmdFn + openResumeCmdFn = func(id string, rc platform.ResumeConfig) (string, error) { + return "copilot --resume " + id + " --cwd " + rc.Cwd + " --model " + rc.Model, nil + } + t.Cleanup(func() { + configLoadFn = prevConfig + openResumeCmdFn = prevResume + }) + + var buf bytes.Buffer + if err := runSearch(&buf, []string{"search", "--commands"}); err != nil { + t.Fatalf("runSearch returned error: %v", err) + } + + want := "copilot --resume session-a --cwd /code/app --model gpt-test\n" + + "copilot --resume session-b --cwd /code/other --model gpt-test\n" + if got := buf.String(); 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