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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -443,11 +443,12 @@ dispatch config list # print every setting and its value
dispatch config list --json # same, as a single JSON object
dispatch config get launch_mode # print one value
dispatch config set launch_mode window
dispatch config unset launch_mode # reset one setting to its default
dispatch config edit # open the config file in your editor
dispatch config path # print the config file path
```

`set` validates the value and writes through the same save path the TUI uses, so migrations and checks still run. 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.
`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.

### Options

Expand Down
6 changes: 3 additions & 3 deletions cmd/dispatch/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ _dispatch_completion() {
fi

if [[ "${COMP_WORDS[1]}" == "config" ]]; then
COMPREPLY=( $(compgen -W "list get set edit path" -- "${cur}") )
COMPREPLY=( $(compgen -W "list get set unset edit path" -- "${cur}") )
return 0
fi
}
Expand All @@ -350,7 +350,7 @@ _dispatch_completion() {
local -a commands shells flags configsubs
commands=(help version open new doctor update completion stats search tags config export)
shells=(bash zsh fish powershell)
configsubs=(list get set edit path)
configsubs=(list get set unset edit path)
flags=(-h --help -v --version --demo --clear-cache --reindex --current --cwd --repo --branch --query)

if (( CURRENT == 2 )); then
Expand Down Expand Up @@ -394,7 +394,7 @@ const powershellCompletionScript = `# PowerShell completion for dispatch
$script:DispatchCommands = @('help', 'version', 'open', 'new', 'doctor', 'update', 'completion', 'stats', 'search', 'tags', 'config', 'export')
$script:DispatchFlags = @('-h', '--help', '-v', '--version', '--demo', '--clear-cache', '--reindex', '--current', '--cwd', '--repo', '--branch', '--query')
$script:DispatchShells = @('bash', 'zsh', 'fish', 'powershell')
$script:DispatchConfigSubcommands = @('list', 'get', 'set', 'edit', 'path')
$script:DispatchConfigSubcommands = @('list', 'get', 'set', 'unset', 'edit', 'path')

Register-ArgumentCompleter -Native -CommandName dispatch, disp -ScriptBlock {
param($wordToComplete, $commandAst, $cursorPosition)
Expand Down
36 changes: 35 additions & 1 deletion cmd/dispatch/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,12 +251,14 @@ func runConfig(w io.Writer, args []string) error {
return runConfigGet(w, rest)
case "set":
return runConfigSet(w, rest)
case "unset":
return runConfigUnset(w, rest)
case "edit":
return runConfigEdit(w, rest)
case "path":
return runConfigPath(w, rest)
default:
return fmt.Errorf("unknown config subcommand %q (want list, get, set, edit, or path)", sub)
return fmt.Errorf("unknown config subcommand %q (want list, get, set, unset, edit, or path)", sub)
}
}

Expand Down Expand Up @@ -354,6 +356,38 @@ func runConfigSet(w io.Writer, args []string) error {
return nil
}

// runConfigUnset resets a single setting to its default value and persists the
// change through the existing save path. It applies the value the setting has
// in a freshly built default config, reusing each field's validated set so the
// reset behaves exactly like an explicit set of the default.
func runConfigUnset(w io.Writer, args []string) error {
if len(args) == 0 {
return fmt.Errorf("config unset requires a key (see config list for keys)")
}
if len(args) > 1 {
return fmt.Errorf("config unset takes a single key, got %d arguments", len(args))
}
key := args[0]

field, ok := findConfigField(key)
if !ok {
return unknownConfigKeyErr(key)
}

cfg, err := configLoadFn()
if err != nil {
return err
}
if err := field.set(cfg, field.get(config.Default())); err != nil {
return err
}
if err := configSaveFn(cfg); err != nil {
return err
}
fmt.Fprintf(w, "%s = %s\n", field.name, field.get(cfg))
return nil
}

// runConfigPath prints the resolved config file path.
func runConfigPath(w io.Writer, args []string) error {
if len(args) > 0 {
Expand Down
73 changes: 73 additions & 0 deletions cmd/dispatch/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,79 @@ func TestRunConfigSet_SaveError(t *testing.T) {
}
}

func TestRunConfigUnset_ScalarResetsToDefault(t *testing.T) {
cfg := config.Default()
cfg.MaxSessions = 250
cfg = withConfigSeams(t, cfg)

var buf bytes.Buffer
if err := runConfig(&buf, []string{"config", "unset", "max_sessions"}); err != nil {
t.Fatalf("runConfig unset: %v", err)
}
if cfg.MaxSessions != 100 {
t.Errorf("MaxSessions = %d, want default 100 after unset", cfg.MaxSessions)
}
if !strings.Contains(buf.String(), "max_sessions = 100") {
t.Errorf("unset output = %q, want default confirmation", buf.String())
}

// After unset, get reports the default value.
var getBuf bytes.Buffer
if err := runConfig(&getBuf, []string{"config", "get", "max_sessions"}); err != nil {
t.Fatalf("runConfig get after unset: %v", err)
}
if strings.TrimSpace(getBuf.String()) != "100" {
t.Errorf("get after unset = %q, want 100", getBuf.String())
}
}

func TestRunConfigUnset_PointerFieldResetsToUnset(t *testing.T) {
cfg := config.Default()
n := 45
cfg.AutoRefreshSeconds = &n
cfg = withConfigSeams(t, cfg)

if err := runConfig(&bytes.Buffer{}, []string{"config", "unset", "auto_refresh_seconds"}); err != nil {
t.Fatalf("runConfig unset auto_refresh_seconds: %v", err)
}
if cfg.AutoRefreshSeconds != nil {
t.Errorf("AutoRefreshSeconds = %v, want nil after unset", *cfg.AutoRefreshSeconds)
}
}

func TestRunConfigUnset_UnknownKey(t *testing.T) {
withConfigSeams(t, config.Default())
err := runConfig(&bytes.Buffer{}, []string{"config", "unset", "nope"})
if err == nil || !strings.Contains(err.Error(), "unknown config key") {
t.Errorf("error = %v, want unknown config key", err)
}
}

func TestRunConfigUnset_RequiresKey(t *testing.T) {
withConfigSeams(t, config.Default())
if err := runConfig(&bytes.Buffer{}, []string{"config", "unset"}); err == nil {
t.Fatal("expected error when unset is missing a key")
}
}

func TestRunConfigUnset_TooManyArgs(t *testing.T) {
withConfigSeams(t, config.Default())
err := runConfig(&bytes.Buffer{}, []string{"config", "unset", "agent", "model"})
if err == nil || !strings.Contains(err.Error(), "single key") {
t.Errorf("error = %v, want single-key guidance", err)
}
}

func TestRunConfigUnset_SaveError(t *testing.T) {
withConfigSeams(t, config.Default())
configSaveFn = func(*config.Config) error { return errors.New("disk full") }

err := runConfig(&bytes.Buffer{}, []string{"config", "unset", "agent"})
if err == nil || !strings.Contains(err.Error(), "disk full") {
t.Errorf("error = %v, want save failure", err)
}
}

func TestRunConfigPath(t *testing.T) {
withConfigSeams(t, config.Default())

Expand Down
1 change: 1 addition & 0 deletions cmd/dispatch/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ Config commands:
config list [--json] Print every setting and its value
config get <key> Print one setting value
config set <key> <val> Validate and save one setting
config unset <key> Reset one setting to its default
config edit Open the config file in your editor
config path Print the config file path

Expand Down
5 changes: 4 additions & 1 deletion cmd/dispatch/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,10 @@ func TestShiftDemoTimestamps_AlreadyRecent(t *testing.T) {
// ---------------------------------------------------------------------------

func TestPrintUsage_Output(t *testing.T) {
// Capture stdout.
// Capture stdout. The pipe must be drained from a separate goroutine while
// printUsage writes: on Windows the pipe buffer is only a few KB and the
// usage text is larger, so draining after printUsage returned would block
// the write and deadlock.
r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
Expand Down