Repository navigation
Description
Summary
In OpenCode Desktop 2.0.11, defining a project-local custom agent (.opencode/agents/<name>.md, mode: primary) does not make the composer agent picker appear. With default settings there is no way to switch to it: the composer agent control is absent, /agent does nothing, agent.cycle (Ctrl+.) is disabled, and it is filtered out of the command palette. The client decides visibility from a native flag that the public API never returns.
Environment
- opencode version: 2.0.11 (desktop app; bundled
opencode-cli2.0.11) - OS: Windows 10.0.26200.9457 (win32 x64)
- Terminal: Unavailable: desktop (Electron) client, no terminal attached
- Shell: C:\WINDOWS\system32\cmd.exe
- Install/channel: release (OpenCode Desktop 2.0.11)
- Active plugins: none configured
Reproduction
- Create a project-local custom agent, e.g.
.opencode/agents/pipeline.mdwithmode: primary(any custom primary agent reproduces). - Open that project in OpenCode Desktop 2.0.11 with default settings (
Settings → General → Show agent= off). - Composer shows no agent control; typing
/agentdoes nothing;Ctrl+Phas no "Switch agent";Ctrl+.does nothing. - Enable
Settings → General → Show agent(general.showCustomAgents). The control and all entry points appear immediately.
Expected Behavior
A custom primary agent defined in the project should make the agent picker visible even when showCustomAgents is off. That is exactly what hasCustomAgent() exists for.
Actual Behavior
The picker stays hidden because hasCustomAgent() always returns false.
packages/app/src/context/local.tsx:72
const agentsVisible = createMemo(() => settings.visibility.customAgents() || hasCustomAgent(list()))packages/app/src/context/local-agent.ts
export function hasCustomAgent(items: Array<{ native?: boolean }>) {
return items.some((item) => item.native === false)
}settings.visibility.customAgents()defaults tofalse(packages/app/src/context/settings.tsx,general.showCustomAgents: false).list()comes from the API, and the publicAgent.Infoschema has nonativefield.
Verified against the running 2.0.11 server:
GET /api/agent?location[directory]=<project>
returns 20 entries including the custom pipeline agent, each with keys:
id, name, model, request, system, description, mode, hidden, color, steps, permissions
Notice there is no native. /openapi.json → components.schemas["Agent.Info"].properties is the same list, with required: id, name, request, mode, hidden, permissions.
The server does know the flag internally — packages/opencode/src/agent/agent.ts declares native: Schema.optional(Schema.Boolean) (line 39), sets native: true for the built-ins (lines 154, 180, 194, 217, 222, 238, 254) and native: false for custom agents (line 279) — but it is dropped from the public Agent.Info schema, so the client can never observe it.
Impact: any user with a custom agent (project or global) gets no agent picker with default settings, and Ctrl+., /agent and the palette entry are all inert.
Additional Context
- Workaround: enable
Settings → General → Show agent(i18nsettings.general.row.showCustomAgents, zh-CN "显示智能体"). The custom agent itself is registered correctly and selectable afterwards, so only the visibility gate is wrong. - Reproducible consistently.
- Related: fix(app): show picker for custom agents #37138 (show picker for custom agents), fix(app): show agent picker by default in new layout #32277, Agent picker (build/plan) missing in v2 layout #30360 — this looks like the remaining gap for custom agents.
- Suggested fix: expose
nativeon the publicAgent.Infoschema, or basehasCustomAgenton data the API actually returns.
Activity
aron-intframe commentedon Sep 21, 2026
Confirmed on current
main(0e3dfd1), and the reason it only bites the desktop app is that this is a v2-protocol gap, not a client-only bug.Cause. The desktop app loads agents through the v2 route
GET /api/agent(packages/protocol/src/groups/agent.ts), whose response type isAgentV2.Infofrompackages/schema/src/agent.ts. That schema has nonativefield, and the v2 core never records one either: built-ins are registered inpackages/core/src/plugin/agent.ts(draft.update(AgentV2.ID.make("plan"), ...)) and custom.opencode/agents/*.mdagents inpackages/core/src/config/plugin/agent.ts, and neither site marks origin (grep -rn nativeoverpackages/schema/src/agent.ts packages/core/src/agent.ts packages/core/src/plugin/agent.ts packages/core/src/config/plugin/agent.ts packages/server/src/handlers/agent.tsreturns nothing). The app then converts the v2 payload withnormalizeAgentListinpackages/app/src/context/global-sync/utils.ts:15-37, which mapsid -> name,permissions -> permission,system -> promptetc. but has nonativeline, sohasCustomAgent(list())inlocal.tsx:72is alwaysfalseand the picker stays hidden.On the v1 path (
legacy.app.agents()inglobal-sync/bootstrap.ts:269) the internalAgent.Infofrompackages/opencode/src/agent/agent.tsstill carriesnative: true/false, sohasCustomAgentdoes its job against a legacy server and only the v2 desktop client is affected.Repro (the two app functions copied verbatim with types stripped, fed a v2-shaped and a v1-shaped list for the same
build+ custompipelineagents):Fix. Two pieces are needed, the client-side map alone cannot recover the information:
native: Schema.Boolean.pipe(optional)(or asourceenum) toAgentV2.Infoinpackages/schema/src/agent.ts, set it totrueinpackages/core/src/plugin/agent.tsforbuild/plan/general/explore, and inpackages/core/src/config/plugin/agent.tssetnative = falseonly when!exists(config can override a built-in viadraft.updateon an existing id, and that should staynative: true).normalizeAgentList(native: agent.native) and regenerate the v2 SDK types.Until then
general.showCustomAgentsis the only switch, as you found.We hit the same shape in our own tooling (a gate keyed on a field that quietly stopped arriving, so it degraded to a permanent "no" instead of an error); the write-up and the self-test we use for it are in our repo (ours): https://github.com/aron-intframe/agent-guards#1-the-guard-that-prints-nothing
neriousy commentedon Oct 23, 2026
Additional confirmation from #58363: the missing agent picker also affects the Web UI with a global custom
Remoteagent. Enabling “Show agent” exposes the picker as a workaround. This appears to be the same custom-agent visibility failure tracked here.Dante-dan commentedon Oct 23, 2026
I traced this on V2: missing origin metadata prevents the existing custom-agent visibility check from recognizing custom agents.
Reference implementation preserves
nativethrough the V2 schema, generated client and app adapter. Built-ins (including Plan) are marked native, new agents default to custom, and config overrides retain the built-in flag.Validation: 59 core agent/config/Plan tests and 15 app adapter/selection tests passed. The extended assertions fail before the fix.
bun run checkpassed all 34 tasks. Composer Playwright QA was blocked before interaction by this macOS sandbox's Chromium MachPort permission restriction; I have not reproduced the original Windows desktop scenario.Feedback on the metadata contract is welcome; I can adjust the branch.
opencodejr commentedon Oct 28, 2026
Thanks for the detailed report and analysis, @wfmessi. I reproduced this at the API level.
I created a project with
.opencode/agents/pipeline.md(mode: primary), startedopencode serveon 2.0.11 and calledGET /api/agentfor that project. The custompipelineagent is returned along withbuildandplan, but no agent has anativefield. The app'shasCustomAgent()check (native === false) is therefore always false, so the composer agent picker only appears when Show agent is on. The script below does this end to end.The result is the same on the latest
dev(fa1260d). The publicAgent.Infoschema still has nonativefield, and the app'snormalizeAgentListwould drop it anyway. Open PR #59656 adds the flag on the server side. Related: #57812 (agent picker never renders in Desktop, also for built-in agents).Until then, turning on Settings → General → Show agent is the workaround, as you found.
Reproduction script (repro.sh)
Run it:
bash repro.sh