Skip to content

app: project-local custom agents never make the composer agent picker visible (native flag missing from public Agent.Info) #50269

Open

Description

@wfmessi

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-cli 2.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

  1. Create a project-local custom agent, e.g. .opencode/agents/pipeline.md with mode: primary (any custom primary agent reproduces).
  2. Open that project in OpenCode Desktop 2.0.11 with default settings (Settings → General → Show agent = off).
  3. Composer shows no agent control; typing /agent does nothing; Ctrl+P has no "Switch agent"; Ctrl+. does nothing.
  4. 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 to false (packages/app/src/context/settings.tsx, general.showCustomAgents: false).
  • list() comes from the API, and the public Agent.Info schema has no native field.

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

Activity

  1. aron-intframe commented on Sep 21, 2026

    @aron-intframe

    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 is AgentV2.Info from packages/schema/src/agent.ts. That schema has no native field, and the v2 core never records one either: built-ins are registered in packages/core/src/plugin/agent.ts (draft.update(AgentV2.ID.make("plan"), ...)) and custom .opencode/agents/*.md agents in packages/core/src/config/plugin/agent.ts, and neither site marks origin (grep -rn native over packages/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.ts returns nothing). The app then converts the v2 payload with normalizeAgentList in packages/app/src/context/global-sync/utils.ts:15-37, which maps id -> name, permissions -> permission, system -> prompt etc. but has no native line, so hasCustomAgent(list()) in local.tsx:72 is always false and the picker stays hidden.

    On the v1 path (legacy.app.agents() in global-sync/bootstrap.ts:269) the internal Agent.Info from packages/opencode/src/agent/agent.ts still carries native: true/false, so hasCustomAgent does 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 + custom pipeline agents):

    $ node repro.mjs
    v2 normalized keys: name,description,mode,hidden,temperature,topP,color,permission,model,variant,prompt,options,steps
    v2 'native' on custom agent: undefined
    v2 hasCustomAgent -> false
    v1 hasCustomAgent -> true
    

    Fix. Two pieces are needed, the client-side map alone cannot recover the information:

    1. Add native: Schema.Boolean.pipe(optional) (or a source enum) to AgentV2.Info in packages/schema/src/agent.ts, set it to true in packages/core/src/plugin/agent.ts for build/plan/general/explore, and in packages/core/src/config/plugin/agent.ts set native = false only when !exists (config can override a built-in via draft.update on an existing id, and that should stay native: true).
    2. Carry it through in normalizeAgentList (native: agent.native) and regenerate the v2 SDK types.

    Until then general.showCustomAgents is 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

  2. neriousy commented on Oct 23, 2026

    @neriousy
    Member

    Additional confirmation from #58363: the missing agent picker also affects the Web UI with a global custom Remote agent. Enabling “Show agent” exposes the picker as a workaround. This appears to be the same custom-agent visibility failure tracked here.

  3. Dante-dan commented on Oct 23, 2026

    @Dante-dan
    Contributor

    I traced this on V2: missing origin metadata prevents the existing custom-agent visibility check from recognizing custom agents.

    Reference implementation preserves native through 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 check passed 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.

  4. opencodejr commented on Oct 28, 2026

    @opencodejr
    Contributor

    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), started opencode serve on 2.0.11 and called GET /api/agent for that project. The custom pipeline agent is returned along with build and plan, but no agent has a native field. The app's hasCustomAgent() 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 public Agent.Info schema still has no native field, and the app's normalizeAgentList would 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)
    #!/usr/bin/env bash
    # Reproduces #50269: agents in the public agent API carry no `native` flag,
    # so the app's hasCustomAgent() (item.native === false) is never true and the
    # composer agent picker stays hidden while "Show agent" is off.
    # Usage: ./repro.sh                       (uses `opencode` from PATH)
    #        OPENCODE="bun --conditions=browser --preload=<repo>/packages/tui/node_modules/@opentui/solid/scripts/preload.js <repo>/packages/cli/src/index.ts" ./repro.sh
    set -euo pipefail
    OPENCODE=${OPENCODE:-opencode}
    mkdir -p /tmp/opencode
    P=$(mktemp -d /tmp/opencode/proj.XXXX)
    mkdir -p "$P/.opencode/agents"
    cat > "$P/.opencode/agents/pipeline.md" <<'MD'
    ---
    description: Custom primary pipeline agent
    mode: primary
    ---
    You are the pipeline agent.
    MD
    cd "$P"
    export HOME=$(mktemp -d /tmp/opencode/home.XXXX)
    $OPENCODE --version
    PORT=$((4600 + RANDOM % 300))
    $OPENCODE serve --hostname 127.0.0.1 --port $PORT > "$HOME/serve.log" 2>&1 &
    SRV=$!; trap 'kill $SRV 2>/dev/null || true' EXIT
    for i in $(seq 1 90); do grep -q "listening" "$HOME/serve.log" 2>/dev/null && break; sleep 1; done
    sleep 1
    PW=$(sed -n 's/.*server password //p' "$HOME/serve.log" | head -1)
    URL="http://127.0.0.1:$PORT/api/agent?location%5Bdirectory%5D=$P"
    OUT=""
    for i in $(seq 1 30); do
      OUT=$(curl -s ${PW:+-u opencode:$PW} "$URL")
      echo "$OUT" | grep -q '"pipeline"' && break; sleep 1
    done
    echo "$OUT" | bun -e '
    const raw = JSON.parse(await Bun.stdin.text())
    const list = Array.isArray(raw) ? raw : raw.data
    const prim = list.filter(a => a.mode !== "subagent" && !a.hidden)
    for (const a of prim) console.log(`${a.id.padEnd(10)} native=${a.native}  keys=${Object.keys(a).join(",")}`)
    console.log("custom agent `pipeline` returned:", list.some(a => a.id === "pipeline"))
    // Same check as the app: packages/app/src/providers/models/agent.ts hasCustomAgent()
    console.log("hasCustomAgent(list) =", prim.some(a => a.native === false))
    '

    Run it: bash repro.sh

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions