Skip to content

Commit 53a921b

Browse files
committed
Merge remote-tracking branch 'origin/main'
# Conflicts: # README.md # cmd/dispatch/config.go # cmd/dispatch/man.go # internal/config/config_test.go # internal/tui/keys.go # internal/tui/model.go
2 parents 5076108 + 244f4d0 commit 53a921b

55 files changed

Lines changed: 4373 additions & 450 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[ 733013ms] [LOG] [vite] server connection lost. Polling for restart... @ http://localhost:4321/@vite/client:1598
2+
[ 1673030ms] [LOG] [vite] server connection lost. Polling for restart... @ http://localhost:4321/@vite/client:1598

‎.playwright-mcp/page-2026-06-24T18-19-10-290Z.yml‎

Lines changed: 728 additions & 0 deletions
Large diffs are not rendered by default.

‎.playwright-mcp/page-2026-06-24T18-20-30-013Z.yml‎

Lines changed: 830 additions & 0 deletions
Large diffs are not rendered by default.

‎.playwright-mcp/page-2026-06-24T18-22-06-184Z.yml‎

Lines changed: 780 additions & 0 deletions
Large diffs are not rendered by default.

‎README.md‎

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ Dispatch reads your local Copilot CLI session store and presents every past sess
4141
- **Session favorites** (`*`) — star sessions as favorites. Filter to show only favorites via the `!` status picker
4242
- **Session tags** (`#`) — attach comma-separated tags to sessions and filter to a tag with the `tag:` search token
4343
- **Session aliases** (`A`) — give a session a short, memorable alias and resume it from the CLI with `dispatch open <alias>` instead of the full session ID
44-
- **Settings panel** (`,`) — 19 fields: Yolo Mode, Agent, Model, Launch Mode, Pane Direction, Terminal, Shell, Custom Command, Theme, Crash Recovery, Preview Position, Redact Secrets, Excluded Words, Auto Refresh, Notify On Waiting, and column toggles for Repo, Folder, Turns, and Host
44+
- **Settings panel** (`,`) — 20 fields: Yolo Mode, Agent, Model, Launch Mode, Pane Direction, Terminal, Shell, Resume Session Command, New Session Command, Theme, Crash Recovery, Preview Position, Redact Secrets, Excluded Words, Auto Refresh, Notify On Waiting, and column toggles for Repo, Folder, Turns, and Host
4545
- **Configurable list columns** (`,` settings) — choose which optional columns (repo, folder, turns, host) appear in the session list. Defaults show every column, and the session name and attention indicator are always visible
4646
- **Shell picker** — auto-detects installed shells, modal picker when multiple available
4747
- **5 built-in themes** — Dispatch Dark, Dispatch Light, Campbell, One Half Dark, One Half Light + custom via Windows Terminal JSON
@@ -677,7 +677,7 @@ Configuration is stored in the platform-specific config directory:
677677
- **macOS**: `~/Library/Application Support/dispatch/config.json`
678678
- **Windows**: `%APPDATA%\dispatch\config.json`
679679

680-
Set `DISPATCH_CONFIG` to an absolute file path to use a different config file, for example to keep separate work and personal profiles. `config path`, `config get`/`set`/`edit`, and `doctor` all follow the override. A relative or UNC value is ignored and the default location is used.
680+
Set `DISPATCH_CONFIG` to an absolute file path to use a different config file, for example to keep separate work and personal profiles. `config path`, `config get`/`set`/`edit`/`export`/`import`, and `doctor` all follow the override. A relative or UNC value is ignored and the default location is used.
681681

682682
### From the command line
683683

