feat(opencode): per-agent OpenCode agent selection (--agent) - #1308
feat(opencode): per-agent OpenCode agent selection (--agent)#1308pedramamini wants to merge 1 commit into
Conversation
Adds an "OpenCode Agent" field to the agent configuration panel that runs a Maestro agent as `opencode run --agent <name>`, so it keeps that OpenCode agent's persona, model, and instructions instead of falling back to the provider default. Plugin-contributed agents (oh-my-opencode and friends) work too: OpenCode resolves the name at run time even though `opencode agent list` does not print them. The value is stored in the agent's Custom Arguments rather than a provider config option, because Custom Arguments are per-agent (`session.customArgs`) while config options are shared by every agent on that provider. Living in customArgs also means it flows through every spawn path already (desktop, CLI, Cue, group chat) with no extra plumbing. Also fixes a latent conflict this exposes: config-option and custom args are appended after `buildAgentArgs()` has emitted `readOnlyArgs`, so a user's `--agent <name>` landed after plan mode's `--agent plan` and silently won, defeating read-only enforcement. `applyAgentConfigOverrides()` now takes `readOnlyMode` and drops args that repeat a flag the agent's readOnlyArgs pin (both `--flag value` and `--flag=value` spellings); callers thread their existing read-only state through. Closes #284
📝 WalkthroughWalkthroughThe PR adds an OpenCode agent selector backed by ChangesOpenCode agent selection and read-only execution flow
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant AgentConfigPanel
participant opencodeAgentArg
participant applyAgentConfigOverrides
participant OpenCode
AgentConfigPanel->>opencodeAgentArg: writeOpenCodeAgentArg(customArgs, agentName)
opencodeAgentArg->>applyAgentConfigOverrides: provide customArgs
applyAgentConfigOverrides->>applyAgentConfigOverrides: strip read-only pinned flags
applyAgentConfigOverrides->>OpenCode: resolved CLI arguments
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryAdds per-agent OpenCode agent selection and protects read-only arguments from later overrides.
Confidence Score: 3/5The PR should not merge until Cue read-only spawns also enforce the new conflict filtering. Cue continues appending configured OpenCode agent arguments without enabling read-only pinning, so a later Files Needing Attention: src/main/utils/agent-args.ts and src/main/cue/cue-spawn-builder.ts Important Files Changed
Sequence DiagramsequenceDiagram
participant UI as Agent configuration
participant Session as Session customArgs
participant Builder as buildAgentArgs
participant Overrides as applyAgentConfigOverrides
participant CLI as OpenCode
UI->>Session: "Store --agent <name>"
Builder->>Builder: Add --agent plan when read-only
Builder->>Overrides: Base args and session overrides
Overrides->>Overrides: Remove flags pinned by readOnlyArgs
Overrides->>CLI: Spawn final argv
Reviews (1): Last reviewed commit: "feat(opencode): per-agent OpenCode agent..." | Re-trigger Greptile |
| const readOnlyPinnedFlags = new Set<string>( | ||
| overrides.readOnlyMode ? (agent?.readOnlyArgs ?? []).filter((arg) => arg.startsWith('-')) : [] | ||
| ); |
| // `--flag=value` carries its value inline; `--flag value` eats the next | ||
| // token too, as long as that token isn't itself a flag. | ||
| if (equalsIndex === -1 && i + 1 < args.length && !args[i + 1].startsWith('-')) { |
There was a problem hiding this comment.
Pull request overview
This PR adds per-agent OpenCode primary agent selection by exposing an "OpenCode Agent" field in the agent configuration UI that round-trips to session.customArgs as opencode run --agent <name>. It also hardens read-only (plan) mode by preventing later config/custom-args from overriding flags pinned by an agent's readOnlyArgs (notably OpenCode's --agent plan).
Changes:
- Add shared helpers to read and write OpenCode
--agentinside Custom Arguments, plus UI support inAgentConfigPanel. - Prevent read-only pinned flags from being overridden by later config options or custom args via
applyAgentConfigOverrides(..., readOnlyMode). - Add tests and docs coverage for OpenCode agent selection and read-only pinning behavior.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/shared/opencodeAgentArg.ts | New helper utilities to parse and rewrite OpenCode --agent within per-agent Custom Arguments. |
| src/renderer/components/shared/AgentConfigPanel.tsx | Adds the "OpenCode Agent (optional)" UI field and binds it to Custom Arguments. |
| src/main/utils/context-groomer.ts | Threads readOnlyMode into override application so pinned flags cannot be overridden. |
| src/main/utils/agent-args.ts | Adds readOnlyMode support and strips pinned flags from later overrides to enforce read-only args. |
| src/main/ipc/handlers/tabNaming.ts | Marks tab-naming spawns as read-only for override stripping. |
| src/main/ipc/handlers/process.ts | Forwards readOnlyMode into override resolution for process spawns. |
| src/main/group-chat/group-chat-router.ts | Marks moderator/participant spawns as read-only when applicable so pinned flags are enforced. |
| src/cli/services/agent-spawner.ts | Threads readOnlyMode into CLI override resolution to match desktop behavior. |
| src/tests/shared/opencodeAgentArg.test.ts | New unit tests for reading/writing OpenCode --agent and round-tripping behavior. |
| src/tests/renderer/components/shared/AgentConfigPanel.test.tsx | Adds UI tests for the OpenCode Agent field visibility and behavior. |
| src/tests/main/utils/context-groomer.test.ts | Updates coverage to ensure readOnlyMode is forwarded into override resolution. |
| src/tests/main/utils/agent-args.test.ts | Adds tests validating read-only pinning behavior for conflicting flags. |
| docs/provider-notes.md | Documents OpenCode --agent support and how it behaves with plan mode. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /** Wrap a value in double quotes when it contains whitespace. */ | ||
| function quoteIfNeeded(value: string): string { | ||
| return /\s/.test(value) ? `"${value}"` : value; | ||
| } |
| * Maestro stores the flag inside the per-agent Custom CLI Args string rather | ||
| * than in the provider-level agent config, because Custom CLI Args are | ||
| * per-agent (`session.customArgs`) while config options are shared by every | ||
| * agent using that provider. These helpers let the UI expose a dedicated | ||
| * "OpenCode Agent" field that reads and rewrites just that one token, leaving | ||
| * everything else in the string untouched. |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/renderer/components/shared/AgentConfigPanel.tsx`:
- Around line 679-689: Associate the visible “OpenCode Agent (optional)” label
with the input in the relevant AgentConfigPanel field by adding a programmatic
accessible name, using either aria-label or matching id/htmlFor attributes.
Update the component test to locate this input by role and its accessible name.
In `@src/shared/opencodeAgentArg.ts`:
- Around line 35-38: Update quoteIfNeeded to safely serialize values containing
whitespace or embedded quote characters for compatibility with parseCustomArgs.
Escape or otherwise encode internal quotes while preserving the entire value as
one token, and keep unquoted output for values that require no quoting.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 2ea3562b-6196-4721-9679-53a425a8e333
📒 Files selected for processing (13)
docs/provider-notes.mdsrc/__tests__/main/utils/agent-args.test.tssrc/__tests__/main/utils/context-groomer.test.tssrc/__tests__/renderer/components/shared/AgentConfigPanel.test.tsxsrc/__tests__/shared/opencodeAgentArg.test.tssrc/cli/services/agent-spawner.tssrc/main/group-chat/group-chat-router.tssrc/main/ipc/handlers/process.tssrc/main/ipc/handlers/tabNaming.tssrc/main/utils/agent-args.tssrc/main/utils/context-groomer.tssrc/renderer/components/shared/AgentConfigPanel.tsxsrc/shared/opencodeAgentArg.ts
| <label className="block text-xs font-medium mb-2" style={{ color: theme.colors.textDim }}> | ||
| OpenCode Agent (optional) | ||
| </label> | ||
| <input | ||
| type="text" | ||
| value={readOpenCodeAgentArg(customArgs)} | ||
| onChange={(e) => onCustomArgsChange(writeOpenCodeAgentArg(customArgs, e.target.value))} | ||
| onBlur={onCustomArgsBlur} | ||
| onClick={(e) => e.stopPropagation()} | ||
| placeholder="build" | ||
| className="w-full p-2 rounded border bg-transparent outline-none text-xs font-mono" |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Give the input a programmatic accessible name.
The visible label is not associated with this input. Add aria-label="OpenCode Agent (optional)" or an id and matching htmlFor, then query it by role and name in the component test.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/renderer/components/shared/AgentConfigPanel.tsx` around lines 679 - 689,
Associate the visible “OpenCode Agent (optional)” label with the input in the
relevant AgentConfigPanel field by adding a programmatic accessible name, using
either aria-label or matching id/htmlFor attributes. Update the component test
to locate this input by role and its accessible name.
| /** Wrap a value in double quotes when it contains whitespace. */ | ||
| function quoteIfNeeded(value: string): string { | ||
| return /\s/.test(value) ? `"${value}"` : value; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C3 --type ts '(customArgs|shell-quote|parse.*Args|split.*Args)' src/main src/cli src/sharedRepository: RunMaestro/Maestro
Length of output: 37775
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- opencodeAgentArg.ts ---'
cat -n src/shared/opencodeAgentArg.ts
echo '--- agent-args.ts parseCustomArgs section ---'
sed -n '1,120p' src/main/utils/agent-args.ts | cat -n
echo '--- opencodeAgentArg tests ---'
rg -n -C4 --type ts 'readOpenCodeAgentArg|writeOpenCodeAgentArg|quoteIfNeeded|opencodeAgentArg'Repository: RunMaestro/Maestro
Length of output: 9126
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- tests mentioning OpenCode/opencode/agent arg ---'
rg -n -C4 --type ts 'opencode|OpenCode|open-code|--agent|agent name|readOpenCodeAgentArg|writeOpenCodeAgentArg' src | head -n 300
echo '--- custom args write entrypoints ---'
rg -n -C5 --type ts 'setCustomArgs|getOpenCodeAgentArg|writeOpenCodeAgentArg|customArgs.*quoteIfNeeded|quoteIfNeeded' src | head -n 300
echo '--- behavioral probe of matching tokenization semantics ---'
node - <<'JS'
function parseCustomArgs(customArgs) {
if (!customArgs || typeof customArgs !== 'string') return [];
const customArgsArray = customArgs.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || [];
return customArgsArray.map((arg) => {
if ((arg.startsWith('"') && arg.endsWith('"')) || (arg.startsWith("'") && arg.endsWith("'"))) {
return arg.slice(1, -1);
}
return arg;
});
}
function quoteIfNeeded(value) {
return /\s/.test(value) ? `"${value}"` : value;
}
for (const name of ['foo"', 'my "agent"', 'name with "quotes"', 'foo"bar']) {
const serialized = ['--agent', quoteIfNeeded(name)].join(' ');
const parsed = parseCustomArgs(serialized);
console.log(JSON.stringify({ name, serialized, parsed: parsed.slice(parsed.indexOf('--agent') + 1) }));
}
JSRepository: RunMaestro/Maestro
Length of output: 50376
Handle embedded quotes and unpaired whitespace in the OpenCode agent serializer.
quoteIfNeeded still serializes names such as foo"bar as --agent foo"bar and names with embedded quotes like my "agent" as --agent "my "agent", which this custom-args parser would break into tokens or strip quotes for. Use a serializer matching parseCustomArgs or escape/split token values that contain quote or space characters.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/shared/opencodeAgentArg.ts` around lines 35 - 38, Update quoteIfNeeded to
safely serialize values containing whitespace or embedded quote characters for
compatibility with parseCustomArgs. Escape or otherwise encode internal quotes
while preserving the entire value as one token, and keep unquoted output for
values that require no quoting.
Closes #284
Problem
Maestro spawns OpenCode with
opencode run ...and never passes--agent, so every Maestro agent runs OpenCode's default primary agent. There was no way to pin a Maestro agent to a specific OpenCode agent (build,plan, or the ones plugins like oh-my-opencode register), which is what #284 asks for: reuse the agents OpenCode already defines instead of recreating personas as Maestro nudge instructions.What this adds
An OpenCode Agent field in the agent configuration panel (New Agent, Edit Agent, Wizard, Group Chat, Encore). Setting it runs that Maestro agent as
opencode run --agent <name>, so it keeps the OpenCode agent's persona, model, and instructions.opencode agent list(per the OpenCode bug linked in the issue thread), so a discovery-driven picker would hide exactly the agents this issue is about.session.customArgs), which is per-agent, while agent config options are shared by every agent on the provider. Two-way bound, so--agent footyped directly into Custom Arguments shows up in the field and vice versa.The workflow from the issue thread now works end to end: create "Project X: Prometheus" and "Project X: Sisyphus", point each at its OpenCode agent, and group chat across them.
Bug this exposed
Config options and custom args are appended after
buildAgentArgs()has already emittedreadOnlyArgs. For OpenCode, plan mode is--agent plan, so a user's--agent prometheuslanded after it and silently won: read-only mode was defeated with no warning. This is reachable today by anyone who puts--agentin Custom Arguments.applyAgentConfigOverrides()now acceptsreadOnlyModeand drops any config-option or custom arg that repeats a flag the agent'sreadOnlyArgspin (both--flag valueand--flag=valuespellings). Callers thread their existing read-only state through. Nothing else changes: no other agent's config option orreadOnlyArgsoverlap today, so this only bites the conflict case.Testing
src/__tests__/shared/opencodeAgentArg.test.ts(read/write/round-trip, quoting, duplicates, removal).agent-args.test.ts.AgentConfigPanel.test.tsx.npm run lintandnpm run lint:eslintclean.Notes for review
readOnlyMode: true, so they now ignore a configured--agentrather than running the user's persona agent for a title. That seemed clearly right, flagging it in case you disagree.Summary by CodeRabbit
New Features
Bug Fixes
Documentation