diff --git a/README.md b/README.md index b77a7c7..1c3f5ad 100644 --- a/README.md +++ b/README.md @@ -183,6 +183,16 @@ Use `--print` to write the resume command to stdout instead of launching it. Thi dispatch open --print ``` +Override the agent, model, or yolo mode for a single resume without changing your saved config with `--agent`, `--model`, and `--yolo`: + +```sh +dispatch open --agent coder --model gpt-5 +dispatch open --last --yolo +dispatch open --yolo=false # force off even if config enables it +``` + +These flags apply to every launch mode and to `--print`. When omitted, the saved config values are used. + ### Start a New Session Start a brand-new Copilot session from the command line without opening the TUI: @@ -191,9 +201,11 @@ Start a brand-new Copilot session from the command line without opening the TUI: dispatch new # start in the current directory dispatch new ~/code/app # start in a specific directory dispatch new --mode tab # override the launch mode (inplace, tab, window, pane) +dispatch new --agent coder --model gpt-5 # override agent/model for this session +dispatch new --yolo # start with yolo mode on for this session only ``` -The session uses the same agent, model, and launch settings as the TUI. When no directory is given, the current working directory is used. +The session uses the same agent, model, and launch settings as the TUI unless you pass `--agent`, `--model`, or `--yolo` to override them for that one launch. When no directory is given, the current working directory is used. ### Shell Completion diff --git a/cmd/dispatch/cli.go b/cmd/dispatch/cli.go index 5673acb..1004baf 100644 --- a/cmd/dispatch/cli.go +++ b/cmd/dispatch/cli.go @@ -372,7 +372,11 @@ _dispatch_completion() { return 0 ;; open) - COMPREPLY=( $(compgen -W "$("${bin}" __complete aliases)" -- "${cur}") ) + COMPREPLY=( $(compgen -W "$("${bin}" __complete aliases) --mode --last --print --agent --model --yolo" -- "${cur}") ) + return 0 + ;; + new) + COMPREPLY=( $(compgen -W "--mode --agent --model --yolo" -- "${cur}") ) return 0 ;; config) @@ -390,10 +394,12 @@ complete -F _dispatch_completion dispatch disp const zshCompletionScript = `#compdef dispatch disp _dispatch_completion() { - local -a commands flags configsubs shells aliases configkeys + local -a commands flags configsubs shells aliases configkeys openflags newflags local bin=${words[1]} commands=(help version open new doctor update completion stats search tags config export info man) configsubs=(list get set unset edit path) + openflags=(--mode --last --print --agent --model --yolo) + newflags=(--mode --agent --model --yolo) flags=(-h --help -v --version --demo --clear-cache --reindex --current --cwd --repo --branch --query) if (( CURRENT == 2 )); then @@ -422,6 +428,16 @@ _dispatch_completion() { fi return fi + + if [[ ${words[2]} == open ]]; then + _describe -t openflags 'open flag' openflags + return + fi + + if [[ ${words[2]} == new ]]; then + _describe -t newflags 'new flag' newflags + return + fi } _dispatch_completion "$@" ` @@ -442,12 +458,19 @@ function __dispatch_config_key test (count $cmd) -ge 3; and test $cmd[2] = config; and contains -- $cmd[3] get set unset end +function __dispatch_using_subcommand + set -l cmd (commandline -opc) + test (count $cmd) -ge 2; and test $cmd[2] = $argv[1] +end + for bin in dispatch disp complete -c $bin -f complete -c $bin -n '__dispatch_needs_command' -a 'help version open new doctor update completion stats search tags config export info man' complete -c $bin -n '__dispatch_needs_command' -a '-h --help -v --version --demo --clear-cache --reindex --current --cwd --repo --branch --query' complete -c $bin -n '__dispatch_after completion' -a "($bin __complete shells)" complete -c $bin -n '__dispatch_after open' -a "($bin __complete aliases)" + complete -c $bin -n '__dispatch_after open' -a '--mode --last --print --agent --model --yolo' + complete -c $bin -n '__dispatch_after new' -a '--mode --agent --model --yolo' complete -c $bin -n '__dispatch_config_key' -a "($bin __complete config-keys)" end ` @@ -456,6 +479,8 @@ const powershellCompletionScript = `# PowerShell completion for dispatch $script:DispatchCommands = @('help', 'version', 'open', 'new', 'doctor', 'update', 'completion', 'stats', 'search', 'tags', 'config', 'export', 'info', 'man') $script:DispatchFlags = @('-h', '--help', '-v', '--version', '--demo', '--clear-cache', '--reindex', '--current', '--cwd', '--repo', '--branch', '--query') $script:DispatchConfigSubcommands = @('list', 'get', 'set', 'unset', 'edit', 'path') +$script:DispatchOpenFlags = @('--mode', '--last', '--print', '--agent', '--model', '--yolo') +$script:DispatchNewFlags = @('--mode', '--agent', '--model', '--yolo') Register-ArgumentCompleter -Native -CommandName dispatch, disp -ScriptBlock { param($wordToComplete, $commandAst, $cursorPosition) @@ -464,13 +489,15 @@ Register-ArgumentCompleter -Native -CommandName dispatch, disp -ScriptBlock { $values = if ($tokens.Count -ge 2 -and $tokens[1] -eq 'completion') { & $bin __complete shells } elseif ($tokens.Count -ge 2 -and $tokens[1] -eq 'open') { - & $bin __complete aliases + (& $bin __complete aliases) + $script:DispatchOpenFlags } elseif ($tokens.Count -ge 2 -and $tokens[1] -eq 'config') { if ($tokens.Count -ge 3 -and @('get', 'set', 'unset') -contains $tokens[2]) { & $bin __complete config-keys } else { $script:DispatchConfigSubcommands } + } elseif ($tokens.Count -ge 2 -and $tokens[1] -eq 'new') { + $script:DispatchNewFlags } else { $script:DispatchCommands + $script:DispatchFlags } diff --git a/cmd/dispatch/launch_overrides.go b/cmd/dispatch/launch_overrides.go new file mode 100644 index 0000000..ab15c8f --- /dev/null +++ b/cmd/dispatch/launch_overrides.go @@ -0,0 +1,91 @@ +package main + +import ( + "fmt" + "strconv" + "strings" + + "github.com/jongio/dispatch/internal/config" +) + +// launchOverrides holds per-invocation overrides for the agent, model, and +// yolo settings parsed from the open and new subcommand flags. A nil pointer +// means the flag was not given, so the saved config value is used for that +// setting. These overrides are never written back to the config file. +type launchOverrides struct { + agent *string + model *string + yolo *bool +} + +// apply copies any set overrides onto cfg. cfg is the freshly loaded config for +// a single launch and is never persisted, so this only affects the current +// invocation. Callers apply overrides before building a platform.ResumeConfig +// so every launch mode and --print see the same values. +func (o launchOverrides) apply(cfg *config.Config) { + if o.agent != nil { + cfg.Agent = *o.agent + } + if o.model != nil { + cfg.Model = *o.model + } + if o.yolo != nil { + cfg.YoloMode = *o.yolo + } +} + +// matchLaunchOverride interprets rest[i] as one of the shared --agent, --model, +// or --yolo override flags used by both the open and new subcommands. It +// returns matched=true when the token is one of those flags, the index the +// caller should continue from (next), and an error for a missing or invalid +// value. When matched is false the caller handles the token itself. Both +// "--flag value" and "--flag=value" forms are accepted; --yolo also accepts a +// bare form (implies true) and an explicit --yolo=true/false value. +func matchLaunchOverride(rest []string, i int, ov *launchOverrides) (matched bool, next int, err error) { + name, inline, hasInline := splitFlag(rest[i]) + + switch name { + case "--agent", "--model": + value, ni, vErr := takeOverrideValue(rest, i, name, inline, hasInline) + if vErr != nil { + return true, i, vErr + } + if name == "--agent" { + ov.agent = &value + } else { + ov.model = &value + } + return true, ni, nil + case "--yolo": + if !hasInline { + enabled := true + ov.yolo = &enabled + return true, i, nil + } + enabled, pErr := strconv.ParseBool(strings.TrimSpace(inline)) + if pErr != nil { + return true, i, fmt.Errorf("invalid value %q for --yolo (want true or false)", inline) + } + ov.yolo = &enabled + return true, i, nil + default: + return false, i, nil + } +} + +// takeOverrideValue resolves the value for a value-taking override flag, +// accepting both "--flag value" and "--flag=value" forms. It returns the index +// the caller should continue from: unchanged for the inline form, advanced past +// the consumed value for the space-separated form. +func takeOverrideValue(rest []string, i int, name, inline string, hasInline bool) (value string, next int, err error) { + if hasInline { + if inline == "" { + return "", i, fmt.Errorf("%s requires a value", name) + } + return inline, i, nil + } + if i+1 >= len(rest) { + return "", i, fmt.Errorf("%s requires a value", name) + } + return rest[i+1], i + 1, nil +} diff --git a/cmd/dispatch/launch_overrides_test.go b/cmd/dispatch/launch_overrides_test.go new file mode 100644 index 0000000..d95ba6f --- /dev/null +++ b/cmd/dispatch/launch_overrides_test.go @@ -0,0 +1,178 @@ +package main + +import ( + "testing" + + "github.com/jongio/dispatch/internal/config" +) + +func TestParseOpenArgs_Overrides(t *testing.T) { + tests := []struct { + name string + args []string + wantID string + wantMode string + wantAgent *string + wantModel *string + wantYolo *bool + wantErr bool + }{ + {name: "agent space", args: []string{"open", "abc", "--agent", "coder"}, wantID: "abc", wantAgent: ptr("coder")}, + {name: "agent equals", args: []string{"open", "abc", "--agent=coder"}, wantID: "abc", wantAgent: ptr("coder")}, + {name: "model space", args: []string{"open", "abc", "--model", "gpt-5"}, wantID: "abc", wantModel: ptr("gpt-5")}, + {name: "model equals", args: []string{"open", "abc", "--model=gpt-5"}, wantID: "abc", wantModel: ptr("gpt-5")}, + {name: "yolo bare", args: []string{"open", "abc", "--yolo"}, wantID: "abc", wantYolo: ptr(true)}, + {name: "yolo true", args: []string{"open", "abc", "--yolo=true"}, wantID: "abc", wantYolo: ptr(true)}, + {name: "yolo false", args: []string{"open", "abc", "--yolo=false"}, wantID: "abc", wantYolo: ptr(false)}, + { + name: "all overrides before id", + args: []string{"open", "--agent", "coder", "--model", "gpt-5", "--yolo", "abc"}, + wantID: "abc", + wantAgent: ptr("coder"), + wantModel: ptr("gpt-5"), + wantYolo: ptr(true), + }, + {name: "override with mode", args: []string{"open", "abc", "--agent", "coder", "--mode", "tab"}, wantID: "abc", wantMode: "tab", wantAgent: ptr("coder")}, + {name: "agent missing value", args: []string{"open", "abc", "--agent"}, wantErr: true}, + {name: "model missing value at end", args: []string{"open", "abc", "--model"}, wantErr: true}, + {name: "agent empty inline", args: []string{"open", "abc", "--agent="}, wantErr: true}, + {name: "yolo invalid", args: []string{"open", "abc", "--yolo=maybe"}, wantErr: true}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + id, mode, _, _, ov, err := parseOpenArgs(tc.args) + if tc.wantErr { + if err == nil { + t.Fatalf("expected error, got id=%q mode=%q", id, mode) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if id != tc.wantID { + t.Errorf("id = %q, want %q", id, tc.wantID) + } + if mode != tc.wantMode { + t.Errorf("mode = %q, want %q", mode, tc.wantMode) + } + assertStrPtr(t, "agent", ov.agent, tc.wantAgent) + assertStrPtr(t, "model", ov.model, tc.wantModel) + assertBoolPtr(t, "yolo", ov.yolo, tc.wantYolo) + }) + } +} + +func TestParseNewArgs_Overrides(t *testing.T) { + tests := []struct { + name string + args []string + wantDir string + wantMode string + wantAgent *string + wantModel *string + wantYolo *bool + wantErr bool + }{ + {name: "agent only", args: []string{"new", "--agent", "coder"}, wantAgent: ptr("coder")}, + {name: "dir plus overrides", args: []string{"new", "/tmp/proj", "--model=gpt-5", "--yolo"}, wantDir: "/tmp/proj", wantModel: ptr("gpt-5"), wantYolo: ptr(true)}, + {name: "override with mode", args: []string{"new", "--mode", "window", "--agent", "coder"}, wantMode: "window", wantAgent: ptr("coder")}, + {name: "model missing value", args: []string{"new", "--model"}, wantErr: true}, + {name: "yolo invalid", args: []string{"new", "--yolo=nope"}, wantErr: true}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + dir, mode, ov, err := parseNewArgs(tc.args) + if tc.wantErr { + if err == nil { + t.Fatalf("expected error, got dir=%q mode=%q", dir, mode) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if dir != tc.wantDir { + t.Errorf("dir = %q, want %q", dir, tc.wantDir) + } + if mode != tc.wantMode { + t.Errorf("mode = %q, want %q", mode, tc.wantMode) + } + assertStrPtr(t, "agent", ov.agent, tc.wantAgent) + assertStrPtr(t, "model", ov.model, tc.wantModel) + assertBoolPtr(t, "yolo", ov.yolo, tc.wantYolo) + }) + } +} + +func TestLaunchOverridesApply(t *testing.T) { + t.Run("empty overrides leave config unchanged", func(t *testing.T) { + cfg := &config.Config{Agent: "base", Model: "base-model", YoloMode: true} + launchOverrides{}.apply(cfg) + if cfg.Agent != "base" || cfg.Model != "base-model" || !cfg.YoloMode { + t.Fatalf("empty overrides mutated config: %+v", cfg) + } + }) + + t.Run("set overrides replace config values", func(t *testing.T) { + cfg := &config.Config{Agent: "base", Model: "base-model", YoloMode: false} + launchOverrides{agent: ptr("coder"), model: ptr("gpt-5"), yolo: ptr(true)}.apply(cfg) + if cfg.Agent != "coder" { + t.Errorf("agent = %q, want %q", cfg.Agent, "coder") + } + if cfg.Model != "gpt-5" { + t.Errorf("model = %q, want %q", cfg.Model, "gpt-5") + } + if !cfg.YoloMode { + t.Error("yolo = false, want true") + } + }) + + t.Run("yolo false override disables enabled config", func(t *testing.T) { + cfg := &config.Config{YoloMode: true} + launchOverrides{yolo: ptr(false)}.apply(cfg) + if cfg.YoloMode { + t.Error("yolo = true, want false") + } + }) +} + +func TestMatchLaunchOverride_NonOverrideFlag(t *testing.T) { + var ov launchOverrides + matched, next, err := matchLaunchOverride([]string{"--mode", "tab"}, 0, &ov) + if matched { + t.Fatal("expected --mode to not match a launch override") + } + if next != 0 || err != nil { + t.Fatalf("unexpected next=%d err=%v", next, err) + } + if ov.agent != nil || ov.model != nil || ov.yolo != nil { + t.Errorf("overrides mutated on non-match: %+v", ov) + } +} + +func ptr[T any](v T) *T { return &v } + +func assertStrPtr(t *testing.T, field string, got, want *string) { + t.Helper() + switch { + case want == nil && got != nil: + t.Errorf("%s = %q, want unset", field, *got) + case want != nil && got == nil: + t.Errorf("%s unset, want %q", field, *want) + case want != nil && got != nil && *got != *want: + t.Errorf("%s = %q, want %q", field, *got, *want) + } +} + +func assertBoolPtr(t *testing.T, field string, got, want *bool) { + t.Helper() + switch { + case want == nil && got != nil: + t.Errorf("%s = %v, want unset", field, *got) + case want != nil && got == nil: + t.Errorf("%s unset, want %v", field, *want) + case want != nil && got != nil && *got != *want: + t.Errorf("%s = %v, want %v", field, *got, *want) + } +} diff --git a/cmd/dispatch/main.go b/cmd/dispatch/main.go index ac8e7d8..ec37736 100644 --- a/cmd/dispatch/main.go +++ b/cmd/dispatch/main.go @@ -135,6 +135,14 @@ Commands: man Write the man page (roff) to standard output update Update dispatch to the latest release +Open and new flags: + --mode Launch mode for this run (inplace, tab, window, pane) + --agent Override the configured agent for this run only + --model Override the configured model for this run only + --yolo[=true|false] Override yolo mode for this run only (bare form implies true) + --last (open only) Resume the most recently active session + --print (open only) Print the resume command instead of launching + Session IDs: Commands that take (open, export) accept a full session ID or any unique prefix of one, like a short git SHA. An ambiguous prefix lists the diff --git a/cmd/dispatch/new.go b/cmd/dispatch/new.go index cd5217c..4b5da38 100644 --- a/cmd/dispatch/new.go +++ b/cmd/dispatch/new.go @@ -27,7 +27,7 @@ func runNew(w io.Writer, args []string) error { w = io.Discard } - dir, modeFlag, err := parseNewArgs(args) + dir, modeFlag, ov, err := parseNewArgs(args) if err != nil { return err } @@ -36,6 +36,7 @@ func runNew(w io.Writer, args []string) error { if err != nil { return fmt.Errorf("loading config: %w", err) } + ov.apply(cfg) mode := resolveOpenMode(modeFlag, cfg) @@ -47,10 +48,11 @@ func runNew(w io.Writer, args []string) error { return newLaunchFn(w, cfg, resolvedDir, mode) } -// parseNewArgs extracts the optional directory and launch mode from the "new" -// subcommand arguments. args[0] is expected to be "new". A missing directory -// means the current working directory. -func parseNewArgs(args []string) (dir, mode string, err error) { +// parseNewArgs extracts the optional directory, launch mode, and any per-launch +// agent/model/yolo overrides from the "new" subcommand arguments. args[0] is +// expected to be "new". A missing directory means the current working +// directory. +func parseNewArgs(args []string) (dir, mode string, ov launchOverrides, err error) { rest := args if len(rest) > 0 { rest = rest[1:] // drop the "new" token @@ -59,17 +61,24 @@ func parseNewArgs(args []string) (dir, mode string, err error) { var positionals []string for i := 0; i < len(rest); i++ { arg := rest[i] + if matched, next, mErr := matchLaunchOverride(rest, i, &ov); matched { + if mErr != nil { + return "", "", launchOverrides{}, mErr + } + i = next + continue + } switch { case arg == "--mode" || arg == "-m": if i+1 >= len(rest) { - return "", "", errors.New("--mode requires a value: inplace, tab, window, or pane") + return "", "", launchOverrides{}, errors.New("--mode requires a value: inplace, tab, window, or pane") } mode = rest[i+1] i++ case strings.HasPrefix(arg, "--mode="): mode = strings.TrimPrefix(arg, "--mode=") case strings.HasPrefix(arg, "-"): - return "", "", fmt.Errorf("unknown flag: %s", arg) + return "", "", launchOverrides{}, fmt.Errorf("unknown flag: %s", arg) default: positionals = append(positionals, arg) } @@ -81,15 +90,15 @@ func parseNewArgs(args []string) (dir, mode string, err error) { case 1: dir = positionals[0] default: - return "", "", fmt.Errorf("new accepts a single directory, got %d arguments", len(positionals)) + return "", "", launchOverrides{}, fmt.Errorf("new accepts a single directory, got %d arguments", len(positionals)) } if mode != "" { if _, mErr := normalizeLaunchMode(mode); mErr != nil { - return "", "", mErr + return "", "", launchOverrides{}, mErr } } - return dir, mode, nil + return dir, mode, ov, nil } // resolveNewDir validates the target directory and returns an absolute path. diff --git a/cmd/dispatch/new_test.go b/cmd/dispatch/new_test.go index 95629c1..bb9c318 100644 --- a/cmd/dispatch/new_test.go +++ b/cmd/dispatch/new_test.go @@ -31,7 +31,7 @@ func TestParseNewArgs(t *testing.T) { } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - dir, mode, err := parseNewArgs(tc.args) + dir, mode, _, err := parseNewArgs(tc.args) if tc.wantErr { if err == nil { t.Fatalf("expected error, got dir=%q mode=%q", dir, mode) diff --git a/cmd/dispatch/open.go b/cmd/dispatch/open.go index 3b2aa09..210c10f 100644 --- a/cmd/dispatch/open.go +++ b/cmd/dispatch/open.go @@ -40,7 +40,7 @@ func runOpen(w io.Writer, args []string) error { w = io.Discard } - id, modeFlag, last, printCmd, stdin, err := parseOpenArgs(args) + id, modeFlag, last, printCmd, stdin, ov, err := parseOpenArgs(args) if err != nil { return err } @@ -49,6 +49,7 @@ func runOpen(w io.Writer, args []string) error { if err != nil { return fmt.Errorf("loading config: %w", err) } + ov.apply(cfg) if stdin { return runOpenBatch(w, cfg, modeFlag, printCmd) @@ -208,9 +209,9 @@ func openResumeConfig(cfg *config.Config, sess *data.Session) platform.ResumeCon } // parseOpenArgs extracts the session ID, optional launch mode, the --last -// flag, the --print flag, and the --stdin flag from the "open" subcommand -// arguments. args[0] is expected to be "open". -func parseOpenArgs(args []string) (id, mode string, last, printCmd, stdin bool, err error) { +// flag, the --print flag, the --stdin flag, and any per-launch agent/model/yolo +// overrides from the "open" subcommand arguments. args[0] is expected to be "open". +func parseOpenArgs(args []string) (id, mode string, last, printCmd, stdin bool, ov launchOverrides, err error) { rest := args if len(rest) > 0 { rest = rest[1:] // drop the "open" token @@ -219,6 +220,13 @@ func parseOpenArgs(args []string) (id, mode string, last, printCmd, stdin bool, var positionals []string for i := 0; i < len(rest); i++ { arg := rest[i] + if matched, next, mErr := matchLaunchOverride(rest, i, &ov); matched { + if mErr != nil { + return "", "", false, false, false, launchOverrides{}, mErr + } + i = next + continue + } switch { case arg == "--last" || arg == "-l": last = true @@ -226,7 +234,7 @@ func parseOpenArgs(args []string) (id, mode string, last, printCmd, stdin bool, stdin = true case arg == "--mode" || arg == "-m": if i+1 >= len(rest) { - return "", "", false, false, false, errors.New("--mode requires a value: inplace, tab, window, or pane") + return "", "", false, false, false, launchOverrides{}, errors.New("--mode requires a value: inplace, tab, window, or pane") } mode = rest[i+1] i++ @@ -235,7 +243,7 @@ func parseOpenArgs(args []string) (id, mode string, last, printCmd, stdin bool, case arg == "--print": printCmd = true case strings.HasPrefix(arg, "-"): - return "", "", false, false, false, fmt.Errorf("unknown flag: %s", arg) + return "", "", false, false, false, launchOverrides{}, fmt.Errorf("unknown flag: %s", arg) default: positionals = append(positionals, arg) } @@ -244,32 +252,32 @@ func parseOpenArgs(args []string) (id, mode string, last, printCmd, stdin bool, switch { case stdin: if last { - return "", "", false, false, false, errors.New("open --stdin cannot be combined with --last") + return "", "", false, false, false, launchOverrides{}, errors.New("open --stdin cannot be combined with --last") } if len(positionals) > 0 { - return "", "", false, false, false, errors.New("open --stdin reads session IDs from standard input; do not also pass an ID") + return "", "", false, false, false, launchOverrides{}, errors.New("open --stdin reads session IDs from standard input; do not also pass an ID") } case last: if len(positionals) > 0 { - return "", "", false, false, false, errors.New("open --last does not take a session ID") + return "", "", false, false, false, launchOverrides{}, errors.New("open --last does not take a session ID") } default: switch len(positionals) { case 0: - return "", "", false, false, false, errors.New("open requires a session ID (or use --last or --stdin)") + return "", "", false, false, false, launchOverrides{}, errors.New("open requires a session ID (or use --last or --stdin)") case 1: id = positionals[0] default: - return "", "", false, false, false, fmt.Errorf("open accepts a single session ID, got %d arguments", len(positionals)) + return "", "", false, false, false, launchOverrides{}, fmt.Errorf("open accepts a single session ID, got %d arguments", len(positionals)) } } if mode != "" { if _, mErr := normalizeLaunchMode(mode); mErr != nil { - return "", "", false, false, false, mErr + return "", "", false, false, false, launchOverrides{}, mErr } } - return id, mode, last, printCmd, stdin, nil + return id, mode, last, printCmd, stdin, ov, nil } // normalizeLaunchMode maps a user-facing mode string to a config launch mode. diff --git a/cmd/dispatch/open_test.go b/cmd/dispatch/open_test.go index 808220d..6d23153 100644 --- a/cmd/dispatch/open_test.go +++ b/cmd/dispatch/open_test.go @@ -50,7 +50,7 @@ func TestParseOpenArgs(t *testing.T) { } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - id, mode, last, printCmd, stdin, err := parseOpenArgs(tc.args) + id, mode, last, printCmd, stdin, _, err := parseOpenArgs(tc.args) if tc.wantErr { if err == nil { t.Fatalf("expected error, got id=%q mode=%q last=%v", id, mode, last) @@ -244,6 +244,37 @@ func TestRunOpen_PrintError(t *testing.T) { } } +func TestRunOpen_OverridesFlowToResumeConfig(t *testing.T) { + cfg := config.Default() + cfg.Agent = "base-agent" + cfg.Model = "base-model" + cfg.YoloMode = false + sess := &data.Session{ID: "sess-1", Cwd: "/tmp/project"} + withOpenStubs(t, cfg, sess, nil) + + var gotRC platform.ResumeConfig + origResume := openResumeCmdFn + openResumeCmdFn = func(id string, rc platform.ResumeConfig) (string, error) { + gotRC = rc + return "copilot --resume " + id, nil + } + t.Cleanup(func() { openResumeCmdFn = origResume }) + + args := []string{"open", "sess-1", "--print", "--agent", "coder", "--model", "gpt-5", "--yolo"} + if err := runOpen(io.Discard, args); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if gotRC.Agent != "coder" { + t.Errorf("agent = %q, want %q", gotRC.Agent, "coder") + } + if gotRC.Model != "gpt-5" { + t.Errorf("model = %q, want %q", gotRC.Model, "gpt-5") + } + if !gotRC.YoloMode { + t.Error("yolo = false, want true") + } +} + func TestRunOpen_NotFound(t *testing.T) { withOpenStubs(t, config.Default(), nil, nil) err := runOpen(io.Discard, []string{"open", "missing"})