Skip to content

Commit 118477a

Browse files
committed
feat(cli): add interactive resume command
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 913a81c6-b54f-4608-aa94-225366fe142a
1 parent f50192f commit 118477a

34 files changed

Lines changed: 1329 additions & 229 deletions

‎.github/workflows/ci.yml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ jobs:
8787
go run -tags screenshots ./cmd/screenshots --check --out .screenshots-check
8888
rm -rf .screenshots-check
8989
90+
- name: Test screenshot build tag
91+
run: go test -tags screenshots ./... -count=1
92+
9093
- name: Test with coverage
9194
run: go test -coverprofile=coverage.out ./...
9295

‎CHANGELOG.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
99
### Added
1010

1111
#### CLI
12-
- **`dispatch list`** — list sessions under the current directory in table form, with positional query text, an explicit `--folder` override, and the existing search filters and output formats. (#391)
12+
- **`dispatch resume`** — select and resume any matching session under the current directory from an aligned table showing full session ID, summary, repository, and branch columns. Large result sets can be revealed in batches from the picker. Positional query text and an explicit `--folder` override remain available. (#391)
1313

1414
### Fixed
1515

16-
- **Search/list safety and routing** — match only exact folders and true platform-native descendants, normalize Windows separator variants, protect CSV and terminal output from control-sequence injection, and keep command names inside multi-word TUI queries from hijacking dispatch.
16+
- **Search/resume safety and routing** — match only exact folders and true platform-native descendants, normalize Windows separator variants, protect CSV and terminal output from control-sequence injection, and keep command names inside multi-word TUI queries from hijacking dispatch.
1717

1818
## [v0.15.0] — 2026-08-12
1919

‎README.md‎

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Dispatch reads your local Copilot CLI session store and presents every past sess
2020

2121
- **Full-text search** (`/`) — FTS5 full-text search with BM25 ranking when available, falling back to LIKE for older CLI versions. Two-tier: quick search (summaries, branches, repos, directories) returns results instantly; deep search (turns, checkpoints, files, refs) kicks in after 300ms. Searching a number (e.g. "42", "#42", "PR42") also matches session refs (PRs, issues, commits)
2222
- **Directory filtering** (`f`) — hierarchical tree panel for toggling directory exclusion, persisted to config
23-
- **Word filtering** (Settings panel) — comma-separated list of words to exclude sessions by content. Sessions whose name or conversation turns contain any excluded word (case-insensitive) are hidden from the list
23+
- **Word filtering** (Settings panel) — comma-separated list of words to exclude sessions by content. Sessions whose summary or conversation turns contain any excluded word (case-insensitive) are hidden from the list
2424
- **Sorting** (`s` / `S`) — 4 fields (updated, folder, name, attention) with toggleable direction. Sort applies within each group; configuration also accepts the legacy `created` and `turns` values
2525
- **Grouping modes** (`Tab`) — list, folder, repo, branch, date, host. Displayed as collapsible trees with session counts
2626
- **Time range filtering** (`1`–`4`) — 1 hour, 1 day, 7 days, all
@@ -162,7 +162,7 @@ alias dc='dispatch --current'
162162
3. Navigate with arrow keys or `j`/`k`
163163
4. Press `p` to toggle the preview pane and read the conversation
164164
5. Press `Enter` to resume the selected session (opens in a new tab by default)
165-
6. Use `Tab` to cycle grouping modes (folder → repo → branch → date → flat)
165+
6. Use `Tab` to cycle grouping modes (list → folder → repo → branch → date → host)
166166
7. Press `s` to cycle sort fields, `S` to flip direction
167167
8. Press `,` to open settings — change theme, launch mode, model, and more
168168

@@ -467,23 +467,24 @@ dispatch watch --exec '[ "$DISPATCH_SESSION_STATE" = waiting ] && \
467467
curl -s -X POST "$WEBHOOK_URL" -d "session=$DISPATCH_SESSION_ID needs input"'
468468
```
469469

470-
### List
470+
### Resume
471471

472-
List sessions under the current working directory in a readable table:
472+
Select and resume a session under the current working directory:
473473

474474
```sh
475-
dispatch list
476-
dispatch list auth
477-
dispatch list --folder ../other-project
478-
dispatch list --limit 10
479-
dispatch list --json
475+
dispatch resume
476+
dispatch resume auth
477+
dispatch resume --folder ../other-project
478+
dispatch resume --demo
480479
```
481480

482-
`dispatch list` is a current-directory preset for `dispatch search`. Positional
481+
`dispatch resume` is a current-directory session picker. Positional
483482
text remains a search query, `--folder <path>` selects another directory, and
484-
all search filters are available. Table output is the default; explicit output
485-
flags such as `--json`, `--jsonl`, `--csv`, `--ids`, `--paths`, and
486-
`--commands` override it.
483+
all search filters are available. By default it shows every match in an
484+
interactive table with columns for the full session ID, summary, repository,
485+
and branch; pressing Enter resumes the selected session through the same launch
486+
path as the main TUI. The first 50 matches are shown initially; select the
487+
show-more row or press `m` to reveal the next batch.
487488

488489
To launch the TUI with a query that starts with a command name, use `--`:
489490
`dispatch -- list bug`.

‎cmd/dispatch/cli.go‎

Lines changed: 74 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,16 @@ func runVersion(w io.Writer, args []string) error {
8080
}
8181

8282
func handleArgs(args []string, origStderr io.Writer, updateCh <-chan *update.UpdateInfo) (done bool, cleanup func(), startup startupOptions, err error) {
83+
args, demo := extractDemoFlag(args)
84+
if demo {
85+
c, demoErr := setupDemo()
86+
if demoErr != nil {
87+
fmt.Fprintf(os.Stderr, "demo: %v\n", demoErr)
88+
return true, nil, startupOptions{}, demoErr
89+
}
90+
cleanup = c
91+
}
92+
8393
var flags startupFlags
8494
positionalQueryStarted := false
8595
for i := 0; i < len(args); i++ {
@@ -179,9 +189,9 @@ func handleArgs(args []string, origStderr io.Writer, updateCh <-chan *update.Upd
179189
}
180190
return true, cleanup, startupOptions{}, nil
181191

182-
case "list":
192+
case "resume":
183193
if lErr := runList(os.Stdout, args[i:]); lErr != nil {
184-
fmt.Fprintf(os.Stderr, "list: %v\n", lErr)
194+
fmt.Fprintf(os.Stderr, "resume: %v\n", lErr)
185195
return true, cleanup, startupOptions{}, lErr
186196
}
187197
return true, cleanup, startupOptions{}, nil
@@ -289,14 +299,6 @@ func handleArgs(args []string, origStderr io.Writer, updateCh <-chan *update.Upd
289299
// dynamic candidates. Deliberately omitted from help and usage.
290300
runComplete(os.Stdout, args[i:])
291301
return true, cleanup, startupOptions{}, nil
292-
case "--demo":
293-
c, demoErr := setupDemo()
294-
if demoErr != nil {
295-
fmt.Fprintf(os.Stderr, "demo: %v\n", demoErr)
296-
return true, cleanup, startupOptions{}, demoErr
297-
}
298-
cleanup = c
299-
300302
case "--clear-cache":
301303
if cErr := configResetFn(); cErr != nil {
302304
fmt.Fprintf(os.Stderr, "clear-cache: %v\n", cErr)
@@ -387,6 +389,49 @@ func handleArgs(args []string, origStderr io.Writer, updateCh <-chan *update.Upd
387389
return false, cleanup, startup, nil
388390
}
389391

392+
func extractDemoFlag(args []string) ([]string, bool) {
393+
cleaned := make([]string, 0, len(args))
394+
demo := false
395+
command := ""
396+
for i, arg := range args {
397+
if arg == "--" {
398+
cleaned = append(cleaned, args[i:]...)
399+
break
400+
}
401+
if command == "" && !strings.HasPrefix(arg, "-") {
402+
command = arg
403+
}
404+
if arg == "--demo" {
405+
if command != "" && command != "resume" && command != "search" {
406+
cleaned = append(cleaned, arg)
407+
continue
408+
}
409+
if i > 0 && searchFlagConsumesValue(args[i-1]) {
410+
cleaned = append(cleaned, arg)
411+
continue
412+
}
413+
demo = true
414+
continue
415+
}
416+
cleaned = append(cleaned, arg)
417+
}
418+
return cleaned, demo
419+
}
420+
421+
func searchFlagConsumesValue(arg string) bool {
422+
if strings.Contains(arg, "=") {
423+
return false
424+
}
425+
switch arg {
426+
case "--branch", "--folder", "--format", "--host", "--limit", "--order",
427+
"--query", "--repo", "--repository", "--since", "--sort", "--tag",
428+
"--until", "-n", "-q":
429+
return true
430+
default:
431+
return false
432+
}
433+
}
434+
390435
// unknownFlag prints the usage banner and returns the unknown-flag error. It is
391436
// shared by the direct and inline flag parsing paths.
392437
func unknownFlag(arg string) error {
@@ -426,7 +471,7 @@ func splitInlineFlag(arg string) (flag, value string, ok bool) {
426471
// assert every command here appears in each of them.
427472
var cliCommands = []string{
428473
"help", "version", "open", "new", "doctor", "update", "completion",
429-
"stats", "search", "list", "tags", "notes", "views", "aliases", "alias",
474+
"stats", "search", "resume", "tags", "notes", "views", "aliases", "alias",
430475
"compare", "prune", "tag", "watch", "config", "export", "info",
431476
"path", "man",
432477
}
@@ -462,7 +507,7 @@ const bashCompletionScript = `# bash completion for dispatch
462507
_dispatch_completion() {
463508
local cur="${COMP_WORDS[COMP_CWORD]}"
464509
local bin="${COMP_WORDS[0]}"
465-
local commands="help version open new doctor update completion stats search list tags notes views aliases alias compare prune tag watch config export info path man"
510+
local commands="help version open new doctor update completion stats search resume tags notes views aliases alias compare prune tag watch config export info path man"
466511
local flags="-h --help -v --version --demo --clear-cache --reindex --current --cwd --repo --branch --query --"
467512
local cmd_index=1
468513
[[ "${COMP_WORDS[1]}" == "--demo" ]] && cmd_index=2
@@ -478,14 +523,14 @@ _dispatch_completion() {
478523
return 0
479524
;;
480525
open)
481-
COMPREPLY=( $(compgen -W "$("${bin}" __complete aliases) --mode --last --print --agent --model --yolo" -- "${cur}") )
526+
COMPREPLY=( $(compgen -W "$("${bin}" __complete aliases) --mode --last --print --agent --model --yolo --repo --branch --folder --current" -- "${cur}") )
482527
return 0
483528
;;
484529
new)
485530
COMPREPLY=( $(compgen -W "--mode --agent --model --yolo" -- "${cur}") )
486531
return 0
487532
;;
488-
list)
533+
resume)
489534
COMPREPLY=( $(compgen -W "--json --jsonl --ids --paths --commands --csv --table --format -q --query --deep --repo --repository --branch --folder --tag --host --since --until --sort --order -n --limit" -- "${cur}") )
490535
return 0
491536
;;
@@ -504,15 +549,15 @@ complete -F _dispatch_completion dispatch disp
504549

