Repository navigation
fix(opencode): report OpenCode 2 status from each pane's own TUI - #23722
Conversation
1a17546 to
4203db5
Compare
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, Head: What changed during review (four review loops plus two independent architecture reviews):
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. Open (blocks ready):
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 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.
4203db5 to
5bb3190
Compare
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.
…2-same-folder-panes
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.
Review status at
|
|
Warning Review limit reachedNext included review available in 16 minutes. View limit detailsLimit 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. Review configuration: ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (59)
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. Comment |
|
| function derive() { | ||
| const route = currentRoute(); | ||
| let owned = [...memory.owned]; | ||
| if (route && !owned.includes(route) && running(route)) owned = own(route, owned); |
There was a problem hiding this comment.
✅ 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 asplugins/<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 —
setupOpenCode2Statusreturns a no-op in anyserveprocess when the TUI copy sits beside it, so an installer that predates the TUI copy keeps reporting. opencode runprocess-lifetime producer (src/main/runtime/opencode-run-lifetime-status.ts) reads the pane's foreground argv at command start, posts Working for a realrun, 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.tsnow reads only thesessiontable, and OpenCode 2 posts declareopencodeMajor: 2so 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-exitcompletion 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.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
…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)
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).








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 runandopencode minihad 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 runshows Working and Done on the pane that runs it.opencode minishows nothing instead of lighting up the wrong pane.Scope.
opencodeapp (includingopencode --standalone) on local, SSH and WSL hosts, andopencode runon local macOS, Linux and Windows panes and on panes hosted by another machine's Orca server.runno 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).opencode minishows no status on any pane;opencode runover SSH or WSL shows no status; anopencode runthat 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 runandopencode minilit up the first pane too.After (user):
opencode runin 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 minishows nothing, on its own pane or any other.Mechanism 1: a reporter in each full-screen OpenCode 2 pane.
plugins/<name>-tui/tui.jsbeside 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.Mechanism 2: the shared server stands down. The OpenCode 2 server plugin now does nothing in any
serveprocess (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 runreported from the pane's own process. OpenCode 2'srunloads 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:node,npx,bunand similar) that may still start OpenCode.opencodeoropencode2does it read that process's command line, and only if the command isrundoes 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.runloads 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.run, the plugin no longer posts its idle "session start" row inside arunprocess, which removes a Working, idle, Working blink at start.Mechanism 4: the binder keeps covering OpenCode 1
serve+attach, and only that. An OpenCode 1opencode serveshared byopencode attachpanes 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 optionalopencodeMajor: 2field 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/opencodeinstall 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).opencode runwith 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.agent-completion-coordinator-process-exit-turn-boundary.test.tsdrives 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
run's process lifetime is its turn.opencode runanswers 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.opencode mini's painted screen. A real terminal capture ofminion 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.opencode runto Orca's list of one-shot agent commands. That list removes agent identity from the pane everywhere, which would also silence OpenCode 1run, whose own plugin reports it.serve+attachpanes then reported on theservepane.--standalone. It covers only agents Orca launches, not a user typingopencode, and gives up OpenCode's shared service.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.
opencode runstatus derived on the host from the pane's foreground process: INTENDED.runloads 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 reportsrunfrom inside the process, that report wins with no Orca change), and when a read fails the pane stays silent rather than guessing.serve+attachdoes not regress. It and theopencodeMajorpost field that gates it go when OpenCode 1 support is removed.opencode runover 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 runthat 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 sendingopencode runinto a background terminal, and by keyboard type-ahead: typingopencode 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 miniis silent: TEMPORARY. OpenCode 2.0.18'sminiemits 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 byminiloading TUI plugins. Asking OpenCode for it waits on a maintainer go-ahead.opencode mini --standaloneandopencode 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), somini --standalonegoes from correct on main to silent, andrun --standaloneshows only Working and Done, no Needs input. Removed by the same OpenCode change that fixesmini. Orca's own launcher runs the full-screen app, which is covered.opencode runcommand 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 staysnode); 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.opencode runlonger 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.runthat has to start the shared service may be silent (unverified): TEMPORARY if real. On Windows the service starts as a child of thatrun, so the pane's process tree may hold twoopencode.exeand the foreground read then picks neither. Later runs, with the service already up, are unaffected. Removed by picking therunprocess by its command line when several OpenCode processes match.runWorking and Done that the status store dropped for a hook (unverified effect): TEMPORARY. For an OpenCode 1run,orca terminal wait --for tui-idlecould briefly see "working" until the command exits. Not reproduced. Removed by recording only what the store accepted.opencode runDone 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.serve+attachrouted 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):
opencode miniandopencode attach, on both builds.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.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.Known limitations
runshows 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.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.
This PR, same steps: the bell is on the pane that ran the turn. The first pane shows only its idle check.
Main, overlapping turns in two worktrees: after the second pane finished, its row (wt2) is still spinning, and it stayed on Working.
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.
This PR, Needs input: OpenCode asks for permission and only that pane shows the orange Needs input icon, while another pane keeps working.
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.
Live QA, round 3:
opencode run,miniand OpenCode 1Setup: a separate Apple M4 Mac, real OpenCode 2.0.18 (
@opencode/cli) and OpenCode 1.18.33 (opencode-ai), both on the freelongcat-2.5-preview-freemodel, 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 at5bb3190ea04(before the review fix) and main atfb67d5d7c3f. 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 1servepane, A1 and B1 = OpenCode 1attachpanes, R = a pane for OpenCode 1run.opencode runin B, B's tab visible when it startsopencode runin B, B's tab hidden when it startsopencode runin B (twice)opencode runin B565feb79c84. With OpenCode and OpenCode 2 both off: nothing at allopencode miniturn in Cminiorattachclientmini)565feb79c84servein S,attachin A1 and B1, turn only in A1 (three variants)opencode runin hidden pane RMain,
opencode runin Terminal 2 (B, hidden): the spinner is on the first tab, the pane that started the service.This PR, the same command with Terminal 2 visible: the spinner is on Terminal 2's tab and in the worktree row.
This PR, 30 s after that run ended (with Terminal 2 hidden again): Terminal 2 still carries its Done.
This PR, Ctrl-C of a long
opencode run: the worktree row shows the red Interrupted dot.This PR, OpenCode status turned off: a running
opencode runshows no spinner anywhere.Main,
opencode miniworking in Terminal 3: the spinner is on the first tab.This PR, the same
miniturn: no tab changes.This PR, overlapping turns in one folder (the tab titles are OpenCode's own text): B is Done while A is still Working.
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 in565feb79c84).This PR (main looks the same): an OpenCode 1 turn sent from an
attachpane spins on theservepane.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 anopencode runwith 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 oneagent-task-completedispatch in the app log.opencode runin pane B, status back on, then two OpenCode 2 turns in Bopencode runin pane R, then an OpenCode 2 turn in RNot 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
7b2d32624ccthe status-off-run scenario and the status-onopencode runcontrol were rerun: every Done notified exactly once (status-off run then three turns in B;opencode runthen a turn in R), and the run's Done and its process exit did not both notify.Testing
Unit tests.
hook-plugin-opencode2-tui-ownership.test.tsruns 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.tsandopencode-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 1runprocess; still posted for the TUI andserve.serve+attachreports on the attaching pane, an OpenCode 2 session never binds, and an OpenCode 2 post is never moved.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.5bb3190ea04: neighbouring suites (1132 files, 12,689 tests) pass except two tests that timed out under machine load (src/relay/subprocess.test.tsandpty-connection-command-finished-cleanup.test.ts); both pass when run alone.tc:nodeandtc:webpass with no errors at5bb3190ea04;tc:webpasses at565feb79c84(the fix changes only renderer files). oxlint and the changed-code quality gate report 0 new findings on the changed files.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:nodeandtc:webpass; 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 freelongcat-2.5-previewmodel, Orca in a hidden window driven through the Chrome DevTools protocol, each app with its own isolated home and OpenCode service. Main armceffecf12a0e(includes #23700), PR arm1a1754618f4(before therunproducer 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.opencode --standaloneReview
Agent skill upstream boundary
docs/reference/agent-skill-sharing-upstream-boundary.mdand copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.Notes
runproducer reads the command line of the pane's own foreground process only when its name is OpenCode, uses it only to recogniserun, and neither stores nor sends it.opencodeMajor, which older hosts ignore; a plugin that predates it sends none and gets main's binder behaviour. Rows from therunproducer 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.join; the stand-down check reads the TUI file through a file URL, which works on Windows. Windowsrunreads 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.runover SSH is silent (see above). Per-pane relay overlays created with a customOPENCODE_CONFIG_DIRare not refreshed on connect; that gap predates this PR.runproducer 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.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)