@@ -694,9 +694,11 @@ dispatch config path # print the config file path
694694
dispatch config validate # validate config.json before launching the TUI
695695
dispatch config validate --json # machine-readable diagnostics for CI/scripts
696696
dispatch config schema # print the JSON Schema for editor integration
697+
dispatch config export --out dispatch-config.json
698+
dispatch config import --in dispatch-config.json
697699
```
698700

699-
`set` validates the value and writes through the same save path the TUI uses, so migrations and checks still run. `unset` resets one key to its default through that same save path. Unknown keys and invalid values exit non-zero with a clear message. The keys match the option names in the table below. Set `auto_refresh_seconds` to `default` to clear it back to unset. `edit` opens the file in `$VISUAL` or `$EDITOR` (falling back to a platform default) and re-checks it after you save, which is handy for list and map settings that `set` does not cover.
701+
`set` validates the value and writes through the same save path the TUI uses, so migrations and checks still run. `unset` resets one key to its default through that same save path. `export` prints the full JSON config to stdout, or writes it to `--out <file>`. `import` reads JSON from stdin, or from `--in <file>`, then saves it through the normal config path. Unknown keys and invalid values exit non-zero with a clear message. The keys match the option names in the table below. Set `auto_refresh_seconds` to `default` to clear it back to unset. `edit` opens the file in `$VISUAL` or `$EDITOR` (falling back to a platform default) and re-checks it after you save, which is handy for list and map settings that `set` does not cover.
700702

701703
Use `dispatch config validate --path <file>` to check another config file without changing your active profile. The generated schema is published at [`docs/config.schema.json`](docs/config.schema.json); point your editor's JSON Schema integration at that file, or run `dispatch config schema` to emit the same schema from your installed binary.
702704

@@ -721,7 +723,8 @@ Use `dispatch config validate --path <file>` to check another config file withou
721723
| `model` | string | `""` | Pass `--model <name>` to Copilot CLI |
722724
| `launch_mode` | string | `"tab"` | How to open sessions: `in-place`, `tab`, `window`, `pane` |
723725
| `pane_direction` | string | `"auto"` | Split direction for pane mode: `auto`, `right`, `down`, `left`, `up` (see note below) |
724-
| `custom_command` | string | `""` | Custom launch command (`{sessionId}` is replaced) |
726+
| `resume_session_command` | string | `""` | Custom resume command (`{sessionId}` is replaced). Defaults to `copilot --resume` |
727+
| `new_session_command` | string | `""` | Command to launch new sessions (`{cwd}` is replaced). Defaults to `copilot` |
725728
| `excluded_dirs` | array | `[]` | Directory paths to hide from session list |
726729
| `excluded_words` | array | `[]` | Comma-separated words; sessions containing any word are hidden |
727730
| `attention_threshold` | string | `"15m"` | Duration after which an inactive running session is marked stale |
@@ -784,7 +787,8 @@ The split starts in the session's working directory (`-c`) and runs the resume c
784787
"model": "",
785788
"launch_mode": "tab",
786789
"pane_direction": "auto",
787-
"custom_command": "",
790+
"resume_session_command": "",
791+
"new_session_command": "",
788792
"excluded_dirs": [],
789793
"theme": "auto",
790794
"workspace_recovery": true,
@@ -798,12 +802,20 @@ The split starts in the session's working directory (`-c`) and runs the resume c
798802
}
799803
```
800804

801-
### Custom Command
805+
### Resume Session Command
802806

803-
Set `custom_command` to replace the default Copilot CLI launch entirely. Use `{sessionId}` as the placeholder. When set, Agent, Model, and Yolo Mode fields are ignored.
807+
Set `resume_session_command` to replace the default Copilot CLI resume command. Use `{sessionId}` as the placeholder for the session to resume. When set, Agent, Model, and Yolo Mode fields are ignored.
804808

805809
```json
806-
"custom_command": "my-tool resume {sessionId}"
810+
"resume_session_command": "my-tool resume {sessionId}"
811+
```
812+
813+
### New Session Command
814+
815+
Set `new_session_command` to customize the command used when launching a brand new session from dispatch (the `+` keybinding). Use `{cwd}` as the placeholder for the working directory. When empty, defaults to `copilot`.
816+
817+
```json
818+
"new_session_command": "copilot --agent workspace"
807819
```
808820