505550
const zshCompletionScript = `#compdef dispatch disp
506551
_dispatch_completion() {
507-
local -a commands flags configsubs shells aliases configkeys openflags newflags listflags
552+
local -a commands flags configsubs shells aliases configkeys openflags newflags resumeflags
508553
local bin=${words[1]}
509554
local cmd_index=2
510555
[[ ${words[2]} == --demo ]] && cmd_index=3
511-
commands=(help version open new doctor update completion stats search list tags notes views aliases alias compare prune tag watch config export info path man)
556+
commands=(help version open new doctor update completion stats search resume tags notes views aliases alias compare prune tag watch config export info path man)
512557
configsubs=(list get set unset edit path validate schema export import)
513-
openflags=(--mode --last --print --agent --model --yolo)
558+
openflags=(--mode --last --print --agent --model --yolo --repo --branch --folder --current)
514559
newflags=(--mode --agent --model --yolo)
515-
listflags=(--json --jsonl --ids --paths --commands --csv --table --format -q --query --deep --repo --repository --branch --folder --tag --host --since --until --sort --order -n --limit)
560+
resumeflags=(--json --jsonl --ids --paths --commands --csv --table --format -q --query --deep --repo --repository --branch --folder --tag --host --since --until --sort --order -n --limit)
516561
flags=(-h --help -v --version --demo --clear-cache --reindex --current --cwd --repo --branch --query --)
517562
518563
if (( CURRENT == cmd_index )); then
@@ -529,6 +574,7 @@ _dispatch_completion() {
529574
if [[ ${words[cmd_index]} == open ]]; then
530575
aliases=(${(f)"$($bin __complete aliases)"})
531576
_describe -t aliases 'session alias' aliases
577+
_describe -t openflags 'open flag' openflags
532578
return
533579
fi
534580
@@ -542,18 +588,13 @@ _dispatch_completion() {
542588
return
543589
fi
544590
545-
if [[ ${words[cmd_index]} == open ]]; then
546-
_describe -t openflags 'open flag' openflags
547-
return
548-
fi
549-
550591
if [[ ${words[cmd_index]} == new ]]; then
551592
_describe -t newflags 'new flag' newflags
552593
return
553594
fi
554595
555-
if [[ ${words[cmd_index]} == list ]]; then
556-
_describe -t listflags 'list flag' listflags
596+
if [[ ${words[cmd_index]} == resume ]]; then
597+
_describe -t resumeflags 'resume flag' resumeflags
557598
return
558599
fi
559600
}
@@ -592,25 +633,25 @@ end
592633
593634
for bin in dispatch disp
594635
complete -c $bin -f
595-
complete -c $bin -n '__dispatch_needs_command' -a 'help version open new doctor update completion stats search list tags notes views aliases alias compare prune tag watch config export info path man'
636+
complete -c $bin -n '__dispatch_needs_command' -a 'help version open new doctor update completion stats search resume tags notes views aliases alias compare prune tag watch config export info path man'
596637
complete -c $bin -n '__dispatch_needs_command' -a '-h --help -v --version --demo --clear-cache --reindex --current --cwd --repo --branch --query --'
597638
complete -c $bin -n '__dispatch_after completion' -a "($bin __complete shells)"
598639
complete -c $bin -n '__dispatch_after open' -a "($bin __complete aliases)"
599-
complete -c $bin -n '__dispatch_after open' -a '--mode --last --print --agent --model --yolo'
640+
complete -c $bin -n '__dispatch_after open' -a '--mode --last --print --agent --model --yolo --repo --branch --folder --current'
600641
complete -c $bin -n '__dispatch_after new' -a '--mode --agent --model --yolo'
601-
complete -c $bin -n '__dispatch_after list' -a '--json --jsonl --ids --paths --commands --csv --table --format -q --query --deep --repo --repository --branch --folder --tag --host --since --until --sort --order -n --limit'
642+
complete -c $bin -n '__dispatch_after resume' -a '--json --jsonl --ids --paths --commands --csv --table --format -q --query --deep --repo --repository --branch --folder --tag --host --since --until --sort --order -n --limit'
602643
complete -c $bin -n '__dispatch_after config' -a 'list get set unset edit path validate schema export import'
603644
complete -c $bin -n '__dispatch_config_key' -a "($bin __complete config-keys)"
604645
end
605646
`
606647

