Skip to content

feat: add OpenCode provider via webhook adapter - #327

Open
jorgelop1994 wants to merge 12 commits into
pixel-agents-hq:mainfrom
jorgelop1994:feat/opencode-provider
Open

feat: add OpenCode provider via webhook adapter#327
jorgelop1994 wants to merge 12 commits into
pixel-agents-hq:mainfrom
jorgelop1994:feat/opencode-provider

Conversation

@jorgelop1994

Copy link
Copy Markdown

Description

Adds a new HookProvider implementation for OpenCode (opencode.ai), enabling Pixel Agents to visualize OpenCode sessions alongside Claude Code.

OpenCode is a server-based AI agent runtime (not a CLI like Claude Code). It exposes an SSE endpoint (/global/event) and a REST API. This PR adds:

Changes

  1. server/src/providers/hook/opencode/ — New provider directory:

    • opencode.ts — HookProvider implementation
    • opencode.webhook.tsnormalizeHookEvent + formatToolStatus
    • opencode-constants.ts — Tool classification sets
  2. server/src/compositeProvider.ts — New utility to wrap multiple HookProviders, dispatching normalizeHookEvent to each in registration order. Used by the standalone CLI to support both Claude Code and OpenCode.

  3. server/src/cli.ts — Updated to create a compositeProvider(claudeProvider, opencodeProvider) so the standalone server handles both webhook formats.

  4. Event mapping (bridge → webhook):

    • session.status: busysessionStart
    • session.status: idleturnEnd
    • message.updated (assistant) → toolStart
    • tool.starttoolStart with tool name
    • tool.endtoolEnd
  5. Unit tests — 23 tests covering all event types and tool status formatting

How to test

  1. Start Pixel Agents standalone: npx pixel-agents --port 3100 --host 0.0.0.0
  2. POST a webhook event:
    curl -X POST http://localhost:3100/api/hooks/opencode \
      -H "Content-Type: application/json" \
      -H "Authorization: Bearer $(cat ~/.pixel-agents/server.json | jq -r .token)" \
      -d '{"event_type":"session.status","session_id":"ses_test","payload":{"status":"busy"}}'
  3. Verify the agent character appears in the Pixel Agents web UI

Related issues

Closes #67

Test plan

  • Unit tests for normalizeOpenCodeEvent (23/23 passing)
  • Manual testing: webhook POST returns 200 and agent appears in standalone UI
  • Existing server tests unaffected

E2E coverage

Tested manually via standalone CLI (e2e tests pending; will follow up in a separate PR).

Implements HookProvider for OpenCode (opencode.ai), a server-based AI
agent runtime. Unlike Claude Code (CLI-based), OpenCode runs as a
persistent server with REST API + SSE event stream.

Provider details:
- normalizeHookEvent: translates bridge webhook payloads (session.status,
  message.updated, tool.start/end, session.start/end) to AgentEvent format
- formatToolStatus: covers OpenCode + escal-ai custom tools (github-*,
  plane-*)
- No file fallback (OpenCode uses DB, not JSONL transcripts)
- install/uninstall are no-ops (server is always connected)
- areHooksInstalled returns true

Closes pixel-agents-hq#67
- compositeProvider.ts wraps multiple HookProviders, dispatching
  normalizeHookEvent to each in registration order.
- cli.ts creates compositeProvider(claudeProvider, opencodeProvider)
  so the standalone server handles both webhook formats.
sessionStart necesita cwd para que Pixel Agents cree un pending external
session. watchAllSessions=true permite que sesiones OpenCode (sin
directorio tracked de Claude) sean aceptadas.
- webview-ui/public/assets/characters/: reemplazados char_0 (Aiko),
  char_1 (Nairo), char_2 (Kobai) con sprites recoloreados
- core/src/provider.ts: AgentEvent.sessionStart incluye palette?
- core/src/messages.ts: AgentCreated incluye palette?
- server/src/types.ts: AgentState incluye palette?
- server/src/sessionRouter.ts: PendingExternalSession incluye palette
- hookEventHandler.ts: extrae palette de sessionStart, la pasa a pending
  y a onExternalSessionDetected
- httpServer.ts: incluye palette en agentCreated WebSocket msg
- fileWatcher.ts: hooks-only agent setea palette en AgentState
- agentRuntime.ts: pasa palette a adoptExternalSessionFromHook
- opencode.webhook.ts: normalizeHookEvent extrae agent_name + palette
  del payload y los usa en sessionStart
- useExtensionMessages.ts: webview usa palette from agentCreated si
  esta presente
@gerardo-cipriano-dinova

Copy link
Copy Markdown

Tried this branch end-to-end on Linux with the standalone CLI (node dist/cli.js --port 3100) plus a small bridge that reads opencode's SQLite store and POSTs the documented payload. The provider itself works — once events reach it, agents appear and tools render correctly (agentCreatedagentToolStart "Reading file" → agentToolDone). Two things got in the way, one of which looks like a real bug in this PR.

1. The hook route drops every OpenCode event, with a 200

server/src/httpServer.ts:123:

if (event.session_id && event.hook_event_name) {
  options.onHookEvent?.(providerId, event);
}

The webhook payload this PR documents (and that normalizeOpenCodeHookEvent parses) carries event_type, not hook_event_name:

POST /api/hooks/opencode
{ "event_type": "tool.start", "session_id": "ses_...", "payload": { ... } }

So every OpenCode POST is accepted with 200 ok and then silently discarded before onHookEvent. Nothing in the logs says so, which makes it a slow one to find: the request log shows 200, the provider is never called.

Confirmed by isolating the pieces: normalizeOpenCodeHookEvent returns the right AgentEvent for all four documented event types, and adding hook_event_name to the body (leaving event_type in place, which is what the normalizer reads) makes agents appear immediately.

One-line fix, if it fits your intent:

if (event.session_id && (event.hook_event_name || event.event_type)) {

Or have the provider declare which discriminator field it uses, since this is exactly the kind of thing a second hooks-only provider will hit again.

2. Opening the UI silently turns watchAllSessions off

An OpenCode session started outside Pixel Agents becomes an agent only when
watchAllSessions is on, or when its cwd is a tracked project dir — otherwise
onExternalSessionDetected returns early (server/src/agentRuntime.ts:99), and again
the POSTs still return 200 with nothing visible.

The two defaults disagree, which makes it look flaky rather than configurable:

  • server/src/cli.ts:134getSetting('pixel-agents.watchAllSessions', true)
  • server/src/clientMessageHandler.ts:177getSetting(KEY_WATCH_ALL_SESSIONS, false)

and clientMessageHandler.ts:194 writes that value back into runtime.watchAllSessions.current
on every webviewReady. So on a fresh install the standalone server starts with watch-all
on, and the first time you open the UI it flips off — without the user touching the
toggle. Aligning the two defaults (or not overwriting the runtime ref when nothing is stored)
would make this predictable.

Happy to open a PR for the one-liner if you'd rather have it separately. For what it's worth, the hooks-only adoption path in adoptExternalSessionFromHook works nicely for opencode as it stands — no transcript file needed, the agent picks up the cwd's folder name.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OpenCode support

2 participants