809821
### Customizing Keybindings

‎SECURITY.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ The application may contact the following domains:
5555

5656
- **Configuration file** (platform config directory, e.g.
5757
`~/.config/dispatch/config.json`): Treated as trusted user
58-
input. If a `custom_command` is configured, it is executed as-is. Keep your
58+
input. If a `resume_session_command` is configured, it is executed as-is. Keep your
5959
config file permissions restricted to your user account.
6060
- **Session data**: Read from the Copilot CLI SQLite database in read-only mode.
6161
Malformed session data cannot cause code execution.

‎cmd/dispatch/cli.go‎

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ var (
4343
// seams so tests can substitute them without touching the environment.
4444
doctorCopilotVersionFn = defaultCopilotVersion
4545
doctorSessionCountFn = defaultSessionCount
46+
doctorWorkspacesFn = defaultDoctorWorkspaces
4647
)
4748

4849
type versionOutput struct {
@@ -442,7 +443,7 @@ _dispatch_completion() {
442443
;;
443444
config)
444445
if [[ "${COMP_CWORD}" -eq 2 ]]; then
445-
COMPREPLY=( $(compgen -W "list get set unset edit path" -- "${cur}") )
446+
COMPREPLY=( $(compgen -W "list get set unset edit path export import" -- "${cur}") )
446447
elif [[ "${COMP_WORDS[2]}" == "get" || "${COMP_WORDS[2]}" == "set" || "${COMP_WORDS[2]}" == "unset" ]]; then
447448
COMPREPLY=( $(compgen -W "$("${bin}" __complete config-keys)" -- "${cur}") )
448449
fi
@@ -458,7 +459,7 @@ _dispatch_completion() {
458459
local -a commands flags configsubs shells aliases configkeys openflags newflags
459460
local bin=${words[1]}
460461
commands=(help version open new doctor update completion stats search tags notes views aliases alias compare prune tag watch config export info path man)
461-
configsubs=(list get set unset edit path)
462+
configsubs=(list get set unset edit path export import)
462463
openflags=(--mode --last --print --agent --model --yolo)
463464
newflags=(--mode --agent --model --yolo)
464465
flags=(-h --help -v --version --demo --clear-cache --reindex --current --cwd --repo --branch --query)
@@ -539,7 +540,7 @@ end
539540
const powershellCompletionScript = `# PowerShell completion for dispatch
540541
$script:DispatchCommands = @('help', 'version', 'open', 'new', 'doctor', 'update', 'completion', 'stats', 'search', 'tags', 'aliases', 'alias', 'compare', 'prune', 'tag', 'watch', 'config', 'export', 'info', 'path', 'man')
541542
$script:DispatchFlags = @('-h', '--help', '-v', '--version', '--demo', '--clear-cache', '--reindex', '--current', '--cwd', '--repo', '--branch', '--query')
542-
$script:DispatchConfigSubcommands = @('list', 'get', 'set', 'unset', 'edit', 'path')
543+
$script:DispatchConfigSubcommands = @('list', 'get', 'set', 'unset', 'edit', 'path', 'export', 'import')
543544
$script:DispatchOpenFlags = @('--mode', '--last', '--print', '--agent', '--model', '--yolo')
544545
$script:DispatchNewFlags = @('--mode', '--agent', '--model', '--yolo')
545546
@@ -588,15 +589,25 @@ type doctorEntry struct {
588589
// Both the text and JSON renderers consume this struct so their outputs stay
589590
// in sync.
590591
type doctorReport struct {
591-
Version string `json:"version"`
592-
OS string `json:"os"`
593-
OK bool `json:"ok"`
594-
Config doctorEntry `json:"config"`
595-
SessionStore doctorEntry `json:"session_store"`
596-
SessionState doctorEntry `json:"session_state"`
597-
CopilotCLI doctorEntry `json:"copilot_cli"`
598-
CopilotVersion string `json:"copilot_version"`
599-
SessionCount int `json:"session_count"`
592+
Version string `json:"version"`
593+
OS string `json:"os"`
594+
OK bool `json:"ok"`
595+
Config doctorEntry `json:"config"`
596+
SessionStore doctorEntry `json:"session_store"`
597+
SessionState doctorEntry `json:"session_state"`
598+
CopilotCLI doctorEntry `json:"copilot_cli"`
599+
CopilotVersion string `json:"copilot_version"`
600+
SessionCount int `json:"session_count"`
601+
Workspaces workspaceReport `json:"workspaces"`
602+
}
603+
604+
// workspaceReport summarizes whether stored session working directories still
605+
// exist on disk.
606+
type workspaceReport struct {
607+
Total int `json:"total"`
608+
Missing int `json:"missing"`
609+
Samples []string `json:"samples,omitempty"`
610+
Error string `json:"error,omitempty"`
600611
}
601612

602613
// collectDoctorReport gathers the environment diagnostics once so they can be
@@ -633,6 +644,7 @@ func collectDoctorReport() doctorReport {
633644
}
634645

635646
r.SessionCount = doctorSessionCountFn()
647+
r.Workspaces = doctorWorkspacesFn()
636648
r.OK = doctorEntryOK(r.Config) &&
637649
doctorEntryOK(r.SessionStore) &&
638650
doctorEntryOK(r.SessionState) &&
@@ -712,6 +724,35 @@ func defaultSessionCount() int {
712724
return n
713725
}
714726

727+
func defaultDoctorWorkspaces() workspaceReport {
728+
store, err := data.Open()
729+
if err != nil {
730+
return workspaceReport{Error: err.Error()}
731+
}
732+
defer store.Close() //nolint:errcheck // read-only, best-effort close
733+
734+
folders, err := store.ListFolders(context.Background())
735+
if err != nil {
736+
return workspaceReport{Error: err.Error()}
737+
}
738+
739+
r := workspaceReport{Total: len(folders)}
740+
for _, folder := range folders {
741+
if strings.TrimSpace(folder) == "" {
742+
continue
743+
}
744+
if _, err := os.Stat(folder); err != nil {
745+
if errors.Is(err, os.ErrNotExist) {
746+
r.Missing++
747+
if len(r.Samples) < 5 {
748+
r.Samples = append(r.Samples, folder)
749+
}
750+
}
751+
}
752+
}
753+
return r
754+
}
755+
715756
// pathStatus stats a path and reports whether it is found, missing, or the
716757
// wrong type (a file where a directory is expected, or vice versa).
717758
func pathStatus(path string, wantDir bool) string {
@@ -748,6 +789,7 @@ func runDoctor(w io.Writer) doctorReport {
748789
fmt.Fprintf(w, "Copilot CLI version: not detected\n")
749790
}
750791
fmt.Fprintf(w, "Stored sessions: %d\n", r.SessionCount)
792+
writeWorkspaceLine(w, r.Workspaces)
751793
fmt.Fprintf(w, "OK: %t\n", r.OK)
752794
return r
753795
}
@@ -791,6 +833,17 @@ func writeDoctorLine(w io.Writer, label string, e doctorEntry, wantDir bool) {
791833
}
792834
}
793835

836+
func writeWorkspaceLine(w io.Writer, r workspaceReport) {
837+
if r.Error != "" {
838+
fmt.Fprintf(w, "Missing workspaces: unknown (%s)\n", r.Error)
839+
return
840+
}
841+
fmt.Fprintf(w, "Missing workspaces: %d of %d folders\n", r.Missing, r.Total)
842+
for _, sample := range r.Samples {
843+
fmt.Fprintf(w, " - %s\n", sample)
844+
}
845+
}
846+
794847
// setupLogRedirect opens the log file (if configured via DISPATCH_LOG) and
795848
// redirects stderr to it. When no log file is configured, stderr is sent to
796849
// os.DevNull to keep Bubble Tea's alt-screen clean. Returns the writer for

‎cmd/dispatch/cli_test.go‎

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -196,10 +196,15 @@ func TestRunDoctor_PrintsDiagnostics(t *testing.T) {
196196
t.Setenv("DISPATCH_DB", db)
197197
t.Setenv("DISPATCH_SESSION_STATE", stateDir)
198198

199-
origCount, origVer := doctorSessionCountFn, doctorCopilotVersionFn
199+
origCount, origVer, origWorkspaces := doctorSessionCountFn, doctorCopilotVersionFn, doctorWorkspacesFn
200200
doctorSessionCountFn = func() int { return 7 }
201201
doctorCopilotVersionFn = func(string) string { return "1.2.3" }
202-
t.Cleanup(func() { doctorSessionCountFn, doctorCopilotVersionFn = origCount, origVer })
202+
doctorWorkspacesFn = func() workspaceReport {
203+
return workspaceReport{Total: 3, Missing: 1, Samples: []string{filepath.Join(stateDir, "missing")}}
204+
}
205+
t.Cleanup(func() {
206+
doctorSessionCountFn, doctorCopilotVersionFn, doctorWorkspacesFn = origCount, origVer, origWorkspaces
207+
})
203208

204209
var buf bytes.Buffer
205210
runDoctor(&buf)
@@ -213,6 +218,7 @@ func TestRunDoctor_PrintsDiagnostics(t *testing.T) {
213218
"Session state: found",
214219
"Copilot CLI:",
215220
"Stored sessions: 7",
221+
"Missing workspaces: 1 of 3 folders",
216222
} {
217223
if !strings.Contains(out, want) {
218224
t.Errorf("doctor output missing %q:\n%s", want, out)
@@ -229,9 +235,10 @@ func TestRunDoctorJSON_Shape(t *testing.T) {
229235
t.Setenv("DISPATCH_DB", db)
230236
t.Setenv("DISPATCH_SESSION_STATE", stateDir)
231237

232-
origCount := doctorSessionCountFn
238+
origCount, origWorkspaces := doctorSessionCountFn, doctorWorkspacesFn
233239
doctorSessionCountFn = func() int { return 3 }
234-
t.Cleanup(func() { doctorSessionCountFn = origCount })
240+
doctorWorkspacesFn = func() workspaceReport { return workspaceReport{Total: 2, Missing: 1, Samples: []string{"missing-dir"}} }
241+
t.Cleanup(func() { doctorSessionCountFn, doctorWorkspacesFn = origCount, origWorkspaces })
235242

236243
var buf bytes.Buffer
237244
if _, err := runDoctorJSON(&buf); err != nil {
@@ -257,11 +264,22 @@ func TestRunDoctorJSON_Shape(t *testing.T) {
257264
if r.SessionCount != 3 {
258265
t.Errorf("session_count = %d, want 3", r.SessionCount)
259266
}
267+
if r.Workspaces.Missing != 1 || len(r.Workspaces.Samples) != 1 {
268+
t.Errorf("workspaces = %+v, want one missing sample", r.Workspaces)
269+
}
260270
if !strings.HasSuffix(buf.String(), "}\n") {
261271
t.Errorf("JSON output should end with a single newline, got:\n%q", buf.String())
262272
}
263273
}
264274

275+
func TestWriteWorkspaceLine_Error(t *testing.T) {
276+
var buf bytes.Buffer
277+
writeWorkspaceLine(&buf, workspaceReport{Error: "store unavailable"})
278+
if !strings.Contains(buf.String(), "Missing workspaces: unknown") {
279+
t.Fatalf("workspace line = %q, want unknown status", buf.String())
280+
}
281+
}
282+
265283
func TestParseDoctorArgs(t *testing.T) {
266284
opts, err := parseDoctorArgs([]string{"doctor", "--json", "--strict"})
267285
if err != nil {

0 commit comments

Comments
 (0)