Skip to content

Phase 7: wire opencode-headless as the third provider (plug-and-play acceptance test) #406

Closed

Description

@Juliusolsson05

Tracking issue for #394 phase 7 — wiring packages/opencode-headless as the third provider. This is the acceptance test of the plug-and-play architecture: opencode has no PTY, no screen, no keystrokes, no transcript files.

Contract surface (compiler-enforced once 'opencode' joins AGENT_PROVIDER_KINDS)

registry.main (createSession→AgentSession, listSessions, getProjectDir, resolveTranscriptPath, deliverPrompt) · registry.renderer.capabilities (identity, conditionViews/policy, mapper, extractProviderSessionId, composerSubmit, flags) · registry.renderer (TileLeaf) · registry.setup (binaryName/label/required/detail) · build plumbing (vite aliases + tsconfig paths + headlessExclude).

Clean mappings

  • OpencodeHeadless (spawn mode via SpawnedServer) → OpencodeSession implements AgentSession; ready→started (projectDir undefined — optional by contract), exit→exit (null→-1), ScreenChannel activity→process-state, CommittedChannel entry→jsonl-entry (synthetic opencode://session/<id> file arg; resume replay arrives as a live burst the bulk ingest already handles), history_error/sse-error→jsonl-error, SemanticChannel event→semantic-event (vocabulary already matches the app's fold).
  • deliverPrompt via HTTP prompt() — strictly better than both PTY protocols (no readiness gate, no paste confirm).
  • Conditions: SSE permission/question events → the OPEN snapshot union (opencode.permission, opencode.question) with custom actions only; resolveCondition → PermissionService.reply / replyQuestion — exactly what the phase-2a contract comment predicted.
  • Identity/commands: pure data; spawn/split palette entries derive automatically (phase 4).

Degraded-by-design (no PTY/screen)

pty-data/agent-pty-data never emitted (AgentTerminalLeaf needs a capability gate — else permanently blank xterm) · screen never emitted (all consumers degrade free; paste-baseline is Claude-only) · write/resize no-ops · conditionPolicy.actionKinds MUST be empty (keystroke routing would silently eat keys) · composerSubmit must NOT use io.send — needs a new session:deliver-prompt IPC onto the existing manager.deliverPromptToAgent.

Hard blockers → smallest viable resolutions

  1. No offline session listing (SyncClient needs a running server; SQLite storage, no walkable files; the package's planned transcript/SessionList.ts was never implemented) → v1: listSessions: async () => []; real fix: ephemeral opencode serve lister or app-side JSONL mirror.
  2. No durable transcript file → resolveTranscriptPath: async () => null (all consumers type string|null); initial history comes from the start-time committed replay, NOT the file loader. Real fix later: app-owned JSONL mirror written from committed events (also unlocks history paging + switch sources).
  3. Composer input path — the new IPC is mandatory for a usable pane (small: handler + preload method + 10-line composerSubmit).
  4. message.updated re-emission vs feed dedupe — assistant messages update as parts complete; naive uuid-dedupe freezes rows at first partial. v1: forward only time.completed messages (semantic streaming card covers the live view — same division of labor as Claude/Codex).
  5. First required: false setup provider — verify SetupGate soft-handles a missing optional binary.

Wiring order

  1. Build plumbing + add 'opencode' to AGENT_PROVIDER_KINDS (compiler enumerates the checklist)
  2. Minimal registry stubs (compiles; opencode appears in spawn UI)
  3. OpencodeSession runtime (event translation; semantic streaming renders even pre-mapper)
  4. Transcript mapper + extractProviderSessionId (feed rows + durable identity)
  5. deliverPrompt + composer IPC (interactive; MCP orchestration free)
  6. Conditions (permission/question views + resolveCondition — last must-have; without it the agent silently stalls on first permission)
  7. Follow-ups: ephemeral lister, JSONL mirror, AgentTerminalLeaf gate, dangerousMode/MCP config injection, atp codec for switching

Merge policy: steps 1–6 land as one branch so what ships is a usable pane, not a spawnable-but-mute one.

Package-side gaps to fix in opencode-headless: missing transcript/SessionList.ts/ProjectKey.ts; ScreenChannel.publishSnapshot is dead code.

Part of #394.

Activity

  1. Juliusolsson05 commented on Jul 6, 2026

    @Juliusolsson05
    OwnerAuthor

    Closing — the useful parts of this tracker landed across #408/#411/#412/#413/#415/#417/#419 plus opencode-headless#1/#2 (spawn, commands, full rendering pipeline, session ownership, tool/todo rows — verified against a live server session). The honest remainder is re-scoped elsewhere: resume UX + switching for OpenCode is blocked on agent-transcript-parser#5 (neutral-hub codecs) and will be tracked with that campaign; subagent rendering is #178's scope; ingestion residue folds into the rendering-engine rewrite. This issue's phase-numbered naming outlived its usefulness.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions