Skip to content

fix(opencode): report OpenCode 2 status from each pane's own TUI - #23722

Merged
brennanb2025 merged 26 commits into
mainfrom
brennanb2025/opencode2-same-folder-panes
Sep 30, 2026
Merged

brennanb2025 merged 26 commits into
mainfrom
brennanb2025/opencode2-same-folder-panes

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 19 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​2279 $\color{#cf222e}{\Huge{\mathbf{−}}}$​23 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​2256
Prod 40 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​995 $\color{#cf222e}{\Huge{\mathbf{−}}}$​163 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​832

What this fixes and where

The bug. With two or more OpenCode 2 panes open (in the same folder or in different worktrees), a turn run in one pane showed its spinner and its Done on a different pane: the pane that happened to start OpenCode's shared background service. When two panes' turns overlapped, the pane that finished first never got its Done, and with panes in two worktrees the second pane stayed on Working indefinitely. opencode run and opencode mini had the same problem: their status landed on the pane that started the service, not the pane running them.

Why it happened. OpenCode 2 runs one background server (the "shared service") for every OpenCode 2 pane, and Orca's status plugin lived inside that server. The server's environment only names the pane that started it, so every status it posted was stamped with that pane. The plugin also kept a single status for the whole server, so "pane B finished" was published as "pane A is still busy". A host-side repair job (the "session-to-pane binder") tried to re-assign sessions to panes by matching OpenCode's session database against running processes, but it deliberately gave up when two panes shared a folder, and it only ran on the local machine.

What you see after this PR. Each OpenCode 2 pane shows its own Working, Needs input and Done, and gets its own Done notification, no matter how many panes share the folder or overlap in time. opencode run shows Working and Done on the pane that runs it. opencode mini shows nothing instead of lighting up the wrong pane.

Scope.

  • Affected surface: OpenCode 2 in Orca terminal panes. The full-screen opencode app (including opencode --standalone) on local, SSH and WSL hosts, and opencode run on local macOS, Linux and Windows panes and on panes hosted by another machine's Orca server.
  • Conditions: more than one OpenCode 2 pane sharing the background service, most visibly with overlapping turns. A lone OpenCode 2 pane that started the service itself was already correct.
  • Also touched, for every agent: the renderer code that decides whether a Done is a repeat of an already-notified completion (see "Fixed during review"). For agents that set a "working" window title (Claude, Codex and the other title-driven agents) it only makes a reset their working title already does happen slightly earlier; review found no behaviour change for them.
  • Not affected: every other agent's status; OpenCode 1 panes (their plugin and the binder behave as on main, except that an OpenCode 1 run no longer blinks idle at start and its Done no longer vanishes at exit); MiMo Code (its plugin file gains two unused lines, no behaviour change).
  • Not solved yet (each labelled below): opencode mini shows no status on any pane; opencode run over SSH or WSL shows no status; an opencode run that starts while its tab is not the visible tab shows no status. On main all three showed on the pane that started the service, which was right only when that pane was the one running them.

ELI5

Before, one shared OpenCode helper reported for every OpenCode pane, and it always signed its reports with the name of the first pane. Now each pane's own OpenCode window reports its own status, so the status lands on the right tab, and the shared helper stays quiet. For opencode run, which has no window of its own, Orca watches the pane itself: Working while the command runs, Done when it exits.

What Changed

Before (user): a turn in the second OpenCode 2 pane lit up the first pane's tab. Overlapping turns left one pane without its Done, and a pane in a second worktree could stay on Working forever. opencode run and opencode mini lit up the first pane too.

After (user):

  • Each OpenCode 2 pane shows its own Working, Needs input and Done, whether or not other panes share the folder or overlap in time.
  • A pane that started a turn still reaches Done if you switch it to another OpenCode session mid-turn.
  • opencode run in a pane shows Working on that pane about 0.4 s after you press Enter, and Done when the command exits, with one completion notification. The Done stays on the pane. Ctrl-C shows the Done as Interrupted on macOS and Linux. The row carries no prompt text. With OpenCode status turned off in Settings, no status is shown.
  • opencode mini shows nothing, on its own pane or any other.
  • After an Orca upgrade, an already-running OpenCode service picks up the new plugin when Orca starts, with no OpenCode restart.
  • New: a freshly opened OpenCode 2 pane shows an idle agent row as soon as OpenCode starts (a check icon on the tab and the worktree row, no notification, not marked unread). This is the same "session started" row Claude panes get when Claude starts. On main, no row appeared until the first prompt.

Mechanism 1: a reporter in each full-screen OpenCode 2 pane.

  • Orca now installs the same status plugin a second time as an OpenCode 2 TUI plugin (TUI = the full-screen terminal app that runs inside the pane), at plugins/<name>-tui/tui.js beside the existing server plugin file. The TUI process runs in the pane's own terminal and carries that pane's key, so every status it posts names the right pane by construction.
  • On every OpenCode event and every 100 ms, the reporter re-reads OpenCode's own session data in the TUI and derives the pane's status from it: Needs input if the conversation or any of its subagent sessions has an open permission or form while running, otherwise Working if any of them is running, otherwise Done. It keeps no "a turn started" or "a turn ended" flags that could get stuck.
  • A pane owns a conversation once the pane shows it while it is running. It keeps owning it while it runs, even if the user browses to another session, and lets go once it is neither shown nor running.
  • Ownership and the last reported status live in OpenCode's per-TUI memory store, which OpenCode keeps across plugin reloads and drops when the TUI exits. A plugin reload mid-turn therefore posts nothing from the old copy, and the new copy continues from the same memory, so a reload never shows a false Done.
  • It posts only when the derived status changes, through the plugin's existing delivery code, on the same hook events and routes the server plugin used. Needs input payloads go through the same translation function the server path uses. It re-checks open requests after OpenCode reconnects and re-posts its status within about 5 s if Orca restarts mid-turn.
  • When a TUI starts with nothing running, it posts one idle "session start" event, which clears any status an earlier process left on the pane. Plugin reloads skip it.

Mechanism 2: the shared server stands down. The OpenCode 2 server plugin now does nothing in any serve process (the shared service, or the private server behind --standalone) when the TUI copy is installed beside it. An installer that predates the TUI copy (for example an older SSH relay) writes only the server file, so that server keeps reporting as it does today. The server path keeps #23700's rule that a plugin unload posts nothing.

Mechanism 3: opencode run reported from the pane's own process. OpenCode 2's run loads no plugin in its own process and its session runs inside the shared service, so no plugin can say which pane it belongs to. Orca's main process (or the Orca server that owns the terminal) now does it from the pane itself:

  • When the pane's shell marks a command start (the OSC 133 "command started" marker Orca's shell integration already prints), it waits the same 350 ms the pane tracker already uses, then reads the pane's foreground process name. It re-reads at about 1.5 s and 7.5 s only while the foreground is still the shell or a JavaScript launcher (node, npx, bun and similar) that may still start OpenCode.
  • Only when the name is opencode or opencode2 does it read that process's command line, and only if the command is run does it post Working. When the shell marks the command finished, it posts Done (Interrupted for exit code 130). A new command start also ends an armed run with Done.
  • The rows are written into the one status store with the provenance "derived from the pane's own process". If a hook reports the same pane after the command started (OpenCode 1 run loads its plugin in-process, as does an OpenCode 2 server with no TUI copy beside it), the store drops this producer's writes for that command, so there is never a second Done.
  • The Done is published after the terminal's "command finished" event, so the existing rule that clears an exited agent's unchanged row sees a changed row and keeps it.
  • A pane where an Orca-launched agent previously exited is revived by this Working, through the same branch a new prompt from a hook uses.
  • Windows panes use the native process table (no PowerShell or other interpreter is started). SSH and WSL panes are skipped, because their foreground process lives on another host or inside the guest.
  • For OpenCode 1 run, the plugin no longer posts its idle "session start" row inside a run process, which removes a Working, idle, Working blink at start.

Mechanism 4: the binder keeps covering OpenCode 1 serve + attach, and only that. An OpenCode 1 opencode serve shared by opencode attach panes has the same "one server stamps one pane" problem, and the binder is what re-assigns those sessions today. It now reads only OpenCode 1's session table, and OpenCode 2 plugin posts carry a new optional opencodeMajor: 2 field that the binder never touches. OpenCode 2 session-start posts no longer trigger binder rounds. Side effect: on a database both OpenCode versions have written, main read only the OpenCode 2 table and missed every OpenCode 1 session; the binder now finds them.

Mechanism 5: refresh installed plugins at app start. After the first window paints, Orca rewrites its OpenCode status plugin files, TUI copy first, but only where Orca's install already exists (the global OpenCode config folder, Orca's per-source config overlays and the retired shared folder), only when the bytes differ, and only when status for that agent is on. The SSH relay does the same for its ~/.config/opencode install when Orca connects. This exists because a running OpenCode 2 service keeps the old plugin until the file changes: in live QA, after an upgrade with the service still running, the old wrong-pane behaviour continued until a new pane was opened. Unchanged files are not rewritten, and nothing is created for users who never used OpenCode.

Left to #23717: the server-side rule that re-derives Working from OpenCode 2 step events is not in this PR. This PR's server path translates events exactly as main does, so whichever PR lands second rebases onto one shared translation.

Fixed during review

