Skip to content

OpenCode structured chat over the Agent Client Protocol - #25845

Merged
brennanb2025 merged 464 commits into
mainfrom
brennanb2025/native-chat-opencode-serve
Oct 7, 2026
Merged

brennanb2025 merged 464 commits into
mainfrom
brennanb2025/native-chat-opencode-serve

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 30 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​2034 $\color{#cf222e}{\Huge{\mathbf{−}}}$​22 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​2012
Prod 63 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1755 $\color{#cf222e}{\Huge{\mathbf{−}}}$​257 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1498

Builds on #25225 (the structured chat lane for agents that speak the Agent Client Protocol, with Grok as its first agent), which is now on main; this PR's diff is only OpenCode's change. DRAFT.

ELI5

Today, when you open OpenCode in Orca's chat view, Orca runs OpenCode in a hidden terminal, rebuilds the conversation by reading OpenCode's database, and answers OpenCode's permission questions by typing keys into that terminal. With Settings > Chat UI > "Use updated structured native chat" on, OpenCode now talks to Orca directly over the Agent Client Protocol (ACP), the open protocol Orca's Grok chat already uses: Orca sees every message, tool run and permission request as it happens and answers them itself. With the setting off, nothing changes.

What Changed

The problem. OpenCode's chat in Orca is a terminal session read back from OpenCode's database. Tool output, approvals and Stop all go through a screen Orca has to interpret, so they lag, can be missed, and can't follow the account a chat was started under. An earlier version of this PR fixed that by running a private OpenCode server per chat; that approach is replaced here by the protocol other desktop agent clients use for OpenCode, which needs no server ownership in Orca.

What you see, with the setting on (OpenCode 1.x from 1.18.31, the opencode command)

  • Starting OpenCode opens a structured chat: streamed replies and reasoning, tool rows with their output and the shell command's exit code, and permission cards you answer in place.
  • The permission card's "Always allow" option reads "Allow until OpenCode restarts", because that is how long OpenCode keeps it here: in the chat's own OpenCode process, which ends when you press Stop, when Orca quits, or when Orca stops an idle chat's agent.
  • Images you attach are sent to OpenCode (up to about 11 MB per message, the most one protocol message can carry).
  • Stop cancels the turn, waits up to 4 seconds, then ends the OpenCode process, as for every structured agent. The next message reopens the conversation.
  • Each chat stays on the OpenCode account it was created with (a managed OpenCode profile, or your own data directories), even if you switch accounts later. No credentials are stored.
  • If Orca quits while a message was being sent, reopening the chat checks OpenCode's own history and marks the message as sent when OpenCode recorded it; otherwise it stays "delivery unconfirmed" so you choose to resend or discard. This check never blocks the chat (5 seconds at most, silent on failure).

Unchanged: with the setting off, and always for OpenCode 2, OpenCode keeps today's terminal-backed chat. That includes an opencode command that is itself 2.x (Homebrew's opencode is 2.x now), and any opencode older than 1.18.31 or whose version can't be read. Orca asks the installed binary its version before it creates the chat, so a launch on this computer opens the terminal chat instead. That fallback is #25947, so merge #25947 before this PR: without it, the same launch shows a failed chat start instead of the terminal.

Your Command setting now reaches the structured chat. Before: Settings lets you point an agent at a specific program (its Command), and detection and the terminal chat ran that program, but the structured OpenCode and Grok chats ran whatever opencode/grok came first on PATH, which with Homebrew first can be a different install and a different version. Now the structured chat, and the version check before it, run the program your Command names, exactly as Claude and Codex chats already do; with no Command set they search PATH and the agent's install folder as before. A Command that isn't a runnable program stops the chat with "the opencode Command setting is not a runnable program" rather than quietly running another binary. Pi (#25851) picks this up after this PR lands, together with moving onto the shared version check.

The first new tab of any registered agent opens its chat, including Grok on main today. Before: Orca decides whether a new agent tab is a chat in the window, from two answers it gets during startup: whether this computer's Orca runtime supports chats, and the list of agents it can run as chats (anything beyond Claude and Codex: OpenCode here, Grok already on main, Pi in #25851). The list was read once and never again, and a tab opened before either answer arrived went straight to the terminal chat, so after a slow or failed read every OpenCode or Grok tab opened as a terminal until restart, with nothing saying why (live QA hit this with a supported OpenCode 1.18.31 and with Pi). Now a new tab that is missing either answer asks for it and waits up to 3 seconds before opening anything, the list is read again as soon as the runtime has answered, and the list no longer depends on opening the chat journal first. If a tab still falls back to the terminal chat, one console line says why.

Mechanism

  • OpenCode is one row in the ACP agent list (src/main/acp/acp-launch-specs.ts): opencode acp, on stable 1.x releases from 1.18.31 (the release the recordings capture). A row can name the releases it runs on: agentSession.createSupport runs opencode --version (5 seconds, 4 KB at most) on the binary a launch would spawn, resolved from the same environment a launch starts from, and every launch asks again before spawning, so a binary swapped after the chat was created never starts. The probe is shared (src/main/agent-cli-version-probe.ts), a version it can't read counts as unsupported, and every check logs one line to Orca's main log ([agent-cli-version] <program> --version: <why>: <what it printed>, or ... is supported) so a decision on a real machine can be explained. The launch sets OPENCODE_CLIENT=acp and OPENCODE_ENABLE_QUESTION_TOOL=false last, after your own variables (OpenCode's question tool has no protocol answer and would wait forever). With full access, Orca answers each permission request "allow once", never "always".
  • The program a structured chat spawns comes from one resolver for every agent (native-chat/structured-agent-command-resolution.ts, previously Claude and Codex only): your Command setting when set, refusing with agentCommandNotRunnable when it names nothing runnable, else the stock name on PATH and then the agent's install folders. The ACP launch and the create-time version check both call it, so they ask about and spawn the same file. At create an unrunnable Command is let through so the launch can say why, instead of silently opening a terminal.
  • Which surface a new tab gets is decided in the window: a non-Claude/Codex agent goes to its chat only once the window has learned that the host registers it (agentSession.agents). That answer now comes from the host's registration list, as agentSession.createSupport already does, instead of from installing the chat host first; the window reads it again when its own runtime's capabilities land (host-structured-agents-sync.ts); and a new tab whose route waits only on that list, or on this computer's runtime capabilities during startup, asks for the missing answer and opens nothing until it arrives or 3 seconds pass, the same bound as a local chat's admission (launch-agent-in-new-tab-host-agents.ts). A host that does not publish its agents is never waited on.
  • Every place that sends a launch to a terminal says why in one line, naming the check and never a path's contents or an environment value: the window's route ([agent-launch-route], window console), create-support's refusing check ([structured-create-support]), and a host launch the default asked to be a chat ([agent-launch]). Create-support's composition moved out of the unchecked runtime class into structured-agent-launch-support.ts.
  • A small OpenCode dialect (acp-dialects/opencode-dialect.ts) reads a command's output and exit code from where OpenCode puts them, and relabels the "always" option.
  • OpenCode repeats every approved edit as a file-write request to Orca, which never offered file access; that request is refused without adding a row to the chat.
  • The OpenCode process never inherits Orca's terminal status plugin: the inherited OPENCODE_CONFIG_DIR overlay is replaced by your own config directory (or removed, including Orca's retired shared plugin directory), and every Orca OpenCode and data-account variable is stripped, so the chat is the only thing reporting its status.
  • A chat's account is stored on its record as a tagged account locator (src/shared/agent-session-account-home.ts): a managed OpenCode profile id, or "unmanaged". Agent definitions now say whether their records pin one config-directory variable (Claude, Codex, Grok) or a locator (OpenCode), and each ACP agent row carries an account binding that resolves the locator when a chat is created and turns it into the launch environment. An unmanaged chat records no folders and no credentials: every launch passes your current OpenCode variables through unchanged (inline credentials in OPENCODE_AUTH_CONTENT, relative folders and OPENCODE_DB included), and OpenCode decides what they mean. No account setup refuses the structured chat.
  • Image prompts are a generic ACP path (acp-prompt-content.ts): sent only when the agent row offers images and the agent's start advertises them, read and size-checked before anything reaches the agent, and bounded by what one protocol message can carry.
  • Restart recovery reads OpenCode's own database through Orca's existing bounded OpenCode reader. OpenCode keeps no Orca message id, so a stored message can only match by content: every message Orca already saw delivered first claims its own stored copy, and only what remains can confirm a message in doubt. A missing message is never called undelivered.

Why

The common pattern among desktop agent clients runs OpenCode over ACP: the agent is a process per chat that Stop can end, and nothing in Orca has to own a server's lifetime. The previous server-based version of this PR needed port and password handling, an ownership ledger, two protocol dialects and orphan cleanup; all of that is deleted. The one thing the server route did that ACP cannot (approvals asked by OpenCode's subagents, below) is a known OpenCode bug every ACP client shares, and Stop is the way out.

Differences from the common pattern

  • Intended: a new tab may wait up to 3 seconds for the computer's runtime capabilities and its list of chat-capable agents before choosing the chat or the terminal. The common pattern's client and agent host are one process and need no such list; here the window decides, so it waits once, bounded, rather than guessing the terminal. Once both are known there is no wait.
  • Intended: "Always allow" is relabelled "Allow until OpenCode restarts" rather than shown under OpenCode's own wording, because that is its real reach in Orca (recorded: a new OpenCode process asks again).
  • Partly the common pattern: a provider's binary is asked its --version and held to a minimum supported release, as the common pattern does per provider. Intended differences: Orca also caps the major line (2.x runs acp in its background service, which a chat's launch environment can't reach), and an unsupported release falls back to the terminal chat instead of showing install guidance, because that chat already works with any release.
  • Intended: an image is refused, not replaced by a text note, when the agent did not advertise images. OpenCode always advertises them, so this path is a guard.
  • Common pattern: a chat without an Orca-managed OpenCode profile records no account and launches with your current OpenCode environment, passed through unchanged; no account setup refuses the chat.
  • Intended (Orca-only): a chat on an Orca-managed OpenCode profile records that profile's id, so reopening it uses the profile it was created under; and the child's environment is cleaned of Orca's terminal status plugin, which a structured chat must not load.
  • Intended (Orca-only): the presence-only restart check of OpenCode's database. The common pattern has no restart recovery for messages in doubt; this one can only ever confirm a send, never fail one, and never delays the chat.
  • Temporary: OpenCode 2 (opencode2, or an opencode that is 2.x) is not on this lane. Its ACP command runs inside your per-user OpenCode background service, which a chat's launch environment and account do not reach, and a desktop launch that the host refuses has no terminal fallback yet. Follow-ups: the fallback (a declined local structured launch opens the terminal chat, as paired launches already do) is its own PR (fix(native-chat): a local chat this computer can't run opens the agent in a terminal #25947); OpenCode 2 returns after it, with how its chats name the service's account decided there.
  • Temporary: usage shows tokens but not cost; cost recording comes with provider usage accounting for every agent.

Known OpenCode bugs this inherits (every ACP client shares them)

  • OpenCode never sends approvals asked by its subagents (Task tool) over ACP (ACP drops permission requests from Task subagent sessions anomalyco/opencode#48232), so such a turn waits until you press Stop (recorded: Stop ends it in under a second).
  • OpenCode treats a message starting with an unknown /word (for example "/tmp/x is broken") as a command and ends the turn with no reply. The common pattern does not work around it either.

Linked Issue

N/A (maintainer).

Visual Proof

Live QA in hidden Orca builds of this branch on macOS (two machines), with a private OpenCode 1.18.31 that Orca was pointed at through the OpenCode Command setting. Every launch ran with its own home and profile, and the real ~/.codex, ~/.claude, ~/.orca, ~/.grok and OpenCode login files were hashed before and after every check: no change came from the test app.

Check Result Screenshot
Setting off: OpenCode opens the existing terminal chat Pass O1
opencode that is OpenCode 2.x (2.0.21): the version check keeps it on the terminal chat, no structured chat starts Pass O9
Setting on, OpenCode 1.18.31: structured "OpenCode Chat"; Orca logs --version: 1.18.31 is supported; the shell command's row opens to show its output (hi) and exit code; the reply reads "Exit code: 3" Pass O2
Permission card offers "Allow once", "Allow until OpenCode restarts" and "Reject"; Allow once runs the command; Reject settles the card as Reject / Resolved Pass O3
Stop while sleep 30 runs: "Interrupted after 31s", the command's file never written, the OpenCode process ends, the next prompt runs on a new process and remembers the conversation Pass O4
Image: a pasted image reaches OpenCode Partly verified O5
Credentials given only inline (OPENCODE_AUTH_CONTENT in Orca's environment, OpenCode's data folder empty): New tab > OpenCode opens the chat view, not a terminal, and it replies INLINE-OK; the variable reaches the spawned OpenCode 1.18.31 and no refusal is logged Pass inline
Relative XDG_STATE_HOME (rel-state) in OpenCode's environment setting: the chat view opens and replies REL-OK; OpenCode puts its state under the project folder, as it decides Pass relative
Deliberately invalid inline credentials ({}): the chat view opens (no terminal, no failed-chat card) and the chat answered hi with no error shown. Why it could still answer with no stored login was not investigated Pass invalid

On the image check: the image was attached and sent, but the model used for testing (OpenCode Go's DeepSeek V4.1 Flash) answered the wrong colour for a tiny 1x1 test image, so this does not prove the model saw it; the size bound and capability gate are unit-tested.

Found and fixed during QA: right after Orca started, a new OpenCode tab could open a terminal because the window had not yet learned which agents the host runs as chats (it read that list once at startup). The first launch now waits up to 3 seconds for that answer, which affects every registered agent, including Grok today.

Not verified live for OpenCode: restart recovery and quit mid-reply (shared with Grok's chat lane, verified there), Windows, Linux, SSH and mobile. The first-launch fix (3 s wait) is covered by tests; the live runs above opened the chat after startup had finished. Checks ran at 4a1fce5; later commits add the startup wait, a cross-version manifest update and type fixes.

Also found during QA: inline credentials in OPENCODE_AUTH_CONTENT were refused only when the chat was created, so a new tab showed "Chat could not be started" with Retry. That refusal is now gone entirely, as in the common pattern: with inline credentials the structured chat opens and OpenCode receives them unchanged. Re-checked live after that change on head 4aa7e63b16a (the three rows above); the QA machine's real login and agent files were unchanged.

Not part of this PR: the resolved card's "Answered on desktop-renderer" text names an internal client id for every agent on main today; that is a separate fix.

Testing

  • Recorded traffic: five scrubbed recordings of real opencode acp 1.18.31 sessions replay through the adapter (acp-structured-opencode-recordings.test.ts): an approved shell command with its exit code and the "Allow until OpenCode restarts" label, a denial, the subagent approval that never comes and Stop, the repeated file write leaving no row, and a provider error shown in OpenCode's own words.

  • Version gate: 2.0.21 refused at create and at launch, 1.18.31 allowed, an unreadable version refused, and the create check asking about the same binary the launch spawns (structured-agent-launch-support.test.ts, acp-structured-launch-resolution-opencode.test.ts, agent-cli-version-probe.test.ts); removing either check, or letting any version through, fails them.

  • Real version check end to end (structured-agent-launch-support-probe.test.ts): --version actually runs against stand-in opencode scripts (2.0.21 first on the login-shell PATH, 1.18.31 elsewhere). The per-agent PATH and the Command setting each make both the create check and the launch use 1.18.31; with neither, 2.x keeps the terminal chat; an unrunnable Command is admitted and the launch refuses with agentCommandNotRunnable. Ignoring the Command setting fails two of them. The probe's refusal log line is asserted in agent-cli-version-probe.test.ts.

  • Unit tests: launch environment and account, including inline credentials, relative folders and OPENCODE_DB reaching the spawned OpenCode unchanged (acp-structured-launch-resolution-opencode.test.ts, opencode-structured-account-home.test.ts), dialect, image prompts and their size bound, restart recovery including identical-text cases, registrations.

  • Final head e00b9f6a886 (latest main merged, including fix(native-chat): a local chat this computer can't run opens the agent in a terminal #25947, feat(native-chat): say in the chat why Orca stopped a reply, and offer Continue #25675 and [CI] Keep the structured-session compatibility manifest within the file limit #26342): both typechecks pass locally, as do the account, launch and cross-version wire tests. CI on the previous head a8c3aeec15f passed every check (24 passed, 15 skipped); CI is running on this head.

  • From live QA (a supported 1.18.31 named by the Command setting and the per-agent PATH still opened the terminal chat, with no version-check line in the main log): the host's whole create check under that run's environment (HOME and every XDG directory in the test rig, the Command and per-agent PATH naming a stand-in 1.18.31) admits it, including with inline credentials or relative folders set (structured-agent-create-support-opencode-rig.test.ts), so the host did not decide; the window did, on an agent list it never learned. Tests: the first launch waits and opens the chat once the host lists OpenCode, decides without it once 3 seconds pass, never waits once the list is learned or with the setting off, and during startup, after a failed first capability probe, opens the Claude or OpenCode chat when a later probe lands within the wait, or the terminal chat (saying why) when none does (launch-agent-in-new-tab-host-agents.test.ts); the list is read again when the runtime's capabilities land (host-structured-agents.test.ts); agentSession.agents answers without installing the host (structured-agent-session-agents.test.ts); the window says why a launch opened the terminal chat (agent-launch-route-registered-agents.test.ts). Each was checked to fail with its fix removed. Which of the two silent startup paths the test machine hit (the read skipped while capabilities were unknown, or the host install failing) is not verified; both are removed.

  • Rendered chat verified live (see Visual Proof). Not verified: Windows/Linux/WSL/SSH, the mobile app, and a chat on a real Orca-managed OpenCode profile.

  • I manually tested these changes locally (hidden Orca builds driven over CDP; see Visual Proof)

  • Automated tests added/updated, or explained why not below

Review

Reviewed with fresh adversarial reviewers and an independent architecture challenge; findings and fixes are summarized in the review comment.

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

SSH and WSL workspaces keep the terminal-backed chat (the ACP lane runs agents only on the machine Orca runs on). An older Orca that reads a chat record with the new OpenCode account locator sets that one record aside, unchanged, rather than failing.

…P no longer borrows Codex's location rule

The rule a self-supervised agent child runs under (this machine, no WSL, Windows only with process
start-time proof) is its own module that Codex and the ACP adapter both use. Grok's registration
takes the full account-home resolver signature, and D3's tests build hosts with the agent registry.
… the managed process's close

A request the agent or a Stop cancels is answered with the agent's own cancelled reply by the code that
owns it (the runtime no longer answers a silent handler), so a Stop needs no separate decline pass. A
permission answer still being saved when the agent stopped waiting is reported unconfirmed, since the
protocol already answered it cancelled. Cancelling the agent's own turn is the plain cancel. Request
rows are matched under their generation-scoped ids. A refusal's reason comes from the dialect's wording
path. The child drops its own stderr tail and close policy for the managed process's, and a close
whose process tree was not proven gone is reported as the adapter contract asks.
…ing what Grok replays

A chat with a saved Grok session reattaches with session/resume where the agent offers it, else
session/load. Either way the call runs inside the translator's load window, so what Grok sends while
it reattaches (its saved exchange, a task the dead process left running, ended by the restart) opens
no turn and writes no row; only context usage reads on. A reply an Orca or Grok crash cut short is no
longer completed from Grok's saved history: it reads like a Claude or Codex chat's, with the existing
notice. The attach window also closes after a failed attach, and a created session that session/resume
reports missing is replaced like one session/load reports missing.

The replay reconciliation is removed: the lane no longer reads the journal, and D3's replayed-input
grammar test and completed-turn check in the assembler go with it.
…mpty chat is reusable

Main's idle-empty-chat lookup and launch join now take any registered agent, as the rest of the
launch path does. The refusal check moved into the prompt turns and the prompt-block conversion beside
the turns that send it, keeping both files in their line limit.
…turn; the next send resumes

Grok's session/cancel ends only the running turn: work it already moved to the background keeps
running and can begin a turn of its own after the person pressed Stop. Stop is now a session
boundary, as it is for Claude: the cancel answers open requests and lets Grok end the turn, the host
waits a bounded grace for that, then ends the process; the next send relaunches and resumes.
The adapter's own bounded close of a turn Grok began is gone. Its named-turn check stays: the host
ends the session unless the provider declines a Stop naming a turn that has since ended.
…en sends it

A send that reached Grok while a prompt ran was held in the adapter until that turn ended: Steer
on a queued card took the card out of the host's editable queue and meant 'send after this turn'.
It now cancels the running prompt (session/cancel; the session stays) and sends as the next prompt
once Grok answers the cancel, as the common pattern does; a steer behind another cancels it in
turn, so the last one runs. The adapter holds a send only while that cancel lands, so its general
held-send queue and its holdsDispatch report are gone (every send it holds has its turn open in
the journal). An older client's mid-turn send takes the same path. capabilities.steering is
unchanged and still unread.
…quire's own abort signal

The host owns the acquire it runs, so it now owns its cancellation: each attach's acquire gets an
AbortSignal, aborted from outside the session's queue by a close and by a Stop admitted now (the
same admission rule as before). The optional abandonStart adapter hook, the router's fan-out to
every adapter and the ACP adapter's session-keyed start map are gone; the ACP adapter keeps an
unkeyed set of starts only so quit can prove their children gone, and keeps a failed start's
unproven child until its exit is proven.
The hook also let a later close ask that child again. The host now does that from state it holds:
a close of a chat with no live child whose record still names an owner process with no death
evidence asks the adapter to release it. The answer is not recorded as proof (the lease probe
does that), so an owner pid an earlier Orca left is never killed or marked gone. Claude and Codex
ignore the signal and hold no such child; their release is a no-op (tested).
…ll ends with Grok's last words

On macOS and Linux the agent's stdout ends before its exit is observed, with or without the
supervisor's EOF forwarding, so the connection's loss closed the journal first and its error text
became the session's ended reason, dropping Grok's stderr. The reason is now read at the proven
exit: the agent's last words when it left any, else why the connection closed. The failure already
carried them. Comments that assumed the exit comes first, that early frames past the cap refuse the
start, and that dispatch re-checks image support are corrected.
…written, marked as replay or not

The reattach window relied on the dialect's replay verdict, and Grok's frames read as live unless
they carry isReplay, so an unmarked chat frame during session/resume opened a turn that never
ended. D3 now marks every frame inside the window as replay before the translator reads it, so the
translator keeps only context usage whatever the agent marked; options and commands are still
adopted. The translator's load semantics are unchanged.
…re draining attaches

A Grok that never answered its handshake held quit until the start's own 60 s bound, past the
20 s quit deadline. The host's teardown now aborts each in-flight acquire (and any the drain
still begins), so the adapter's own quit controller and its map of starts are gone: a start
has one canceller, the host's signal.
…the start has returned

The listener stayed on the host's signal until the attach finished committing, so a Close in that
window killed the now-live child behind the host's back and it read as Grok crashing. The start
now detaches it when it ends; a later Close goes through the session's own stop.
…tart before it launches

The attach began its abort controller only after reconciling leases, resolving recovery and
probing the previous owner, so a close or admitted Stop in those phases reached nothing and Grok
launched anyway. The controller now begins first, and the acquisition checks it before asking the
adapter to start.
…apter to start

Also renames the close test after the hook it no longer exercises.
…failed cleanup left

The re-ask is not a no-op for them: when the adapter still holds the child its cleanup could not
prove gone, the close stops it again as a requested close, and Claude persists the handle of the
conversation it ran so the next send resumes it. Corrects the tests' and comment's wording; the
close awaits the re-ask, bounded by each adapter's kill ladder.
…d sends once it ends

A send while Grok ran a turn of its own (a background task waking it) went straight to Grok, which
queued it behind that turn where Orca could no longer withdraw it, while Stop treated the same turn
as the running reply. The send now waits as a steer, the turn is cancelled once, and the message
goes when the turn ends; a Stop withdraws it and an exit rejects it as never sent.
…and is rejected as never sent

Pins the bounded steer cancel kept from the runtime: past the bound the connection closes, the
running reply reads unverifiable, Grok's end reads as its exit, and the waiting steer is rejected
as never sent.
… its exit is proven

After the connection broke and the close could not prove Grok's exit, any later stop Orca asked
for (the next start, a Stop, a Close) marked the child as closed by Orca, so the crash read as a
requested close and Grok's last words were dropped; a send meanwhile was recorded unconfirmed.
The connection loss now decides the cause, and a send on that session is rejected as never sent.
…ability made stale

CI's unit shards failed on tests outside the PR's own lists. Each encodes something this PR changes
on purpose: Grok is now a registered agent (the seam test's unregistered agent is now Cursor); the
desktop now advertises registered agents (the restart-offer tests' older client drops that
capability explicitly); the attach context carries the start's abort controllers (the forget-status
double gains them); and the ACP real-host test rig sends to the host directly (listed beside the
other real-host rig in the send ratchet).
…t failure

With quit now aborting a start it would have waited for, the delivery step recorded the aborted
start as the message's failure ("couldn't restart"). After quit has stopped delivery, the step
leaves the message to quit, which settles it as a close does ("The chat closed before this message
was sent."). The test that pinned quit waiting for that start and stopping its child now pins that
nothing is launched behind quit.
…e cross-version manifest

Older builds read the list from the installed host, newer ones from the registrations it is built from; the manifest now asks only that Codex is listed, which holds for both.
…a slow startup

Field QA: soon after a slow startup, a new tab opened the terminal chat because the local runtime had not reported its capabilities yet, which routes straight to the terminal; the bounded wait covered only an unlearned agent list. A new tab whose chat route waits only on either answer now asks for it and opens nothing until it comes or 3 seconds pass, then decides; the [agent-launch-route] line still names the blocker if it falls back. Tests that never meant to exercise startup now start with the runtime's answer known.
…failed at startup

When the startup probe fails (a rig whose terminal daemon failed to start), asking again answers null at once, so the new-tab wait ended early and the tab opened the terminal chat. The wait now subscribes to the event that delivers the capabilities, probes once, and stops at the 3 s cap; it applies to both routes, so OpenCode (whose blocker is the unlearned agent list) and Claude (unknown capabilities) both wait for it. Tests run the real capability module with a failed startup probe: Claude and OpenCode open their chat when a later probe lands within the wait, and the terminal chat after the cap otherwise.
…a failed chat

Live QA with inline OpenCode credentials (OPENCODE_AUTH_CONTENT) passed the version check at create support and was refused only at create, so the window showed a failed "OpenCode Chat" with Retry where a declined launch opens the terminal chat. Create support now resolves the account a new chat would pin through the same read-only resolver the model catalog and create use, and answers no, logging the account-pin check, when the host refuses to pin it; any other failure is left for create to state. Create's own refusal stays as the backstop.
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Ready for review.

  • Head: 804b050217b, CI green (15 pass, 17 skipped by change detection), mergeable cleanly with main.
  • Live QA: done on macOS, see Visual Proof. With OpenCode 1.18.31 the structured chat opens, shows command output and exit codes, offers the permission card, and Stop works. OpenCode 2.x keeps the terminal chat, and so does the setting being off.
  • Fixed during QA:
    • The first new tab after startup could open a terminal; it now waits up to 3 s for the host's answer. This affects every registered agent, including Grok today.
    • An account Orca cannot pin is now refused before any tab opens.
    • Orca now honours the OpenCode Command setting.
  • Not verified live: restart and quit mid-reply for OpenCode (that code is shared with Grok's chat lane, verified there), whether the test model read an image, Windows, Linux, SSH and mobile.
  • Merge order: fix(native-chat): a local chat this computer can't run opens the agent in a terminal #25947 first, then this PR. Add Pi structured native chat through its RPC mode #25851 (Pi) conflicts with this PR and will merge main after it lands.

@brennanb2025
brennanb2025 marked this pull request as ready for review October 7, 2026 12:30
@coderabbitai

coderabbitai Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 57e26584-f4b1-4acc-8ca7-418c3d47169b
📥 Commits

Reviewing files that changed from the base of the PR and between 7c518bc and 4aa7e63.

📒 Files selected for processing (2)
  • src/renderer/src/lib/launch-agent-in-new-tab.ts
  • src/renderer/src/lib/launch-agent-structured-chat-guard.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 2 remain after this review.


📝 Walkthrough

Walkthrough

The pull request adds OpenCode as a structured ACP agent, including account binding, version checks, image prompts, tool-timeline translation, and recovery-history support. It extends account-home types and updates existing structured-agent consumers. Host and renderer launch flows now check structured-agent support, report downgrades, and can wait for host or runtime capability information before routing.

Priority: ➖ Normal

Merge Risk: 🔵 Low · up to 4aa7e

On some Windows setups using Git Bash or MSYS, OpenCode’s structured launch may not find an installation under the Windows profile. This is a bounded platform-specific risk; affected users may need to correct their environment or command path.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.47% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 99 functions across 67 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely identifies the main change: adding OpenCode structured chat over ACP.
Description check ✅ Passed The description is detailed and covers the required user impact, implementation, rationale, linked-issue exception, visual proof, testing, review notes, platform limitations, and compatibility conside…
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/main/native-chat/agent-model-catalog/agent-model-catalog-fingerprint.ts (1)

27-35: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Fingerprints for OpenCode homes depend on the order of locator keys.

JSON.stringify(account.locator) serializes keys in insertion order. It also includes extra fields from newer builds, which the validator accepts. Two equal locators can therefore produce different fingerprints, and the catalog misses its cache. The failure is safe because the result is only a cache miss. Build the parts explicitly from the fields that agentSessionAccountHomesEqual compares.


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 12e268e2-f9a2-43bf-ae9d-5c27e2647c52
📥 Commits

Reviewing files that changed from the base of the PR and between 2dc98c9 and 804b050.

📒 Files selected for processing (94)
  • src/main/acp/acp-account-binding.ts
  • src/main/acp/acp-dialects/acp-dialect.ts
  • src/main/acp/acp-dialects/opencode-dialect.ts
  • src/main/acp/acp-launch-specs.ts
  • src/main/acp/acp-prompt-content.test.ts
  • src/main/acp/acp-prompt-content.ts
  • src/main/acp/acp-recovery-history.test.ts
  • src/main/acp/acp-recovery-history.ts
  • src/main/acp/acp-structured-acquire.ts
  • src/main/acp/acp-structured-adapter.test-support.ts
  • src/main/acp/acp-structured-agent-definitions.ts
  • src/main/acp/acp-structured-electron-free.test.ts
  • src/main/acp/acp-structured-fixture-replay.test-support.ts
  • src/main/acp/acp-structured-host.test-support.ts
  • src/main/acp/acp-structured-image-prompts.test.ts
  • src/main/acp/acp-structured-launch-resolution-opencode.test.ts
  • src/main/acp/acp-structured-launch-resolution.ts
  • src/main/acp/acp-structured-opencode-recordings.test.ts
  • src/main/acp/acp-structured-session-adapter-deps.ts
  • src/main/acp/acp-structured-session-adapter-lifecycle.test.ts
  • src/main/acp/acp-structured-session-adapter.ts
  • src/main/acp/acp-structured-turns.ts
  • src/main/acp/acp-timeline-fixture.test-support.ts
  • src/main/acp/acp-timeline-opencode-dialect.test.ts
  • src/main/acp/acp-timeline-requests.ts
  • src/main/acp/acp-tool-timeline.ts
  • src/main/acp/fixtures/opencode-v1-deny.jsonl
  • src/main/acp/fixtures/opencode-v1-provider-error.jsonl
  • src/main/acp/fixtures/opencode-v1-stray-write.jsonl
  • src/main/acp/fixtures/opencode-v1-subagent-hang.jsonl
  • src/main/acp/fixtures/opencode-v1-tool.jsonl
  • src/main/agent-cli-version-probe.test.ts
  • src/main/agent-cli-version-probe.ts
  • src/main/agent-launch/agent-launch-executor.ts
  • src/main/agent-launch/agent-launch-mode.ts
  • src/main/claude/claude-at-rest-commands.ts
  • src/main/claude/claude-structured-agent-definition.ts
  • src/main/claude/claude-structured-launch-resolution.ts
  • src/main/claude/claude-structured-session-adapter.ts
  • src/main/codex/codex-structured-agent-definition.ts
  • src/main/codex/codex-structured-launch-resolution.ts
  • src/main/managed-data-accounts/service.test.ts
  • src/main/managed-data-accounts/service.ts
  • src/main/native-chat/agent-model-catalog/agent-model-catalog-fingerprint.ts
  • src/main/native-chat/agent-model-catalog/agent-model-catalog-service.ts
  • src/main/native-chat/agent-session-journal/journal-restart-reconciliation.ts
  • src/main/native-chat/agent-session-journal/journal-submission-reconciler.ts
  • src/main/native-chat/agent-session-wire/structured-agent-definition.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-launch-env.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-provider-support.ts
  • src/main/native-chat/structured-agent-command-resolution.test.ts
  • src/main/native-chat/structured-agent-command-resolution.ts
  • src/main/native-chat/structured-agent-session-create-support.ts
  • src/main/opencode-usage/opencode-database-discovery.ts
  • src/main/opencode-usage/scanner-wsl-gate.test.ts
  • src/main/opencode/opencode-acp-environment.ts
  • src/main/opencode/opencode-acp-stored-messages.test.ts
  • src/main/opencode/opencode-acp-stored-messages.ts
  • src/main/opencode/opencode-data-directory.test.ts
  • src/main/opencode/opencode-data-directory.ts
  • src/main/opencode/opencode-structured-account-home.test.ts
  • src/main/opencode/opencode-structured-account-home.ts
  • src/main/runtime/agent-session-reservation-admission.ts
  • src/main/runtime/orca-runtime-get-structured-agent-session-create-support.ts
  • src/main/runtime/orca-runtime-get-worktree-ps.ts
  • src/main/runtime/orca-runtime-structured-agent-registration-seam.test.ts
  • src/main/runtime/rpc/methods/structured-agent-session-agents.test.ts
  • src/main/runtime/rpc/methods/structured-agent-session-agents.ts
  • src/main/runtime/structured-agent-create-support-opencode-rig.test.ts
  • src/main/runtime/structured-agent-launch-support-probe.test.ts
  • src/main/runtime/structured-agent-launch-support.test.ts
  • src/main/runtime/structured-agent-launch-support.ts
  • src/main/runtime/structured-agent-runtime-registrations-acp.test.ts
  • src/main/runtime/structured-agent-runtime-registrations.ts
  • src/main/runtime/structured-agent-session-runtime.ts
  • src/renderer/src/lib/agent-launch-route-registered-agents.test.ts
  • src/renderer/src/lib/agent-launch-routing.ts
  • src/renderer/src/lib/agent-session-launch-plan.test.ts
  • src/renderer/src/lib/agent-session-launch-plan.ts
  • src/renderer/src/lib/launch-agent-in-new-tab-host-agents.test.ts
  • src/renderer/src/lib/launch-agent-in-new-tab-host-agents.ts
  • src/renderer/src/lib/launch-agent-in-new-tab-placement.test.ts
  • src/renderer/src/lib/launch-agent-in-new-tab.test.ts
  • src/renderer/src/lib/launch-agent-in-new-tab.ts
  • src/renderer/src/lib/launch-agent-structured-chat-guard.test.ts
  • src/renderer/src/runtime/host-structured-agents-sync.ts
  • src/renderer/src/runtime/host-structured-agents.test.ts
  • src/renderer/src/runtime/host-structured-agents.ts
  • src/renderer/src/runtime/local-runtime-capabilities.ts
  • src/shared/agent-session-account-home.test.ts
  • src/shared/agent-session-account-home.ts
  • src/shared/agent-session-record.ts
  • src/shared/agent-session-stored-agent.ts
  • tests/e2e/cross-version-wire/structured-agent-session-surface-manifest.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.

homePath?: string
} = {}
): string {
const homePath = env.HOME ?? env.USERPROFILE ?? options.homePath ?? homedir()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

homePath uses the POSIX home on Windows even when USERPROFILE is set.

env.HOME ?? env.USERPROFILE gives HOME priority on every platform. On Windows, Git Bash and MSYS often set HOME to a different directory. The install-directory search (~/.opencode/bin) can then miss the binary. The account resolver, childHomeDirectory, prefers USERPROFILE on win32. Select the home variable by platform so that both code paths use the same home.

An OpenCode chat with no managed profile no longer records data/state
folders or a database choice, no longer refuses inline credentials or
relative paths, and launches with the user's environment unchanged.
Drops the create-support account-pin probe that only those refusals used.
…hat-opencode-serve

# Conflicts:
#	src/main/claude/claude-structured-launch-resolution.ts
…hat-opencode-serve

# Conflicts:
#	src/renderer/src/lib/launch-agent-in-new-tab.ts
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Status update: OpenCode accounts are no longer refused.

The problem: when OpenCode had no Orca-managed account, this PR copied the data folder, state folder and database choice onto each chat. As a side effect it refused the chat view for two setups: credentials given inline in OPENCODE_AUTH_CONTENT, and relative HOME / XDG_DATA_HOME / XDG_STATE_HOME paths. Those setups opened in a terminal instead. That was stricter than the common pattern, which passes the user's OpenCode environment through at every launch and never refuses a chat over its contents.

What changes for you: OpenCode with inline credentials or relative folders now opens the chat view like any other setup. If the credentials are wrong, the chat shows OpenCode's own error.

How (6d13791dc99):

  • A chat without a managed account records only that; no folders and no credentials are saved.
  • Every launch passes your current OpenCode variables through unchanged. The two refusals, the stripping of those variables and the pre-launch account check are deleted (121 lines added, 386 removed).
  • Chats on an Orca-managed OpenCode profile still record that profile's id.

Since then main has been merged in three times. The last merge (4aa7e63b16a) brings in #25947 with no conflicts. On that tree both typechecks pass locally, and so do 16 launch and account test files (125 tests). CI on the previous head 7c518bc110c was all green; CI is re-running on the new head.

Not verified live after this change: the inline-credentials chat. Unit and rig tests cover it.

…hat-opencode-serve

# Conflicts:
#	src/shared/agent-session-account-home.ts
#	src/shared/agent-session-record.ts
…hat-opencode-serve

# Conflicts:
#	tests/e2e/cross-version-wire/structured-agent-session-surface-manifest.ts
@brennanb2025
brennanb2025 merged commit a551aa5 into main Oct 7, 2026
8 checks passed
luisgonzaleznf added a commit to luisgonzaleznf/orca that referenced this pull request Oct 7, 2026
Main's new host-agents route (stablyai#25845) imports `LaunchAgentInNewTabResult`
from the new-tab launcher, which this branch moved into
`launch-agent-in-new-tab-result` to keep the launcher under the line limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant