Skip to content

Commit 80d4b03

Browse files
feat(setup): persist command defaults (#59)
1 parent b95549e commit 80d4b03

File tree

12 files changed

+409
-31
lines changed

12 files changed

+409
-31
lines changed

README.md

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,17 +93,30 @@ To check your configured providers:
9393
| `/codex:status` | `/opencode:status` | Show running/recent jobs |
9494
| `/codex:result` | `/opencode:result` | Show finished job output |
9595
| `/codex:cancel` | `/opencode:cancel` | Cancel active background job |
96-
| `/codex:setup` | `/opencode:setup` | Check install/auth, toggle review gate |
96+
| `/codex:setup` | `/opencode:setup` | Check install/auth, configure defaults, toggle review gate |
9797

9898
## Slash Commands
9999

100-
- `/opencode:review` -- Normal OpenCode code review (read-only). Supports `--base <ref>`, `--pr <number>`, `--model <provider/model>`, `--free`, `--wait`, and `--background`.
101-
- `/opencode:adversarial-review` -- Steerable review that challenges implementation and design decisions. Supports `--base <ref>`, `--pr <number>`, `--model <provider/model>`, `--free`, `--wait`, `--background`, and custom focus text.
102-
- `/opencode:rescue` -- Delegates a task to OpenCode via the `safe-command.mjs` bridge, which validates flags and feeds the task text through a shell-insulated heredoc. Supports `--model`, `--free`, `--agent`, `--resume`, `--fresh`, `--worktree`, `--wait`, and `--background`. Foreground is the default; `--wait` is an explicit no-op alias for foreground; `--background` detaches a worker and returns a job id you can poll with `/opencode:status`.
100+
- `/opencode:review` -- Normal OpenCode code review (read-only). Supports `--base <ref>`, `--pr <number>`, `--model <provider/model>`, `--free`, `--wait`, and `--background`. Uses the saved default model when configured and no runtime model flag is supplied.
101+
- `/opencode:adversarial-review` -- Steerable review that challenges implementation and design decisions. Supports `--base <ref>`, `--pr <number>`, `--model <provider/model>`, `--free`, `--wait`, `--background`, and custom focus text. Uses the saved default model when configured and no runtime model flag is supplied.
102+
- `/opencode:rescue` -- Delegates a task to OpenCode via the `safe-command.mjs` bridge, which validates flags and feeds the task text through a shell-insulated heredoc. Supports `--model`, `--free`, `--agent`, `--resume`, `--fresh`, `--worktree`, `--wait`, and `--background`. Foreground is the default; `--wait` is an explicit no-op alias for foreground; `--background` detaches a worker and returns a job id you can poll with `/opencode:status`. Uses saved default model/agent values when configured and no runtime flag is supplied.
103103
- `/opencode:status` -- Shows running/recent OpenCode jobs for the current repo.
104104
- `/opencode:result` -- Shows final output for a finished job, including OpenCode session ID for resuming.
105105
- `/opencode:cancel` -- Cancels an active OpenCode job.
106-
- `/opencode:setup` -- Checks OpenCode install/auth, can enable/disable the review gate hook, and can configure review-gate throttles.
106+
- `/opencode:setup` -- Checks OpenCode install/auth, can configure default model/agent values, can enable/disable the review gate hook, and can configure review-gate throttles.
107+
108+
## Command Defaults
109+
110+
Persist model and rescue-agent defaults with setup:
111+
112+
```
113+
/opencode:setup --default-model anthropic/claude-opus-4-6 --default-agent build
114+
/opencode:setup --default-model off
115+
/opencode:setup --default-agent off
116+
```
117+
118+
- `--default-model <provider/model>` applies to `/opencode:review`, `/opencode:adversarial-review`, and `/opencode:rescue` unless a command includes `--model` or `--free`.
119+
- `--default-agent <build|plan>` applies to `/opencode:rescue` unless the command includes `--agent`. Review commands keep using the bundled read-only review agent.
107120

108121
## Review Gate
109122

plugins/opencode/commands/adversarial-review.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Argument handling:
3737
- Do not strip `--wait`, `--background`, `--model`, `--free`, or `--pr` yourself.
3838
- Adversarial reviews support custom focus text. Any text after flags is treated as a focus area.
3939
- The companion script handles `--adversarial` internally.
40-
- `--model <id>` overrides OpenCode's default model for this single review (e.g. `--model openrouter/anthropic/claude-opus-4-6`). Pass it through verbatim if the user supplied it.
40+
- `--model <id>` overrides the saved setup default model and OpenCode's own default model for this single review (e.g. `--model openrouter/anthropic/claude-opus-4-6`). Pass it through verbatim if the user supplied it.
4141
- `--free` tells the companion script to shell out to `opencode models`, filter for first-party `opencode/*` free-tier models (those ending in `:free` or `-free`), and pick one at random for this review. Restricted to the `opencode/*` provider because OpenRouter free-tier models have inconsistent tool-use support, and the review agent needs `read`/`grep`/`glob`/`list`. Pass it through verbatim if the user supplied it. `--free` and `--model` are mutually exclusive — the companion will error if both are given.
4242
- `--pr <number>` reviews a GitHub pull request via `gh pr diff` instead of the local working tree. The cwd must be a git repo whose remote points at the PR's repository, and `gh` must be installed and authenticated.
4343

plugins/opencode/commands/rescue.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ Flag handling (all of these are recognized, validated, and forwarded by `safe-co
3434
- `--worktree` — run OpenCode in an isolated git worktree instead of editing the working directory in-place.
3535
- `--resume` (or `--resume-last`) — continue the most recent OpenCode session from this Claude session. The bridge translates `--resume` into the companion-native `--resume-last`.
3636
- `--fresh` — explicit marker that the task must NOT resume. The bridge strips it (the absence of `--resume-last` already conveys "fresh").
37-
- `--model <provider/model-id>` — override OpenCode's default model for this single task. Value must match `[A-Za-z0-9._/:-]+`.
37+
- `--model <provider/model-id>` — override OpenCode's default model for this single task. Value must match `[A-Za-z0-9._/:-]+`. When `--model` and `--free` are both omitted, the companion applies the saved default model from `/opencode:setup --default-model` if one is configured.
3838
- `--free` — tells the companion to pick a random first-party `opencode/*` free-tier model from `opencode models`. Restricted to `opencode/*` because OpenRouter free models have inconsistent tool-use support.
39-
- `--agent <build|plan>` — override the OpenCode agent. Value must be `build` or `plan`.
39+
- `--agent <build|plan>` — override the OpenCode agent. Value must be `build` or `plan`. When `--agent` is omitted, the companion applies the saved default agent from `/opencode:setup --default-agent` if one is configured.
4040
- `--free` and `--model` are mutually exclusive — the bridge rejects payloads that include both. If the user supplies both, return the bridge's error verbatim and stop.
4141

4242
Resume detection (runs before the final bridged call, only when neither `--resume` nor `--fresh` is in the raw user request):

plugins/opencode/commands/review.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ Argument handling:
3939
- The companion script parses `--wait` and `--background`, but Claude Code's `Bash(..., run_in_background: true)` is what actually detaches the run.
4040
- `/opencode:review` is native-review only. It does not support staged-only review, unstaged-only review, or extra focus text.
4141
- If the user needs custom review instructions or more adversarial framing, they should use `/opencode:adversarial-review`.
42-
- `--model <id>` overrides OpenCode's default model for this single review (e.g. `--model openrouter/anthropic/claude-opus-4-6`). Pass it through verbatim if the user supplied it.
42+
- `--model <id>` overrides the saved setup default model and OpenCode's own default model for this single review (e.g. `--model openrouter/anthropic/claude-opus-4-6`). Pass it through verbatim if the user supplied it.
4343
- `--free` tells the companion script to shell out to `opencode models`, filter for first-party `opencode/*` free-tier models (those ending in `:free` or `-free`), and pick one at random for this review. Restricted to the `opencode/*` provider because OpenRouter free-tier models have inconsistent tool-use support, and the review agent needs `read`/`grep`/`glob`/`list`. Pass it through verbatim if the user supplied it. `--free` and `--model` are mutually exclusive — the companion will error if both are given.
4444
- `--pr <number>` reviews a GitHub pull request via `gh pr diff` instead of the local working tree. The cwd must be a git repo whose remote points at the PR's repository, and `gh` must be installed and authenticated. Pass it through verbatim if the user supplied it.
4545
- **PR reference extraction (REQUIRED)**: if the user's input contains a PR reference like `PR #390`, `pr #390`, `PR 390`, or `pr 390` (e.g. `/opencode:review on PR #390`), you MUST extract the number yourself and pass it as `--pr 390`. Do not pass `PR #390` literally to bash — bash strips unquoted `#NNN` tokens as comments before they reach the companion script. Example: `node ... review --pr 390`, NOT `node ... review on PR #390`.

plugins/opencode/commands/setup.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
2-
description: Check whether the local OpenCode CLI is ready and optionally toggle the stop-time review gate
3-
argument-hint: '[--enable-review-gate|--disable-review-gate] [--review-gate-max <n|off>] [--review-gate-cooldown <minutes|off>]'
2+
description: Check whether the local OpenCode CLI is ready, configure defaults, and optionally toggle the stop-time review gate
3+
argument-hint: '[--default-model <provider/model|off>] [--default-agent <build|plan|off>] [--enable-review-gate|--disable-review-gate] [--review-gate-max <n|off>] [--review-gate-cooldown <minutes|off>]'
44
allowed-tools: Bash(node:*), Bash(npm:*), Bash(brew:*), Bash(curl:*), AskUserQuestion
55
---
66

@@ -39,3 +39,5 @@ Output rules:
3939
- Present the final setup output to the user.
4040
- If installation was skipped, present the original setup output.
4141
- If OpenCode is installed but no provider is configured, guide the user to run `!opencode providers` to set up authentication.
42+
- `--default-model <provider/model>` sets the model used by `/opencode:review`, `/opencode:adversarial-review`, and `/opencode:rescue` when no `--model` or `--free` flag is supplied. Use `--default-model off` to clear it.
43+
- `--default-agent <build|plan>` sets the rescue/task agent used when no `--agent` flag is supplied. Review commands keep using the bundled read-only review agent. Use `--default-agent off` to clear it.
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Persistent command-default helpers for the OpenCode companion.
2+
//
3+
// `/opencode:setup` can persist defaults in `state.config.defaults`.
4+
// These helpers keep precedence rules centralized and testable:
5+
// explicit runtime flags win, otherwise saved defaults apply.
6+
7+
import { parseModelString } from "./model.mjs";
8+
9+
const SUPPORTED_DEFAULT_AGENTS = new Set(["build", "plan"]);
10+
11+
/**
12+
* @param {Record<string, unknown>|undefined|null} options
13+
* @param {string} key
14+
* @returns {boolean}
15+
*/
16+
export function hasOwnOption(options, key) {
17+
return Object.prototype.hasOwnProperty.call(options ?? {}, key);
18+
}
19+
20+
/**
21+
* Normalize persisted defaults read from state. Invalid or missing values are
22+
* ignored so a hand-edited state file cannot break every command invocation.
23+
* @param {unknown} raw
24+
* @returns {{ model: string | null, agent: string | null }}
25+
*/
26+
export function normalizeDefaults(raw) {
27+
const defaults = raw && typeof raw === "object" ? raw : {};
28+
29+
const modelRaw = typeof defaults.model === "string" ? defaults.model.trim() : "";
30+
const model = modelRaw && parseModelString(modelRaw) ? modelRaw : null;
31+
32+
const agentRaw = typeof defaults.agent === "string" ? defaults.agent.trim() : "";
33+
const agent = SUPPORTED_DEFAULT_AGENTS.has(agentRaw) ? agentRaw : null;
34+
35+
return { model, agent };
36+
}
37+
38+
/**
39+
* Parse a `/opencode:setup --default-model` value. Returns null for "off".
40+
* @param {unknown} value
41+
* @returns {string | null}
42+
*/
43+
export function parseDefaultModelSetting(value) {
44+
const raw = typeof value === "string" ? value.trim() : "";
45+
if (raw === "off") return null;
46+
if (!raw || !parseModelString(raw)) {
47+
throw new Error(
48+
`--default-model must be "off" or a provider/model-id value ` +
49+
`(e.g. anthropic/claude-opus-4-6).`
50+
);
51+
}
52+
return raw;
53+
}
54+
55+
/**
56+
* Parse a `/opencode:setup --default-agent` value. Returns null for "off".
57+
* @param {unknown} value
58+
* @returns {"build" | "plan" | null}
59+
*/
60+
export function parseDefaultAgentSetting(value) {
61+
const raw = typeof value === "string" ? value.trim() : "";
62+
if (raw === "off") return null;
63+
if (!SUPPORTED_DEFAULT_AGENTS.has(raw)) {
64+
throw new Error(`--default-agent must be "build", "plan", or "off".`);
65+
}
66+
return raw;
67+
}
68+
69+
/**
70+
* Apply a persisted model default when the user did not explicitly supply
71+
* either `--model` or `--free`.
72+
* @param {Record<string, unknown>} options
73+
* @param {{ model?: string | null }} defaults
74+
* @returns {Record<string, unknown>}
75+
*/
76+
export function applyDefaultModelOptions(options, defaults) {
77+
if (hasOwnOption(options, "model") || options?.free) return options;
78+
if (!defaults?.model) return options;
79+
return { ...options, model: defaults.model };
80+
}
81+
82+
/**
83+
* Resolve the task agent using explicit CLI args first, then persisted
84+
* defaults, then the existing write/read-only fallback.
85+
* @param {Record<string, unknown>} options
86+
* @param {{ agent?: string | null }} defaults
87+
* @param {boolean} isWrite
88+
* @returns {string}
89+
*/
90+
export function resolveTaskAgentName(options, defaults, isWrite) {
91+
if (hasOwnOption(options, "agent")) return options.agent;
92+
if (defaults?.agent) return defaults.agent;
93+
return isWrite ? "build" : "plan";
94+
}

plugins/opencode/scripts/lib/render.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,10 @@ export function renderSetup(status) {
207207
} else if (status.installed) {
208208
lines.push(`- **Providers**: None configured. Run \`!opencode providers\` to set up.`);
209209
}
210+
if (status.defaults) {
211+
lines.push(`- **Default Model**: ${status.defaults.model ? `\`${status.defaults.model}\`` : "Unset"}`);
212+
lines.push(`- **Default Agent**: ${status.defaults.agent ? `\`${status.defaults.agent}\`` : "Unset"}`);
213+
}
210214
if (status.reviewGate !== undefined) {
211215
const parts = [status.reviewGate ? "Enabled" : "Disabled"];
212216
if (status.reviewGateMaxPerSession != null) {

0 commit comments

Comments
 (0)