607648
const powershellCompletionScript = `# PowerShell completion for dispatch
608-
$script:DispatchCommands = @('help', 'version', 'open', 'new', 'doctor', 'update', 'completion', 'stats', 'search', 'list', 'tags', 'notes', 'views', 'aliases', 'alias', 'compare', 'prune', 'tag', 'watch', 'config', 'export', 'info', 'path', 'man')
649+
$script:DispatchCommands = @('help', 'version', 'open', 'new', 'doctor', 'update', 'completion', 'stats', 'search', 'resume', 'tags', 'notes', 'views', 'aliases', 'alias', 'compare', 'prune', 'tag', 'watch', 'config', 'export', 'info', 'path', 'man')
609650
$script:DispatchFlags = @('-h', '--help', '-v', '--version', '--demo', '--clear-cache', '--reindex', '--current', '--cwd', '--repo', '--branch', '--query', '--')
610651
$script:DispatchConfigSubcommands = @('list', 'get', 'set', 'unset', 'edit', 'path', 'validate', 'schema', 'export', 'import')
611-
$script:DispatchOpenFlags = @('--mode', '--last', '--print', '--agent', '--model', '--yolo')
652+
$script:DispatchOpenFlags = @('--mode', '--last', '--print', '--agent', '--model', '--yolo', '--repo', '--branch', '--folder', '--current')
612653
$script:DispatchNewFlags = @('--mode', '--agent', '--model', '--yolo')
613-
$script:DispatchListFlags = @('--json', '--jsonl', '--ids', '--paths', '--commands', '--csv', '--table', '--format', '-q', '--query', '--deep', '--repo', '--repository', '--branch', '--folder', '--tag', '--host', '--since', '--until', '--sort', '--order', '-n', '--limit')
654+
$script:DispatchResumeFlags = @('--json', '--jsonl', '--ids', '--paths', '--commands', '--csv', '--table', '--format', '-q', '--query', '--deep', '--repo', '--repository', '--branch', '--folder', '--tag', '--host', '--since', '--until', '--sort', '--order', '-n', '--limit')
614655
615656
Register-ArgumentCompleter -Native -CommandName dispatch, disp -ScriptBlock {
616657
param($wordToComplete, $commandAst, $cursorPosition)
@@ -638,8 +679,8 @@ Register-ArgumentCompleter -Native -CommandName dispatch, disp -ScriptBlock {
638679
}
639680
} elseif ($command -eq 'new') {
640681
$script:DispatchNewFlags
641-
} elseif ($command -eq 'list') {
642-
$script:DispatchListFlags
682+
} elseif ($command -eq 'resume') {
683+
$script:DispatchResumeFlags
643684
} else {
644685
$script:DispatchCommands + $script:DispatchFlags
645686
}

0 commit comments

Comments
 (0)