Done notifications stopped in a pane after an OpenCode process exited without a reported turn (commit 565feb79c84).

  • What QA saw: after an opencode run with OpenCode status turned off, or after quitting an OpenCode client with no turn running, that pane's later OpenCode 2 turns still showed Working and Done on the tab, but never sent a Done notification and never marked the tab unread. Other panes kept notifying. It lasted until the pane was closed.
  • Why: when Orca sees an agent's process exit that no status report covered, it notifies once and remembers "this pane's agent completed". That record names only the agent, not the turn, and any later reported Done from the same agent in that pane is treated as a repeat of it and dropped. Only a "working" window title cleared the record, and OpenCode never sets one, so the record never went away.
  • The underlying latch predates this PR: the code involved is byte-identical on main, where it is reachable for OpenCode 1 full-screen panes (by code reading, not run live). This PR made it reachable for OpenCode 2, because OpenCode 2 turns now report on each pane instead of all landing on the pane that started the service.
  • Fix: a new reported turn start (Working) in the pane now ends that exit record, since a new turn cannot be a repeat of an earlier exit. Records made from reported turns are kept, so the same turn is still never notified twice, and replayed Dones are still suppressed.
  • Test: agent-completion-coordinator-process-exit-turn-boundary.test.ts drives the real two paths (the pane's process watcher and the status-report path). Without the fix, 0 of the 2 later Dones notify; with it, both do.

Why

  • Only the pane's own process knows the pane. OpenCode 2 gives server plugins no client identity: prompts and session events carry none, and a session's client environment is kept in server memory that plugins cannot read (checked on 2.0.14 and in the 2.0.18 source). Anything done in the shared server can only guess. The TUI runs in the pane, carries its environment and knows which session it shows, so reporting from there removes the guess instead of improving it.
  • Deriving from OpenCode's own session data, not replaying events. Earlier revisions of this PR fed TUI events into the old server status engine and kept finding missed starts, missed ends and out-of-order posts. OpenCode keeps its session data current and refetches it after a reconnect, so reading it directly removes that whole class of bugs.
  • One reporter per pane fixes the stuck Working by construction. The old engine was built for one pane per process. With one reporter in each pane, "B finished" can no longer be overwritten by "A is busy".
  • run's process lifetime is its turn. opencode run answers one prompt and exits, and the pane's foreground process and shell markers are facts Orca already reads on the host that owns the terminal. That is deterministic and cannot land on another pane.
  • The notification fix resets at the turn boundary instead of adding a guard. The stuck record existed because an exit record with no turn outlived a new turn. A reported Working is the status path's own proof that a new turn began, which is what a working title already does for title-driven agents. The alternative (stamping each exit record with a time and comparing) covers no extra case and adds a stored field.
  • Rejected: an undocumented way to make the shared service reveal a session's client environment. It worked on 2.0.18, but it breaks OpenCode's hook contract, would start a real shell for every turn if OpenCode ever ignored hook errors (a pattern Windows endpoint security software flags), and still had a wrong-pane case.
  • Rejected: reading opencode mini's painted screen. A real terminal capture of mini on 2.0.18 shows no title, bell or notification sequence at turn start, turn end or on a permission prompt, so the only signal is painted text, and status rules must not depend on screen scraping.
  • Rejected: adding opencode run to Orca's list of one-shot agent commands. That list removes agent identity from the pane everywhere, which would also silence OpenCode 1 run, whose own plugin reports it.
  • Rejected: keep the server reporting and have the TUI tell Orca which session it shows. The TUI switches to a new session about 60 ms after the turn starts, so the first posts would still land on the wrong pane, and the stuck-Working bug would remain.
  • Rejected: deleting the binder. An earlier revision did, and OpenCode 1 serve + attach panes then reported on the serve pane.
  • Rejected: always launch OpenCode 2 with --standalone. It covers only agents Orca launches, not a user typing opencode, and gives up OpenCode's shared service.
  • Rejected: bind same-folder sessions by timing (fix(opencode): bind same-directory sessions to the pane that submitted the prompt #22847). Still a host-side guess, and it fixes neither the stuck Working nor SSH.
  • Rejected: host rules for the upgrade moment (clearing OpenCode 2 rows when the plugin changes, or preferring pane-reported posts). Both race the old plugin's last posts and would add a permanent host rule for a one-time transition.

How this differs from the standard per-process design

The standard design has each agent process report its own status from inside that process, with nothing inferred on the host. Every place this PR departs from that, or leaves a gap, is listed here. INTENDED means it stays, with the reason it is safe. TEMPORARY means it goes, and names what removes it.

  • Status from a plugin inside each full-screen pane's own OpenCode process: INTENDED. This is the standard design applied to OpenCode 2, including a 100 ms check of which session the pane shows. Three details differ, each deliberately: (1) a pane keeps owning a running turn after the user browses away, so the pane that started it still gets its Done, as it did when each pane had its own server; (2) the TUI copy is installed through OpenCode's documented plugin-folder discovery instead of an edit to the user's OpenCode config file, so Orca never rewrites a user file; (3) the shared server stands down at runtime only when the TUI copy exists, instead of never reporting, so an older SSH relay that writes only the server file keeps reporting.
  • Refreshing installed plugin files at app start and on relay connect: INTENDED. The standard design rewrites the file at the next install and relies on OpenCode's reload. That left the wrong-pane behaviour running after an upgrade in live QA until a new pane opened. The refresh only rewrites files Orca already installed and only when the bytes differ.
  • Idle "session start" row on a fresh OpenCode 2 pane: INTENDED. It is the same row Claude panes get, and it clears a status an older shared service left on the pane.
  • opencode run status derived on the host from the pane's foreground process: INTENDED. run loads no plugin in its own process, so the standard design has nothing to report from. For a one-shot command the process lifetime is the turn, so this is exact, not a guess. It reads the foreground only at command start (at most three times), never polls, and never reads the screen. It is safe to keep because both facts come from the pane itself, it yields to any hook that reports the same command (so if a future OpenCode reports run from inside the process, that report wins with no Orca change), and when a read fails the pane stays silent rather than guessing.
  • The OpenCode-1-only session-to-pane binder: TEMPORARY. It is a host-side guess (session database matched against a process sweep), kept only so OpenCode 1 serve + attach does not regress. It and the opencodeMajor post field that gates it go when OpenCode 1 support is removed.
  • opencode run over SSH and WSL is silent: TEMPORARY. The execution host owns this status, and Orca has no producer there yet. Removed by a producer on the remote or guest host (a command-marker scanner and foreground read in the SSH relay and the WSL relay) plus a capability-negotiated wire field carrying the "derived from the pane's process" provenance.
  • opencode run that starts while its tab is not the visible tab is silent: TEMPORARY. While a pane is hidden, the terminal daemon scans its output and forwards "command finished" but has no "command started" event, so the run is never armed. Reached by a script or another agent sending opencode run into a background terminal, and by keyboard type-ahead: typing opencode run … while an earlier command is still running in that pane, then switching tabs before it starts. Removed by a daemon "command started" event (a new daemon-to-app event kind).
  • opencode mini is silent: TEMPORARY. OpenCode 2.0.18's mini emits no per-turn signal Orca could read (no title, bell or notification sequence), loads no TUI plugin, and its turns run in the shared service. Removed by an OpenCode API that exposes a session's client environment to plugins, or by mini loading TUI plugins. Asking OpenCode for it waits on a maintainer go-ahead.
  • opencode mini --standalone and opencode run --standalone: TEMPORARY. Their private server also stands down (the stand-down checks only that the TUI copy is installed, not whether a full-screen app is attached), so mini --standalone goes from correct on main to silent, and run --standalone shows only Working and Done, no Needs input. Removed by the same OpenCode change that fixes mini. Orca's own launcher runs the full-screen app, which is covered.
  • Some opencode run command shapes are not recognised: INTENDED. A compound line whose first part is still running when Orca looks (make && opencode run …, sleep 1; opencode run …); wrappers that keep their own process in front (caffeinate -i opencode run, op run -- opencode run); npx opencode-ai run (unverified, the foreground usually stays node); a script that calls it; opencode run … &; another agent's shell tool running it; an OpenCode binary under a path containing a space on macOS or Linux. Covering these would need continuous polling of every command. Kept because the result is silence on that pane, never status on the wrong pane.
  • An opencode run longer than 30 minutes shows "No update in 30m" instead of Working until it ends: TEMPORARY. Nothing re-confirms the row while the command runs. Removed by a follow-up that refreshes the row while the run is still armed.
  • Windows: a run that has to start the shared service may be silent (unverified): TEMPORARY if real. On Windows the service starts as a child of that run, so the pane's process tree may hold two opencode.exe and the foreground read then picks neither. Later runs, with the service already up, are unaffected. Removed by picking the run process by its command line when several OpenCode processes match.
  • Main's per-terminal prompt record also stores the run Working and Done that the status store dropped for a hook (unverified effect): TEMPORARY. For an OpenCode 1 run, orca terminal wait --for tui-idle could briefly see "working" until the command exits. Not reproduced. Removed by recording only what the store accepted.
  • Panes hosted by another machine's Orca server: rarely, an opencode run Done can disappear on the connected client: INTENDED. It needs a sibling pane in the same worktree updating continuously right as the run ends; it never happens on the host or on mobile, and the Done comes back the next time the host publishes that worktree.
  • Windows long-running non-agent commands get all three foreground reads: INTENDED. On Windows the name read reports the shell for them. Each read is a cached native process-table read (about 6 to 13 ms) with no process spawned.
  • A client that exits mid-turn while a separate server keeps reporting that turn to the same pane can notify twice: INTENDED. Only possible with OpenCode 1 serve + attach routed by the binder (not seen live). The first notification is the pre-existing exit notification at quit time; the second is the turn's real completion, which before the review fix was swallowed. Two notifications is better than one at the wrong moment.

Known behaviours not caused by this PR

Each of these is identical on main (measured in round 3 QA on both builds unless noted):

  • Quitting an idle OpenCode client fires one "task complete" notification. Seen for opencode mini and opencode attach, on both builds.
  • An agent's process exit notifies even when that agent's status is turned off. This is the same for every agent on main: the status setting turns off status rows, not the notification Orca sends when it sees an agent process exit that nothing reported. In round 3 it showed as one notification at the end of a status-off opencode run (once, at the moment status was switched back on). Making the setting also silence it would change every agent's behaviour, so it is out of scope here. After the review fix, it no longer mutes the pane's later notifications.
  • OpenCode 1 serve + attach: in QA's setup, a turn sent from an attach pane showed on the serve pane on both main and this PR, in all three variants tried. The binder behaves as on main; QA did not establish why it did not bind in that setup (the rig gave OpenCode 1 its own data folder, and two attach clients in one folder are ambiguous by design). The binder's unit tests cover the attach-pane case.
  • Quitting any agent in a terminal tab hidden for more than 5 minutes leaves its last Done row in the sidebar (Orca unmounts such tabs to save memory) until the next command in that pane. Same on main for Claude, Codex and OpenCode 1; this PR only makes every OpenCode 2 pane behave like those.

Known limitations

  • Windows: cmd.exe panes, and Git Bash panes Orca starts without a startup command, print no command markers, so run shows nothing there. PowerShell prints the start marker only with PSReadLine loaded. PowerShell reports only 0 or 1 as the exit code, so Ctrl-C shows a plain Done, not Interrupted. Nothing was run on Windows.
  • One-time glitch when upgrading from v1.4.216 with a turn running. The released plugin posts a final Done when it is unloaded (fix(opencode): keep OpenCode 2 panes Working across plugin reloads #23700's fix is not in a release yet), and this PR cannot change old code. Measured in QA: with the turn in the pane that started the service, that pane went straight from Working to the real Done with one notification. With the turn in another pane, the starting pane switched to a false Done (check icon, no notification, not unread) while the pane running the turn showed Working until its real Done. After that one upgrade, plugin reloads are silent.
  • A turn the pane navigated away from during that upgrade shows the pane as idle and skips that turn's Done. One-time, upgrade only.
  • Idle row on every fresh OpenCode 2 pane. It uses the completed-check icon, so it can read as "a task finished here" on a pane where nothing ran yet. No notification, not marked unread.
  • Two panes viewing the same running session both show Working, and both get its Done.
  • A permission parked on an idle session through OpenCode's external HTTP API shows Done, not Needs input. Permissions and forms a tool asks for always wait inside a running turn, so the normal flow is unaffected.
  • Downgrading Orca leaves the TUI copy in place, so the pane reporter and the older version's shared-service plugin both report. The pane reporter's posts are correct, so it is no worse than the older version alone.

Linked Issue

Follow-up to Linear STA-8629 (OpenCode 2 shows Done while still working), #23700 and #23717. Supersedes #22847.

Visual Proof

Round 2 of live Electron QA on a remote Mac with real OpenCode 2.0.18 (details under Testing). Status shows on the tab strip and the worktree rows: spinner = Working, bell or green check = Done, orange = Needs input. The full-screen pane reporter has not changed since these were taken. Round 3 screenshots (opencode run, mini, OpenCode 1) are in the round 3 section below.

Main, turn run only in the second pane: the bell lands on the first pane's tab ("OpenCode"); the pane that ran the turn shows nothing.

main: the second pane's Done shows on the first pane's tab

This PR, same steps: the bell is on the pane that ran the turn. The first pane shows only its idle check.

PR: Done shows on the pane that ran the turn

Main, overlapping turns in two worktrees: after the second pane finished, its row (wt2) is still spinning, and it stayed on Working.

main: the second worktree's pane stuck on Working after its turn finished

This PR, overlapping turns in one folder: the short turn's pane has its Done while the long turn's pane is still Working, and then both reach Done.

PR: the short turn's pane is Done while the long one is still Working

PR: both panes Done after the long turn ends

This PR, Needs input: OpenCode asks for permission and only that pane shows the orange Needs input icon, while another pane keeps working.

PR: Needs input on the pane waiting for permission

This PR, a freshly opened OpenCode 2 pane (the new idle row, see Known limitations): check icon on the "OpenCode" tab before anything has run.

PR: fresh OpenCode 2 pane with the idle row

Live QA, round 3: opencode run, mini and OpenCode 1

Setup: a separate Apple M4 Mac, real OpenCode 2.0.18 (@opencode/cli) and OpenCode 1.18.33 (opencode-ai), both on the free longcat-2.5-preview-free model, auto-update off. Two real Electron builds side by side, each in a hidden window with its own isolated home, OpenCode service and app profile, driven through the Chrome DevTools protocol (no desktop input): this PR at 5bb3190ea04 (before the review fix) and main at fb67d5d7c3f. Status changes were timed from an in-page subscription to the status store and checked against OpenCode's own database. Notifications were counted from the app log, because the unsigned build cannot display them. Panes: A = the first OpenCode 2 pane (it starts the shared service), B to E = other panes in the same folder, S = an OpenCode 1 serve pane, A1 and B1 = OpenCode 1 attach panes, R = a pane for OpenCode 1 run.

Scenario Main This PR
opencode run in B, B's tab visible when it starts Not run with status on; with the tab visible and status off it still landed on A (row below), as did the hidden run Working on B 0.40 s after Enter, Done at exit, still Done 30 s later; one notification; A untouched
opencode run in B, B's tab hidden when it starts Working and Done on A; B shows nothing Nothing on any pane, no notification (the run itself completed). TEMPORARY gap, see differences
Ctrl-C of a long opencode run in B (twice) not run Done marked Interrupted (exit code 130), one notification each
OpenCode status turned off, opencode run in B Ignores the setting: Working and Done still on A, one notification No status on any pane. One notification at the run's exit (pre-existing, see above); afterwards B's later Dones stopped notifying, fixed in 565feb79c84. With OpenCode and OpenCode 2 both off: nothing at all
opencode mini turn in C Working and Done on A, one notification Nothing on any pane, nothing else changes. TEMPORARY gap, see differences
Quitting an idle mini or attach client One notification (measured for mini) One notification for each (pre-existing)
Second full-screen OpenCode 2 pane B, turn only in B not rerun (round 2: on A) Working then Done on B only, Done at the second OpenCode marked the session idle
Overlapping turns: long turn in A, short turn in B not rerun (round 2: only A's tab moved) B Done while A still Working, then A Done; neither reverted 30 s later. B's notification was missing at this head, fixed in 565feb79c84
OpenCode 1 serve in S, attach in A1 and B1, turn only in A1 (three variants) Working and Done on S every time; A1 nothing Identical to main (not caused by this PR)
OpenCode 1 opencode run in hidden pane R An idle Done row 1.4 s before Working, then the Done vanished 3 ms after exit; one notification Straight Working to Done, the Done stays; one notification

Main, opencode run in Terminal 2 (B, hidden): the spinner is on the first tab, the pane that started the service.

main: opencode run is running in Terminal 2, but the spinner is on the first tab "OpenCode" and Terminal 2's tab shows nothing

This PR, the same command with Terminal 2 visible: the spinner is on Terminal 2's tab and in the worktree row.

PR: opencode run in Terminal 2 shows the spinner on Terminal 2's tab and in the worktree row, while the first tab shows only its idle check

This PR, 30 s after that run ended (with Terminal 2 hidden again): Terminal 2 still carries its Done.

PR: 30 seconds after the run ended, Terminal 2's tab still shows the Done bell and the first tab is unchanged

This PR, Ctrl-C of a long opencode run: the worktree row shows the red Interrupted dot.

PR: after Ctrl-C of a long opencode run, Terminal 2's tab has the Done bell and the worktree row shows a red Interrupted dot

This PR, OpenCode status turned off: a running opencode run shows no spinner anywhere.

PR: with OpenCode status off, opencode run is running in Terminal 2 but no tab or worktree row shows a spinner; the sidebar row is the first pane's earlier Done

Main, opencode mini working in Terminal 3: the spinner is on the first tab.

main: opencode mini is working in Terminal 3, but the spinner is on the first tab "OpenCode" and Terminal 3's tab shows nothing

This PR, the same mini turn: no tab changes.

PR: opencode mini answered "pong" in Terminal 3 and no tab shows a spinner or a new Done; the first tab keeps its idle check

This PR, overlapping turns in one folder (the tab titles are OpenCode's own text): B is Done while A is still Working.

PR: the second tab (pane B) shows its green Done check while the first tab (pane A) still shows the Working spinner

This PR at 5bb3190ea04, before the review fix: both turns finished with their tabs hidden, but only A's tab got the unread bell. B's tab shows only a green check because its notification was swallowed (fixed in 565feb79c84).

PR before the review fix: the first tab has the unread Done bell, while the second tab, which also just finished, shows only a green check with no notification

This PR (main looks the same): an OpenCode 1 turn sent from an attach pane spins on the serve pane.

PR, identical on main: an OpenCode 1 turn sent from an attach pane shows its spinner on Terminal 6, the serve pane, while the two attach tabs labelled OpenCode show nothing

Live QA, round 4: the notification fix

Targeted rerun on the same separate Mac, real OpenCode 2.0.18, this PR at 565feb79c84, in a real Electron build (arm proven by grepping the built renderer for the fix). Round 3 had found that after an opencode run with OpenCode status turned off, that pane's later OpenCode 2 Dones showed Done but never notified. Every Done below landed while another tab was active; a notification is one agent-task-complete dispatch in the app log.

Scenario Result Notifications
Status off, opencode run in pane B, status back on, then two OpenCode 2 turns in B No status for the run; each later turn Working → Done on B, B marked unread 1 per Done (round 3: 0). The run's exit still logs one line (known, not caused by this PR, see above)
Pane E: one turn, quit the idle OpenCode with Ctrl-C, start it again, one more turn Working → Done on E both times 1 per Done
Overlap: long turn in A (the pane that started the service), short turn in B during it B Done while A still Working, then A Done; neither reverted 60 s later 1 for B, 1 for A
Status on: opencode run in pane R, then an OpenCode 2 turn in R Working 0.4 s after Enter, Done at exit; then the turn Working → Done 1 each; the run's Done and its process exit did not both notify

Pane B's tab "OC | one" and its worktree row carry the unread bell after B's second turn ended with another tab active
Pane E's tab has the unread bell after a turn in an OpenCode that was quit and restarted in the same pane
Overlap: pane B already Done while pane A's tab and sidebar chip still spin
After A's Done: A's tab "OC | finished" has the unread bell
Status-on opencode run: pane R's tab has the unread bell after the run exited

Not rerun this round: the main arm (round 3 covered it), and round 3's scenarios that passed.

Round 5: smoke test after merging main. The branch then merged current main, which includes a change to the same renderer code that handles an agent's process exiting (#23948). At the merged head 7b2d32624cc the status-off-run scenario and the status-on opencode run control were rerun: every Done notified exactly once (status-off run then three turns in B; opencode run then a turn in R), and the run's Done and its process exit did not both notify.

Merged head: pane B's tab "OC | one" and its worktree row carry the unread bell after B's second turn
Merged head: pane R's tab has the unread bell after a status-on opencode run exited

Testing

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

Unit tests.

  • hook-plugin-opencode2-tui-ownership.test.ts runs the real generated plugin against a fake OpenCode TUI: the two-pane overlap, the session switch right after a turn starts, navigating away mid-turn, a plugin reload mid-turn ending Done with nothing posted on unload, re-posting an undelivered status, subagent turns and permissions, requests answered while disconnected, the Orca-restart re-post, the fresh-pane idle row, the server standing down, and OpenCode 1 staying silent.
  • opencode-run-lifetime-status.test.ts and opencode-run-lifetime-runtime.test.ts (the second through Orca's real terminal byte path): Working then Done on the right pane, Ctrl-C, the re-read schedule and when it stops, status turned off, SSH and WSL panes skipped with no reads, a local Windows pane, a new command ending an armed run, and the Done arriving after the "command finished" event.
  • server-process-lifetime-status.test.ts (real hook HTTP posts): the process producer yields to a hook that reported during the command, revives a pane Orca retired, and never lets terminal status or a lone Done into a retired pane.
  • pty-connection-command-finished-late-done.test.ts: the renderer keeps a Done that lands after the "command finished" event and drops one that lands before it.
  • agent-completion-coordinator-process-exit-turn-boundary.test.ts: after an unreported process exit in a pane, the next two reported turns each notify (0 of 2 without the fix), and a process exit followed by the same turn's Done still notifies once.
  • hook-plugin-run-process-session-start.test.ts: no idle start row inside an OpenCode 1 run process; still posted for the TUI and serve.
  • Binder tests: OpenCode 1 serve + attach reports on the attaching pane, an OpenCode 2 session never binds, and an OpenCode 2 post is never moved.
  • Startup-refresh and relay tests for the app-start and connect-time refresh and the TUI copy.
  • Each fix was ablated (removed) and at least one of these tests failed without it.
  • At 565feb79c84: a 43-file set (all 19 changed test files plus every completion-notification and hook-completion suite) passes, 393 of 393. A first run had one 30 s timeout under heavy machine load in a test that never reaches the changed code; it passed on the rerun and in isolation.
  • At 5bb3190ea04: neighbouring suites (1132 files, 12,689 tests) pass except two tests that timed out under machine load (src/relay/subprocess.test.ts and pty-connection-command-finished-cleanup.test.ts); both pass when run alone.
  • Typecheck: tc:node and tc:web pass with no errors at 5bb3190ea04; tc:web passes at 565feb79c84 (the fix changes only renderer files). oxlint and the changed-code quality gate report 0 new findings on the changed files.
  • At 7b2d32624cc (after merging main, which brings fix(sidebar): an agent's row stays while it runs, whatever its tab title #23948 into the same process-exit code, plus one refactor that shares the foreground read schedule as a single value so a renderer file stays under the line limit): 44 targeted test files pass (1,805 tests); tc:node and tc:web pass; oxlint, the changed-code quality gates and the line-limit ratchet are clean on the 59 changed files; CI is green (typecheck, static analysis, all five test shards, both package builds).

Live QA, round 3 is above. Live QA, round 2 on a remote Mac (Apple M4), real OpenCode 2.0.18 (@opencode/cli) with the free longcat-2.5-preview model, Orca in a hidden window driven through the Chrome DevTools protocol, each app with its own isolated home and OpenCode service. Main arm ceffecf12a0e (includes #23700), PR arm 1a1754618f4 (before the run producer and the OpenCode 1 binder were added), release v1.4.216 (20d7a7d185cd) for the upgrade. Every Done matched, to the second, the moment OpenCode's own database marked the session idle.

Scenario Main This PR
(a) Turn only in the second pane, same folder Working and Done on the first pane's tab; nothing on the pane that ran it Working and Done on the second pane only
(b) Long turn in A, short turn in B during it, same folder Only A's tab moved, carrying B's prompt; B never showed Working or Done B Done while A still Working, then A Done
(c) As (b), panes in two worktrees Second worktree's pane stuck on Working indefinitely Each row reached its own Done
(d) opencode --standalone One Working, one Done on its own pane Same, no duplicate posts although its private server also loads the plugin
(e) Switch the pane to another session mid-turn Done on that pane (only because it started the service) Stayed Working through the switch, Done when the original turn ended
(f) Upgrade from v1.4.216 with a turn running, no OpenCode restart n/a The service and running panes reloaded the new plugin within about 1 s of Orca starting; results as in Known limitations; new turns then reported on their own panes
(h) Needs input while another pane runs a long turn not run Needs input on that pane, Done after approving; the other pane unaffected
(i) Fresh OpenCode 2 pane, nothing sent No row Idle row with a check icon, no notification
(j) Plugin reload mid-turn (edit and restore the plugin file) not run Stayed Working with no change, one Done at the real end
  • Round 2 logged one extra "task complete" notification line on the PR arm (14 lines for 13 explained transitions). Re-running every candidate step with the tab unfocused gave exactly the expected count each time, so it was not reproduced. No real Done notification was missed.
  • Not tested live: Windows, Linux, SSH, WSL; whether OpenCode 2 versions older than 2.0.18 pick up the new TUI plugin folder without a restart. System notifications cannot display on the unsigned build, so they were counted from the app log.

Review

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

  • Security: no new inputs. The run producer reads the command line of the pane's own foreground process only when its name is OpenCode, uses it only to recognise run, and neither stores nor sends it.
  • Backwards compatibility: plugin posts gain one optional top-level field, opencodeMajor, which older hosts ignore; a plugin that predates it sends none and gets main's binder behaviour. Rows from the run producer use the "process" origin value the published status format has accepted since feat(agent-status): add the canonical store and child-work contract #20717, so older paired clients and mobile read them. The rest of the provenance stays inside the host process. An older SSH relay writes no TUI copy, so its server keeps reporting as before. A newer relay with an older app: the TUI copy's old plugin code does nothing.
  • Cross-platform: paths use join; the stand-down check reads the TUI file through a file URL, which works on Windows. Windows run reads use the native process table, with no PowerShell or other interpreter spawned. The installers are the ones that already run on macOS, Linux, Windows, WSL and SSH hosts.
  • SSH: the full-screen app's status is produced inside the pane on the host that runs OpenCode, so SSH panes are attributed correctly without host-side matching. run over SSH is silent (see above). Per-pane relay overlays created with a custom OPENCODE_CONFIG_DIR are not refreshed on connect; that gap predates this PR.
  • Risk, unverified: the server stands down as soon as the TUI file exists. If an OpenCode 2 version older than 2.0.18 does not load a newly added TUI plugin folder without a restart, its running panes would show no status until OpenCode restarts. Pickup without a restart was confirmed on 2.0.18.
  • Performance: each Orca OpenCode 2 TUI re-reads its in-memory session data every 100 ms (no I/O) and checks one file every 5 s. The run producer does nothing while OpenCode status is off. Otherwise, per command start in a local pane it sets one 350 ms timer and does one foreground-name read (on the default daemon-backed terminal, a cheap query of the terminal's own foreground process); it re-reads at most twice, only while the foreground is a shell or a JavaScript launcher; the command-line read runs only for an OpenCode process. No polling. OpenCode 2 session starts no longer trigger binder rounds. The app-start refresh is a few file compares after first paint. The notification fix adds one map lookup per reported Working.
  • Mobile: no change; mobile reads the same status store.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

@brennanb2025
brennanb2025 force-pushed the brennanb2025/opencode2-same-folder-panes branch 2 times, most recently from 1a17546 to 4203db5 Compare September 29, 2026 07:39
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review status: code clean, not ready (one open product decision)

Verdict: the fix works. Every OpenCode 2 pane now shows its own Working, Needs input and Done, and each reaches Done on its own pane: the same folder, overlapping turns, two worktrees, --standalone, a session switch mid-turn, a plugin reload mid-turn, and an upgrade from v1.4.216 with OpenCode's background service still running. The PR stays a draft because OpenCode 2 opencode run and opencode mini panes (and OpenCode 1 serve plus attach) are not solved yet; see "Open" below.

Head: 4203db51db7, rebased onto current main. tc:node passes with 0 errors. The affected suites pass (4171 tests); the one failure is a load timeout in browser-process-user-agent-ordering.test.ts that passes alone and doesn't touch this code. oxlint, the changed-lines quality gate and the max-lines ratchet are clean.

What changed during review (four review loops plus two independent architecture reviews):

  • The pane-side reporter was rewritten to read Needs input / Working / Done directly from OpenCode's own session data in the pane, instead of replaying events through the old server status engine. The first review loops kept finding missed starts, missed ends and flicker in the replay design; reading the data directly removes that whole class of bugs.
  • Pane ownership now lives in OpenCode's per-pane memory, which survives a plugin reload, so a turn that ends during a reload still shows Done. This builds on fix(opencode): keep OpenCode 2 panes Working across plugin reloads #23700's quiet plugin shutdown (merged).
  • Installed plugins are refreshed when Orca starts (only where already installed, only when the bytes differ), so an already-running OpenCode service picks up the new plugin right after an Orca upgrade. The SSH relay does the same when Orca connects.
  • A new OpenCode 2 window now posts the same idle "session started" row Claude panes show, which clears a wrong Working left over from the old plugin during an upgrade.
  • Fixed: reply text knocked a pane off Needs input while a subagent waited on a permission.
  • The server-side step-event rule was removed from this PR and left to fix(opencode): re-derive Working from OpenCode 2 step events #23717.

Live QA on a remote Mac with real OpenCode 2.0.18: main reproduces the bug, and every scenario passes on this PR. Full table in the PR body.

main: the second pane's Done shows on the first pane's tab

PR: Done shows on the pane that ran the turn

PR: the short turn's pane is Done while the long one is still Working

PR: Needs input on the pane waiting for permission

Open (blocks ready):

  • OpenCode 2 opencode run and opencode mini panes show no status on this branch. Neither loads the pane plugin, and the shared server now stays quiet. On main they showed on whichever pane started the shared service. OpenCode 2 gives plugins no documented way to learn which pane a session belongs to; the only working route found relies on undocumented OpenCode internals. Options are with the owner.
  • OpenCode 1 run as a separate opencode serve with opencode attach panes now reports on the pane running serve. The deleted host-side session matcher used to correct that.

Known limits (in the PR body): a one-time false Done when upgrading from v1.4.216 with a turn running in another pane (the released plugin's own shutdown code); a turn the pane had navigated away from during that upgrade skips its Done; two panes showing the same running session both show Working; a permission parked on an idle session through OpenCode's HTTP API shows Done.

Not verified: live runs on Windows, Linux, SSH and WSL; OpenCode 2 versions older than 2.0.18 picking up the new plugin folder without a restart; one extra notification log line on the PR arm that didn't reproduce.

Not caused by this PR: per-pane SSH relay config copies with a custom OPENCODE_CONFIG_DIR aren't refreshed on connect.

OpenCode 2 serves every pane from one shared server whose env names only
the pane that started it, so every same-folder pane's work showed on that
pane, and the plugin's single aggregate swallowed the Idle of a pane whose
turn ended while another pane was busy.

Install the status plugin a second time as an OpenCode 2 TUI plugin
(plugins/<name>-tui/tui.js, written only when its bytes differ, locally,
in overlays and in the SSH/WSL relay installs). In a TUI process setup()
runs the same engine, fed through the same event translation as the
server, but only for root sessions this pane owns: the route's session
from when it starts (or when the route reaches it while running, hydrated
from the TUI's session status) until it settles, is deleted, or the TUI
exits. The server plugin stands down in any serve process when the TUI
copy is installed beside it; a relay that predates the TUI copy keeps the
old behavior. OpenCode 1 loads no plugin directories and is unchanged.

Delete the session-to-pane binder, registry, client sweep and ingest
reattribution: with every post stamped by its own pane nothing is left
for them to correct.

Known gap: OpenCode 2 `opencode run` in a pane has no TUI, so it reports
no pane status.
- The TUI copy now settles an owned root whose run the TUI's session data
  reports ended, with no pending permission or form, when the engine still
  holds it busy. An execution end missed across a service restart or
  reconnect no longer leaves the pane Working until the TUI exits.
- The TUI copy stays idle without ORCA_PANE_KEY. post() cannot report without
  it, so OpenCode 2 TUIs outside Orca no longer run the route poll and engine.
- Installers write the TUI copy before the server plugin file. The server
  decides at load whether to stand down, so a reload between the two writes
  now finds the TUI copy.
…s drain

The TUI's session data applies each event before this plugin's queued handling
reaches it, so settling against it mid-backlog published a false Done before a
fast turn's later steps (Working, Done, Working, Done). Reconcile only when no
event is queued; a mismatch then is a start or end missed across a reconnect,
and both directions are now re-derived (a missed start left the pane on Done).

Also install the TUI copy beside the server plugin in the retired shared hooks
dir, so a TUI or service still loading that dir reports per pane instead of
leaving the service reporting under its starter pane.
A TUI plugin hot reload disposes the plugin while the pane's turn keeps
running. The server path already passes sessionsOutliveDispose so dispose
publishes nothing; the TUI adapter now does the same, or every TUI reload
would still show a false Done. Also refreshes the generated-bytes digest
after rebasing onto the write-if-changed installers.
After an Orca upgrade, an OpenCode 2 service that was already running kept
the previous plugin, and with it the old wrong-pane status, until any new
terminal pane rewrote the file. OpenCode 2 reloads a plugin whose file
changes, so Orca now refreshes its existing installs once after the first
window shows: the global config dir, source overlays and the retired shared
dir, TUI copy first. It reuses the per-pane writers, which skip unchanged
files, never creates an install the user did not have, and honours the
status-hook and per-agent switches. An SSH relay does the same for its
canonical install when Orca connects and ships the plugin sources.

The plugin source assembly moves to its own module (re-exported unchanged)
to keep hook-service.ts under the line limit.
…ion data

The TUI copy of the status plugin translated OpenCode events into the
server-side engine and then patched the engine's latches back toward the
TUI's own session data: a settle on every tick, a queued-event counter, a
re-assert, synthetic Busy and Idle. Each review found another place where
the two copies disagreed: a missed end left the pane Working, a backlog of
slow posts flickered Done, a missed start showed Done mid-turn, a turn held
only by a subagent never settled, and a request answered while disconnected
pinned Needs input.

The TUI reporter now reads the pane's level straight from the session data
OpenCode keeps current (and re-hydrates on reconnect) on every event and on
a 100 ms tick: for each root session this pane owns, Needs input (an open
permission, else form, anywhere in its family while it runs) outranks
Working (any family member running) outranks Done. It posts one status per
level change through the plugin's existing delivery functions (retry,
dedupe, message-part throttle, ordering), so the wire and Orca's ingest are
unchanged. Ownership is kept in OpenCode's storage.memory, which survives a
plugin hot reload, so a turn that ends during a reload still shows Done;
dispose publishes nothing, and a level the old generation could not deliver
is re-posted by the next. On reconnect it re-syncs blockers for the owned
sessions OpenCode would not re-sync itself, ignoring requests already
answered. Events are handled synchronously, so a slow post can no longer
hold up event processing. The server path, OpenCode 1 and mimo are
unchanged.
…streams text

Orca treats every OpenCode MessagePart as Working. While a background
subagent waits on a permission or form, the root session can keep streaming
reply text, and the TUI reporter forwarded that text as a MessagePart. The
pane then flipped from Needs input to Working, and nothing restored it until
the level changed, so the user could miss the open request.

Skip reply text while the pane's level is Needs input, as the reporter
already does for queued prompts.
…change

The shared-server translation re-derived Working from session.step.started.
The pane reporter no longer uses it, so it only changed the server path and
duplicated a separate open change. Drop it; server behaviour matches main.
Before this change, a pane could keep a status an earlier process left on
it. The case that matters is an upgrade mid-turn: the old shared-service
plugin posted pane B's turn under pane A's key, then stood down, and pane
A's TUI loaded the new reporter owning nothing, so it never posted and A
showed a wrong Working until its own next turn.

A freshly started reporter that owns no running turn now posts the host's
existing session-start boundary once, which the host shows as connected
idle: no completion, no notification, and an unseen Done it lands on stays
unread. A plugin hot reload keeps its memory and skips it, and where
OpenCode keeps no plugin memory it is never sent.
…ng pane

OpenCode 1 `opencode serve` in one pane plus `opencode attach` in others
reports every session from the serve process, whose environment names only
the serve pane. Before this branch the session-to-pane binder moved those
posts to the attaching pane; deleting it for OpenCode 2 put them back on the
serve pane.

Restore the binder, registry, correlation and client sweep for OpenCode 1
(and mimo-code, which it also served), gated so OpenCode 2 never uses it:

- The status plugin now sends `opencodeMajor: 2` on every post from a process
  whose loader called setup(), which only OpenCode 2 does. The host skips the
  binder (no rewrite, no kicked round) for any post that carries it. OpenCode 1
  and older plugins send nothing and keep the previous behaviour.
- The binder reads only OpenCode 1's `session` table. OpenCode 2 writes
  `session_v2`, so its sessions never bind; on a database both versions wrote,
  OpenCode 1 sessions are no longer hidden behind the v2 table.

OpenCode-1-only; it goes when OpenCode 1 support is removed.
…on its own pane

OpenCode 2's `opencode run` loads no plugin, and the shared service that
runs its session cannot tell which pane it belongs to, so a pane running it
showed nothing.

The runtime now reports it from the pane's own process lifetime. On the
pane's OSC 133 command start (main already parses 133 for every local PTY;
the start callback was never wired), after the pane tracker's 350 ms settle
it reads the foreground process name through the existing foreground
reader, and only when that name is OpenCode, the foreground command line
from one fresh shell-foreground process-table capture. An `opencode run`
posts Working through the existing terminal-status path into the hook
server's store; the pane's 133;D (or the daemon's background fact) posts
Done, marked interrupted on Ctrl-C. No polling.

Exclusive with plugin reporting: each write carries the command's start
time, and the store drops it once a hook has written the pane since then,
so an OpenCode 1 `run` (in-process plugin) or a server plugin without the
TUI copy owns its command alone and there is never a second Done.

Local macOS and Linux panes only; SSH, WSL and Windows panes stay silent
because their foreground cannot be read on this host.
… Done, on its own pane

Type the selected descendants as process-table rows; ReturnType of the generic collector widened them to bare identity rows.
…xits

The run's Done was published inside the chunk that carried its OSC 133;D,
before the chunk's command-finished fact. The renderer drops an exited
agent's row on command-finished when the row has not changed since that
fact arrived, so it took the Done as the stale row and dropped it, in the
renderer and in main.

Publish the run's Done after the chunk's side-effect facts are emitted (and
after the daemon's background fact). The renderer then sees command-finished
while the row is still Working, and the Done that follows counts as a change,
so it stays, the same way a hook Done that lands after exit already does.
When an agent Orca launched exits, command completion retires the pane, and
only a hook new-turn event revived it. A later `opencode run` in that pane
posts no hook, so its process-lifetime Working was refused and the pane
stayed silent.

A process-lifetime Working is posted only after a fresh OSC 133;C and the
pane's own foreground argv prove a new OpenCode run, which is at least as
strong as a hook new turn. The store now revives the retired pane on it the
same way: it clears the retirement, drops the launch-token fence, and rebinds
the observation. OSC status and a lone process Done still cannot write a
retired pane, and a closed tab stays closed.
The run producer skipped every Windows pane, although Orca already resolves
a Windows pane's foreground agent from the native process table. On main an
OpenCode 2 `run` showed there (on the service's pane); on this branch it was
silent.

The argv read now has a Windows branch: one fresh native table read
(windows-process-table, no interpreter spawn), the same foreground identity
the Windows resolver already computes, and the command line of the process
that identity names. It runs only after the name read says OpenCode. Local
Windows panes whose shell prints OSC 133 C/D (PowerShell with PSReadLine,
Git Bash with Orca's wrapper) now go Working, then Done; cmd.exe prints no
markers and stays silent. SSH and WSL panes stay silent.
…er's ladder

The producer read the pane's foreground once, 350 ms after the command
started. A wrapper, a shim or `sleep 1; opencode run` execs OpenCode later,
so those runs stayed silent. The renderer's pane tracker already re-reads a
command's foreground at 350, then 1200, then 6000 ms for exactly this.

Move those delays to one shared module and use it from both. The producer
re-reads only while the foreground is a non-shell process that is not
OpenCode, and at most on those three rungs; no new polling.
A new OSC 133;C in a pane whose `opencode run` was still armed dropped the
armed state without a Done, so a run whose 133;D never arrived left the pane
on Working. A new command start proves the previous command ended, so it now
posts that run's Done (not marked interrupted: no exit code is known) before
the new command is inspected.
…process

OpenCode 1 `run` loads the status plugin in its own process, and the plugin
posts SessionStart when the run's session is created. The host lands that as
an idle session boundary, so a pane the run producer had already shown as
Working blinked idle before the plugin's Busy.

The plugin now skips SessionStart when its own process is a `run`, read from
its argv the same way isOpenCodeRunCommand reads a pane's foreground (first
positional after global options, past the compiled binary's entry path). A
`run` session goes Busy at once, and its first prompt part still revives a
retired pane and resets the turn caches. The TUI and `serve` still post it,
and OpenCode 2's `run` loads no plugin.
The field is set when OpenCode's loader calls setup(), which only OpenCode 2
does; the plugin never reads OpenCode's version. Say so at the field. Comment
only; the generated plugin is unchanged.
The new renderer test passed its fixtures with `as never`; use the checked
fixture-tuple cast with a SAFETY note, like the sibling pty-connection tests.
…ned off

The run producer ignored the status-hooks switch, so a user who turned
status off globally or for OpenCode (#23667) still got a run's Working and
Done. It now checks isAgentStatusHooksEnabledForAgent for the run's agent
(opencode or opencode2), the same predicate the plugin install honours,
before reading argv or posting anything.
…cess

The mock store types agentStatusByPaneKey values as unknown, so reading
`.state` failed tc:web. Assert the row with toMatchObject instead.
…e `opencode run`

Two costs the run producer paid on every command in every local pane:

- The retry ladder re-read the foreground (a process-table capture on the
  local provider) for any non-shell program still running, including other
  agents, editors and dev servers, up to three times. It now re-reads only
  while the foreground is still the shell (the command has not exec'd yet)
  or an unrecognised launcher that may still exec OpenCode (node, bun, bunx,
  npx, npm, pnpm, pnpx, yarn). Any other program is read once.
- With OpenCode status turned off for both opencode and opencode2, it still
  set the timer and read the foreground only to discard the result. It now
  returns before any timer or read. The per-agent check after the name read
  stays for the case where only one of them is off.
@brennanb2025
brennanb2025 force-pushed the brennanb2025/opencode2-same-folder-panes branch from 4203db5 to 5bb3190 Compare September 30, 2026 02:31
A confirmed agent process exit records a pane-wide completion identity that
names only the agent. Hook Dones are matched against it by agent alone, and
only a working title cleared it, so in a pane whose agent paints no working
title (OpenCode) every later hook-reported Done was treated as already
notified: no notification and no unread mark. An unreported exit (a status-off
`opencode run`, or quitting an idle client) was enough to set it.

A fresh hook Working now clears a process-exit identity, since a new turn
cannot be a duplicate of an earlier exit. Hook identities stay, so
same-turn and replay dedupe is unchanged.
The pane tracker's import of the two shared foreground-read delays took
four lines where its old local constants took three, which put the file
one line over max-lines once merged with main. Export the settle delay
and retry ladder as one object so each consumer imports a single name.
No behaviour change: same 350 ms settle and 1200/6000 ms retries.
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review status at 7b2d32624cc: ready for review

The problem. With more than one OpenCode 2 pane open, a turn in one pane showed its spinner and its Done on whichever pane had started OpenCode's shared background service. When turns overlapped, the pane that finished first never got its Done, and a pane in a second worktree could stay on Working forever. This happened because Orca's status reporter lived inside that one shared service, whose environment names only the pane that started it.

What changes for the user. Each OpenCode 2 pane now shows its own Working, Needs input and Done, and gets its own Done notification, however many panes share a folder or overlap in time. opencode run shows Working and Done on the pane that runs it. opencode mini shows nothing, where main showed it on the wrong pane.

Fixed in this round

  • Live QA round 3 found that after an opencode run in a pane with OpenCode status turned off (or after quitting an idle OpenCode), that pane's later Dones still showed Done but never notified and never marked the tab unread. Cause: when an agent's process exited, the pane recorded a completion that named only the agent, and every later Done from the same agent in that pane matched it and was dropped as already notified. Only a "working" window title cleared it, and OpenCode never sets one. That record predates this PR (the code is unchanged from main); this PR made it reachable for OpenCode 2. A new turn now ends that record (565feb79c84, with a test that fails without it).
  • CI static analysis: merging main put a renderer file one line over the line limit; the shared foreground-read timing is now exported as one value (7b2d32624cc, no behaviour change). The other static-analysis error came from main and cleared when main was merged.

Deferred (each labelled in the PR body)

  • opencode mini stays silent (TEMPORARY). OpenCode 2.0.18's mini gives Orca no per-turn signal; the fix needs a new OpenCode API. Asking OpenCode for it waits on a maintainer go-ahead; nothing has been filed.
  • An opencode run that starts while its tab is not the visible tab shows no status (TEMPORARY). This happens for commands sent from scripts, or typed ahead while an earlier command is still running followed by a tab switch. Fixing it needs a new "command started" event from the background terminal process.
  • opencode run over SSH or WSL shows no status (TEMPORARY).
  • Not caused by this PR (same on main for every agent): an agent's process exit still notifies once when its status is turned off; quitting an idle OpenCode client notifies once; quitting any agent in a tab hidden for over 5 minutes leaves its last Done row until the next command in that pane.

Verified

  • Live QA on a separate Mac with real OpenCode 2.0.18 and OpenCode 1.18.33 in real Electron builds. Round 3 compared main with this PR. Round 4 reran the notification fix: every Done notified exactly once, with no duplicates. Round 5 reran the key scenarios after merging main (fix(sidebar): an agent's row stays while it runs, whatever its tab title #23948 touches the same exit code), again one notification per Done.
  • Unit tests: 44 targeted files (1,805 tests) at this head. tc:node and tc:web pass. CI is green: typecheck, static analysis, all five test shards and both package builds.
  • A narrow code review of the fix found no P0 to P2 issues. The closing readiness check found no P0 or P1, and its two body corrections are applied.

After a status-off opencode run and two later turns, pane B's tab and worktree row carry the unread bell (round 4)
Overlap: pane B already Done while pane A still spins (round 4)
Merged head: pane R's tab has the unread bell after an opencode run exited (round 5)

Not verified: Windows, Linux, SSH and WSL live; OpenCode 1 serve + attach landing on the attach pane (QA's setup showed it on the serve pane on main and this PR alike); system notifications as displayed (the unsigned build can't show them, so they were counted from the app log).

@brennanb2025
brennanb2025 marked this pull request as ready for review September 30, 2026 08:48
@coderabbitai

coderabbitai Bot commented Sep 30, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Next included review available in 16 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 10 included reviews currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: f52b4dcd-3110-420f-b630-359acffe085b

📥 Commits

Reviewing files that changed from the base of the PR and between 7afa4ee and 7b2d326.

📒 Files selected for processing (59)
  • src/main/agent-hooks/server-opencode-binder.test.ts
  • src/main/agent-hooks/server-process-lifetime-status.test.ts
  • src/main/agent-hooks/server/server-ingest-normalization.ts
  • src/main/agent-hooks/server/server-ingest-terminal.ts
  • src/main/agent-hooks/server/server-opencode-binder.ts
  • src/main/agent-hooks/server/server-status-disposition.ts
  • src/main/opencode/hook-plugin-opencode2-setup.test.ts
  • src/main/opencode/hook-plugin-opencode2-tui-ownership.test.ts
  • src/main/opencode/hook-plugin-run-process-session-start.test.ts
  • src/main/opencode/hook-service-startup-refresh.test.ts
  • src/main/opencode/hook-service.test.ts
  • src/main/opencode/hook-service.ts
  • src/main/opencode/opencode-session-binder.ts
  • src/main/opencode/opencode-status-plugin-startup-refresh.test.ts
  • src/main/opencode/opencode-status-plugin-startup-refresh.ts
  • src/main/opencode/status-plugin-factory-source.ts
  • src/main/opencode/status-plugin-module-source.ts
  • src/main/opencode/status-plugin-post-source.ts
  • src/main/opencode/status-plugin-runtime-state-source.ts
  • src/main/opencode2/status-plugin-setup-source.ts
  • src/main/opencode2/status-plugin-tui-source.ts
  • src/main/providers/agent-foreground-command-line.test.ts
  • src/main/providers/agent-foreground-process.ts
  • src/main/providers/windows-agent-foreground-command-line.test.ts
  • src/main/providers/windows-agent-foreground-command-line.ts
  • src/main/providers/windows-agent-foreground-process.ts
  • src/main/runtime/local-pty-foreground-command-line.ts
  • src/main/runtime/opencode-run-lifetime-runtime.test.ts
  • src/main/runtime/opencode-run-lifetime-status.test.ts
  • src/main/runtime/opencode-run-lifetime-status.ts
  • src/main/runtime/orca-runtime-apply-tracked-pty-title.ts
  • src/main/runtime/orca-runtime-create-terminal-side-effect-command-code-detector.ts
  • src/main/runtime/orca-runtime-emit-daemon-pty-transient-fact.ts
  • src/main/runtime/orca-runtime-get-unpersisted-tracked-title-for-pty.ts
  • src/main/runtime/orca-runtime-schedule-wait-blocked-check.ts
  • src/main/runtime/orca-runtime-stop-requested-pty-ids.ts
  • src/main/runtime/runtime-terminal-contracts.ts
  • src/main/runtime/runtime-terminal-state-records.ts
  • src/main/startup/main-process-ready-runtime.ts
  • src/relay/opencode-canonical-config.ts
  • src/relay/opencode-hook-selection.test.ts
  • src/relay/plugin-overlay.test.ts
  • src/relay/plugin-overlay.ts
  • src/relay/relay-agent-hook-runtime.ts
  • src/renderer/src/components/terminal-pane/agent-completion-coordinator-process-exit-turn-boundary.test.ts
  • src/renderer/src/components/terminal-pane/agent-completion-coordinator.ts
  • src/renderer/src/components/terminal-pane/agent-completion-hook-observer.ts
  • src/renderer/src/components/terminal-pane/agent-completion-identity-store.ts
  • src/renderer/src/components/terminal-pane/pane-foreground-agent-tracker.ts
  • src/renderer/src/components/terminal-pane/pty-connection-command-finished-late-done.test.ts
  • src/shared/agent-hook-listener-opencode-reattribution.test.ts
  • src/shared/agent-hook-listener.ts
  • src/shared/agent-hook-listener/opencode-session-registry.ts
  • src/shared/foreground-command-settle.ts
  • src/shared/opencode-headless-command.test.ts
  • src/shared/opencode-headless-command.ts
  • src/shared/opencode-tui-plugin-install.ts
  • src/shared/terminal-output-side-effects.test.ts
  • src/shared/terminal-output-side-effects.ts

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.

@greptile-apps

greptile-apps Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

[High risk] Refactors OpenCode status reporting across multiple runtime systems.

The PR should not merge until fast OpenCode 2 turns can be reported even when they finish before the TUI navigates to their session.

Findings

  1. P1 Fast turns lose status ▶

Summary

The PR moves OpenCode 2 full-screen status reporting into each pane’s TUI, adds pane-process reporting for opencode run, limits session binding to OpenCode 1, and refreshes installed plugins after upgrades.

  • Per-pane reporting addresses status being attributed to the shared service’s starter pane.
  • A fast turn can still be missed if it finishes before its TUI displays the session.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  S[OpenCode session data] --> T[Pane TUI reporter]
  T --> H[Hook status store]
  C[Pane command markers and foreground process] --> R[opencode run producer]
  R --> H
  H --> U[Pane status and notifications]
Loading

Reviews (1) · Last reviewed commit: "refactor(opencode): share the foreground..."

function derive() {
const route = currentRoute();
let owned = [...memory.owned];
if (route && !owned.includes(route) && running(route)) owned = own(route, owned);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Fast turns lose status If an OpenCode 2 turn starts and finishes before the TUI navigates to its session, this check never gives the pane ownership because the session is no longer running. The pane therefore reports neither Working nor Done for that turn.

@pullfrog pullfrog 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.

✅ No new issues found.

Reviewed changes

  • Per-pane OpenCode 2 TUI reporter (src/main/opencode2/status-plugin-tui-source.ts, generated into the same module as the server plugin and installed as plugins/<id>-tui/tui.js) derives each pane's level from the TUI's own session data every 100 ms and on every bus event, keeps ownership/last level in OpenCode's per-TUI memory across reloads, and posts only on change through the existing delivery code.
  • Shared server stand-down — setupOpenCode2Status returns a no-op in any serve process when the TUI copy sits beside it, so an installer that predates the TUI copy keeps reporting.
  • opencode run process-lifetime producer (src/main/runtime/opencode-run-lifetime-status.ts) reads the pane's foreground argv at command start, posts Working for a real run, and publishes Done (Interrupted on 130) after the chunk's side-effect facts; SSH/WSL panes are skipped.
  • OpenCode 1-only binder — opencode-session-binder.ts now reads only the session table, and OpenCode 2 posts declare opencodeMajor: 2 so the reattribution path leaves them on the pane they name.
  • Startup/relay plugin refresh — existing installs are rewritten when bytes differ (TUI copy first), so a running service reloads the upgrade without a pane spawn.
  • Notification latch fix — a reported Working clears a process-exit completion record so later hook Dones in the pane still notify.

Load-bearing contract verified during review: reportingOpenCodeMajor = 2 is set only in setupOpenCode2Status, and the OpenCode 1.x line invokes only a plugin's server() entry, never setup() (upstream readV1Plugin at v1.18.33 only probes id/server/tui), so OpenCode 1 posts remain untagged and the binder is not silently disabled.

Pullfrog  | Fix it ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@brennanb2025
brennanb2025 merged commit 3727100 into main Sep 30, 2026
76 checks passed
OmarHung pushed a commit to OmarHung/orca that referenced this pull request Oct 3, 2026
…blyai#23722)

* fix(opencode): report OpenCode 2 status from each pane's TUI

OpenCode 2 serves every pane from one shared server whose env names only
the pane that started it, so every same-folder pane's work showed on that
pane, and the plugin's single aggregate swallowed the Idle of a pane whose
turn ended while another pane was busy.

Install the status plugin a second time as an OpenCode 2 TUI plugin
(plugins/<name>-tui/tui.js, written only when its bytes differ, locally,
in overlays and in the SSH/WSL relay installs). In a TUI process setup()
runs the same engine, fed through the same event translation as the
server, but only for root sessions this pane owns: the route's session
from when it starts (or when the route reaches it while running, hydrated
from the TUI's session status) until it settles, is deleted, or the TUI
exits. The server plugin stands down in any serve process when the TUI
copy is installed beside it; a relay that predates the TUI copy keeps the
old behavior. OpenCode 1 loads no plugin directories and is unchanged.

Delete the session-to-pane binder, registry, client sweep and ingest
reattribution: with every post stamped by its own pane nothing is left
for them to correct.

Known gap: OpenCode 2 `opencode run` in a pane has no TUI, so it reports
no pane status.

* fix(opencode): settle missed OpenCode 2 turn ends and order TUI installs

- The TUI copy now settles an owned root whose run the TUI's session data
  reports ended, with no pending permission or form, when the engine still
  holds it busy. An execution end missed across a service restart or
  reconnect no longer leaves the pane Working until the TUI exits.
- The TUI copy stays idle without ORCA_PANE_KEY. post() cannot report without
  it, so OpenCode 2 TUIs outside Orca no longer run the route poll and engine.
- Installers write the TUI copy before the server plugin file. The server
  decides at load whether to stand down, so a reload between the two writes
  now finds the TUI copy.

* fix(opencode): reconcile OpenCode 2 TUI status only once queued events drain

The TUI's session data applies each event before this plugin's queued handling
reaches it, so settling against it mid-backlog published a false Done before a
fast turn's later steps (Working, Done, Working, Done). Reconcile only when no
event is queued; a mismatch then is a start or end missed across a reconnect,
and both directions are now re-derived (a missed start left the pane on Done).

Also install the TUI copy beside the server plugin in the retired shared hooks
dir, so a TUI or service still loading that dir reports per pane instead of
leaving the service reporting under its starter pane.

* fix(opencode): keep OpenCode 2 TUI panes silent on plugin dispose

A TUI plugin hot reload disposes the plugin while the pane's turn keeps
running. The server path already passes sessionsOutliveDispose so dispose
publishes nothing; the TUI adapter now does the same, or every TUI reload
would still show a false Done. Also refreshes the generated-bytes digest
after rebasing onto the write-if-changed installers.

* fix(opencode): refresh installed OpenCode status plugins at app start

After an Orca upgrade, an OpenCode 2 service that was already running kept
the previous plugin, and with it the old wrong-pane status, until any new
terminal pane rewrote the file. OpenCode 2 reloads a plugin whose file
changes, so Orca now refreshes its existing installs once after the first
window shows: the global config dir, source overlays and the retired shared
dir, TUI copy first. It reuses the per-pane writers, which skip unchanged
files, never creates an install the user did not have, and honours the
status-hook and per-agent switches. An SSH relay does the same for its
canonical install when Orca connects and ships the plugin sources.

The plugin source assembly moves to its own module (re-exported unchanged)
to keep hook-service.ts under the line limit.

* fix(opencode): derive each OpenCode 2 pane's status from its TUI session data

The TUI copy of the status plugin translated OpenCode events into the
server-side engine and then patched the engine's latches back toward the
TUI's own session data: a settle on every tick, a queued-event counter, a
re-assert, synthetic Busy and Idle. Each review found another place where
the two copies disagreed: a missed end left the pane Working, a backlog of
slow posts flickered Done, a missed start showed Done mid-turn, a turn held
only by a subagent never settled, and a request answered while disconnected
pinned Needs input.

The TUI reporter now reads the pane's level straight from the session data
OpenCode keeps current (and re-hydrates on reconnect) on every event and on
a 100 ms tick: for each root session this pane owns, Needs input (an open
permission, else form, anywhere in its family while it runs) outranks
Working (any family member running) outranks Done. It posts one status per
level change through the plugin's existing delivery functions (retry,
dedupe, message-part throttle, ordering), so the wire and Orca's ingest are
unchanged. Ownership is kept in OpenCode's storage.memory, which survives a
plugin hot reload, so a turn that ends during a reload still shows Done;
dispose publishes nothing, and a level the old generation could not deliver
is re-posted by the next. On reconnect it re-syncs blockers for the owned
sessions OpenCode would not re-sync itself, ignoring requests already
answered. Events are handled synchronously, so a slow post can no longer
hold up event processing. The server path, OpenCode 1 and mimo are
unchanged.

* fix(opencode): keep an OpenCode 2 pane on Needs input while its root streams text

Orca treats every OpenCode MessagePart as Working. While a background
subagent waits on a permission or form, the root session can keep streaming
reply text, and the TUI reporter forwarded that text as a MessagePart. The
pane then flipped from Needs input to Working, and nothing restored it until
the level changed, so the user could miss the open request.

Skip reply text while the pane's level is Needs input, as the reporter
already does for queued prompts.

* fix(opencode): leave OpenCode 2 step events to the server path's own change

The shared-server translation re-derived Working from session.step.started.
The pane reporter no longer uses it, so it only changed the server path and
duplicated a separate open change. Drop it; server behaviour matches main.

* fix(opencode): reset an OpenCode 2 pane to idle when its TUI starts

Before this change, a pane could keep a status an earlier process left on
it. The case that matters is an upgrade mid-turn: the old shared-service
plugin posted pane B's turn under pane A's key, then stood down, and pane
A's TUI loaded the new reporter owning nothing, so it never posted and A
showed a wrong Working until its own next turn.

A freshly started reporter that owns no running turn now posts the host's
existing session-start boundary once, which the host shows as connected
idle: no completion, no notification, and an unseen Done it lands on stays
unread. A plugin hot reload keeps its memory and skips it, and where
OpenCode keeps no plugin memory it is never sent.

* fix(opencode): keep OpenCode 1 serve + attach sessions on the attaching pane

OpenCode 1 `opencode serve` in one pane plus `opencode attach` in others
reports every session from the serve process, whose environment names only
the serve pane. Before this branch the session-to-pane binder moved those
posts to the attaching pane; deleting it for OpenCode 2 put them back on the
serve pane.

Restore the binder, registry, correlation and client sweep for OpenCode 1
(and mimo-code, which it also served), gated so OpenCode 2 never uses it:

- The status plugin now sends `opencodeMajor: 2` on every post from a process
  whose loader called setup(), which only OpenCode 2 does. The host skips the
  binder (no rewrite, no kicked round) for any post that carries it. OpenCode 1
  and older plugins send nothing and keep the previous behaviour.
- The binder reads only OpenCode 1's `session` table. OpenCode 2 writes
  `session_v2`, so its sessions never bind; on a database both versions wrote,
  OpenCode 1 sessions are no longer hidden behind the v2 table.

OpenCode-1-only; it goes when OpenCode 1 support is removed.

* fix(opencode): show OpenCode 2 `opencode run` as Working, then Done, on its own pane

OpenCode 2's `opencode run` loads no plugin, and the shared service that
runs its session cannot tell which pane it belongs to, so a pane running it
showed nothing.

The runtime now reports it from the pane's own process lifetime. On the
pane's OSC 133 command start (main already parses 133 for every local PTY;
the start callback was never wired), after the pane tracker's 350 ms settle
it reads the foreground process name through the existing foreground
reader, and only when that name is OpenCode, the foreground command line
from one fresh shell-foreground process-table capture. An `opencode run`
posts Working through the existing terminal-status path into the hook
server's store; the pane's 133;D (or the daemon's background fact) posts
Done, marked interrupted on Ctrl-C. No polling.

Exclusive with plugin reporting: each write carries the command's start
time, and the store drops it once a hook has written the pane since then,
so an OpenCode 1 `run` (in-process plugin) or a server plugin without the
TUI copy owns its command alone and there is never a second Done.

Local macOS and Linux panes only; SSH, WSL and Windows panes stay silent
because their foreground cannot be read on this host.

* fixup! fix(opencode): show OpenCode 2 `opencode run` as Working, then Done, on its own pane

Type the selected descendants as process-table rows; ReturnType of the generic collector widened them to bare identity rows.

* fix(opencode): keep an `opencode run` pane's Done after the command exits

The run's Done was published inside the chunk that carried its OSC 133;D,
before the chunk's command-finished fact. The renderer drops an exited
agent's row on command-finished when the row has not changed since that
fact arrived, so it took the Done as the stale row and dropped it, in the
renderer and in main.

Publish the run's Done after the chunk's side-effect facts are emitted (and
after the daemon's background fact). The renderer then sees command-finished
while the row is still Working, and the Done that follows counts as a change,
so it stays, the same way a hook Done that lands after exit already does.

* fix(opencode): let an `opencode run` revive a pane Orca retired

When an agent Orca launched exits, command completion retires the pane, and
only a hook new-turn event revived it. A later `opencode run` in that pane
posts no hook, so its process-lifetime Working was refused and the pane
stayed silent.

A process-lifetime Working is posted only after a fresh OSC 133;C and the
pane's own foreground argv prove a new OpenCode run, which is at least as
strong as a hook new turn. The store now revives the retired pane on it the
same way: it clears the retirement, drops the launch-token fence, and rebinds
the observation. OSC status and a lone process Done still cannot write a
retired pane, and a closed tab stays closed.

* fix(opencode): report `opencode run` on local Windows panes too

The run producer skipped every Windows pane, although Orca already resolves
a Windows pane's foreground agent from the native process table. On main an
OpenCode 2 `run` showed there (on the service's pane); on this branch it was
silent.

The argv read now has a Windows branch: one fresh native table read
(windows-process-table, no interpreter spawn), the same foreground identity
the Windows resolver already computes, and the command line of the process
that identity names. It runs only after the name read says OpenCode. Local
Windows panes whose shell prints OSC 133 C/D (PowerShell with PSReadLine,
Git Bash with Orca's wrapper) now go Working, then Done; cmd.exe prints no
markers and stays silent. SSH and WSL panes stay silent.

* fix(opencode): re-read an `opencode run` foreground on the pane tracker's ladder

The producer read the pane's foreground once, 350 ms after the command
started. A wrapper, a shim or `sleep 1; opencode run` execs OpenCode later,
so those runs stayed silent. The renderer's pane tracker already re-reads a
command's foreground at 350, then 1200, then 6000 ms for exactly this.

Move those delays to one shared module and use it from both. The producer
re-reads only while the foreground is a non-shell process that is not
OpenCode, and at most on those three rungs; no new polling.

* fix(opencode): end an armed `opencode run` when the next command starts

A new OSC 133;C in a pane whose `opencode run` was still armed dropped the
armed state without a Done, so a run whose 133;D never arrived left the pane
on Working. A new command start proves the previous command ended, so it now
posts that run's Done (not marked interrupted: no exit code is known) before
the new command is inspected.

* fix(opencode): skip the session-start row inside an OpenCode 1 `run` process

OpenCode 1 `run` loads the status plugin in its own process, and the plugin
posts SessionStart when the run's session is created. The host lands that as
an idle session boundary, so a pane the run producer had already shown as
Working blinked idle before the plugin's Busy.

The plugin now skips SessionStart when its own process is a `run`, read from
its argv the same way isOpenCodeRunCommand reads a pane's foreground (first
positional after global options, past the compiled binary's entry path). A
`run` session goes Busy at once, and its first prompt part still revives a
retired pane and resets the turn caches. The TUI and `serve` still post it,
and OpenCode 2's `run` loads no plugin.

* chore(opencode): say where the plugin's opencodeMajor comes from

The field is set when OpenCode's loader calls setup(), which only OpenCode 2
does; the plugin never reads OpenCode's version. Say so at the field. Comment
only; the generated plugin is unchanged.

* test(opencode): type the late-Done pane fixture without bare casts

The new renderer test passed its fixtures with `as never`; use the checked
fixture-tuple cast with a SAFETY note, like the sibling pty-connection tests.

* fix(opencode): keep `opencode run` silent when OpenCode status is turned off

The run producer ignored the status-hooks switch, so a user who turned
status off globally or for OpenCode (stablyai#23667) still got a run's Working and
Done. It now checks isAgentStatusHooksEnabledForAgent for the run's agent
(opencode or opencode2), the same predicate the plugin install honours,
before reading argv or posting anything.

* test(opencode): read the late-Done row without an untyped property access

The mock store types agentStatusByPaneKey values as unknown, so reading
`.state` failed tc:web. Assert the row with toMatchObject instead.

* perf(opencode): read a command's foreground only while it could become `opencode run`

Two costs the run producer paid on every command in every local pane:

- The retry ladder re-read the foreground (a process-table capture on the
  local provider) for any non-shell program still running, including other
  agents, editors and dev servers, up to three times. It now re-reads only
  while the foreground is still the shell (the command has not exec'd yet)
  or an unrecognised launcher that may still exec OpenCode (node, bun, bunx,
  npx, npm, pnpm, pnpx, yarn). Any other program is read once.
- With OpenCode status turned off for both opencode and opencode2, it still
  set the timer and read the foreground only to discard the result. It now
  returns before any timer or read. The per-agent check after the name read
  stays for the case where only one of them is off.

* fix(opencode): let a new hook turn end a pane's process-exit completion

A confirmed agent process exit records a pane-wide completion identity that
names only the agent. Hook Dones are matched against it by agent alone, and
only a working title cleared it, so in a pane whose agent paints no working
title (OpenCode) every later hook-reported Done was treated as already
notified: no notification and no unread mark. An unreported exit (a status-off
`opencode run`, or quitting an idle client) was enough to set it.

A fresh hook Working now clears a process-exit identity, since a new turn
cannot be a duplicate of an earlier exit. Hook identities stay, so
same-turn and replay dedupe is unchanged.

* refactor(opencode): share the foreground read schedule as one value

The pane tracker's import of the two shared foreground-read delays took
four lines where its old local constants took three, which put the file
one line over max-lines once merged with main. Export the settle delay
and retry ladder as one object so each consumer imports a single name.
No behaviour change: same 350 ms settle and 1200/6000 ms retries.

(cherry picked from commit 3727100)
diegoesolorzano added a commit to diegoesolorzano/orca that referenced this pull request Oct 11, 2026
Integrate ~2007 upstream commits, mostly OpenCode v2 integration maturity
(status reporting per pane stablyai#23722/stablyai#24962, hook install in terminal config
dir stablyai#25296, stop stale plugin from retired shared hooks dir stablyai#23500) plus
@orca/process-host extraction, structured chat over ACP, and many agents.

Fork conflicts resolved (6): minimax/zai catalog entries relocated to the
new split agent-catalog-entries-secondary.ts; redraw shortcut moved to the
new matches() helper; createLocalNestedRepoScanFilesystem updated to
isGitRepoAsync; nestedReposPromise kept after upstream made timing a param;
showNestedReposToast import kept, dead settingsForRepoOwner dropped.
Added minimax/zai to the new Record<TuiAgent> maps (rest-signal, readiness
lane) and switched the Activity wrapper-grouping types to TerminalAgent.
Accepted upstream's deletion of the global-fetch audit test (stablyai#25791).

typecheck clean; git-crypt, time-tracker, activity and shortcut-policy
suites green (2305 tests).
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