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, paths, or a table
search [query] [flags] Print matching sessions as JSON, CSV, IDs, paths, commands, 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 @@ -181,8 +181,9 @@ Search flags:
--json Print results as JSON (default)
--ids Print one session ID per line
--paths Print one working directory per line
--commands Print one resume command per line
--table Print a readable table
--format json|csv|ids|paths|table
--format json|csv|ids|paths|commands|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
38 changes: 32 additions & 6 deletions cmd/dispatch/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"strings"
"text/tabwriter"

"github.com/jongio/dispatch/internal/config"
"github.com/jongio/dispatch/internal/data"
)

Expand All @@ -29,11 +30,12 @@ const searchAllLimit = 100_000
type searchOutputFormat string

const (
searchFormatJSON searchOutputFormat = "json"
searchFormatIDs searchOutputFormat = "ids"
searchFormatTable searchOutputFormat = "table"
searchFormatCSV searchOutputFormat = "csv"
searchFormatPaths searchOutputFormat = "paths"
searchFormatJSON searchOutputFormat = "json"
searchFormatIDs searchOutputFormat = "ids"
searchFormatTable searchOutputFormat = "table"
searchFormatCSV searchOutputFormat = "csv"
searchFormatPaths searchOutputFormat = "paths"
searchFormatCommands searchOutputFormat = "commands"
)

// searchOptions holds the parsed flags for the search command.
Expand Down Expand Up @@ -98,6 +100,13 @@ func runSearch(w io.Writer, args []string) error {
if opts.format == searchFormatPaths {
return writeSearchPaths(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 {
Expand Down Expand Up @@ -192,6 +201,19 @@ func writeSearchPaths(w io.Writer, sessions []data.Session) error {
return nil
}

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
Expand Down Expand Up @@ -259,6 +281,8 @@ func parseSearchArgs(args []string) (searchOptions, error) {
opts.format = searchFormatCSV
case name == "--paths":
opts.format = searchFormatPaths
case name == "--commands":
opts.format = searchFormatCommands
case name == "--format":
v, ni, err := takeValue(i, "--format", inlineOrEmpty(inline, hasInline))
if err != nil {
Expand Down Expand Up @@ -431,8 +455,10 @@ func parseSearchFormat(v string) (searchOutputFormat, error) {
return searchFormatCSV, nil
case string(searchFormatPaths):
return searchFormatPaths, nil
case string(searchFormatCommands):
return searchFormatCommands, nil
default:
return "", fmt.Errorf("invalid --format value %q (want json, ids, table, csv, or paths)", v)
return "", fmt.Errorf("invalid --format value %q (want json, ids, table, csv, paths, or commands)", v)
}
}

Expand Down
42 changes: 42 additions & 0 deletions cmd/dispatch/search_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -116,6 +117,9 @@ func TestParseSearchArgsIDFormats(t *testing.T) {
{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"}},
{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) {
Expand All @@ -133,6 +137,9 @@ func TestParseSearchArgsIDFormats(t *testing.T) {
if strings.Contains(strings.Join(tc.args, " "), "paths") {
want = searchFormatPaths
}
if strings.Contains(strings.Join(tc.args, " "), "commands") {
want = searchFormatCommands
}
if opts.format != want {
t.Errorf("format = %q, want %s", opts.format, want)
}
Expand Down Expand Up @@ -387,6 +394,41 @@ func TestRunSearchPathsOutput(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
Expand Down