show running opencode turns in the live panel

The Live panel asked `claude:list` and nothing else, so a running OpenCode turn was invisible — the panel
claimed to show what the agent is doing and silently omitted half of it.

Adds `opencode:list` / `opencode:sessions` and merges both harnesses in `/chat/live`, asked in parallel,
each failing toward empty so one sidecar being down contributes nothing rather than breaking the panel.

The OpenCode row is deliberately thinner than the Claude one rather than faked into parity:

  isGenerating  always true — a subprocess exists only while it generates, so there is no "merely open"
  pendingTasks  always 0    — `opencode run` has no background-task concept; reporting a number would
                              suggest a capability that does not exist
  title / cwd   null        — the session store is keyed on the `ses_…` id the runner reports, not on
                              our sessionKey, so an unreported turn shows unnamed rather than guessed

This is the incremental option from docs/opencode-serve-path.md — enumeration without moving turns onto
the serve, so it buys the Live panel with no warm sessions, no SSE loop and no lifetime questions.

NOT verified end to end: no OpenCode turn was running to enumerate, so the verb is wired and typechecked
but has never returned a non-empty list. See COMMS/BLOCKERS.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-10 03:00:43 +00:00
co-authored by Claude Opus 5
parent 8b409e8af8
commit e8bd946272
5 changed files with 76 additions and 4 deletions
+5 -1
View File
@@ -5,7 +5,7 @@ import { DATA_PATH } from '../../data-path';
import { createSidecarConnector } from '../connect';
import { createSessionLogStore } from '../claude/session-log';
import type { SidecarCommand, SidecarEvent } from '../protocol';
import { runOpenCodeTurn, killOpenCodeTurn } from './runner';
import { runOpenCodeTurn, killOpenCodeTurn, listRunningOpenCodeTurns } from './runner';
// The OpenCode sidecar (officer-opencode). Same philosophy as officer-claude: a singleton process that
// OWNS its runtime — here, an `opencode serve` — registers with the API server, and answers commands. It
@@ -147,6 +147,10 @@ function handleCommand(cmd: SidecarCommand, reply: ReplyFn) {
reply({ type: 'opencode:spawned', id: cmd.id, sessionKey });
break;
}
case 'opencode:list':
reply({ type: 'opencode:sessions', id: cmd.id, sessions: listRunningOpenCodeTurns() });
break;
case 'opencode:kill':
killOpenCodeTurn(cmd.sessionKey);
sessionLog.drop(cmd.sessionKey);
+15
View File
@@ -200,6 +200,21 @@ export function runOpenCodeTurn(params: OpenCodeRunParams, config: RunnerConfig,
});
}
/**
* The turns this process is running right now.
*
* The OpenCode analog of `claude-manager.listSessions`, and deliberately thinner. Claude holds a warm
* session that outlives a turn, so it can report one that is merely open; OpenCode spawns a subprocess
* per turn and has nothing between them. So a session appears here only while it is generating — which
* is exactly the state the Live panel exists to show, and the state that was invisible for OpenCode.
*
* No `pendingTasks`: `opencode run` has no background-task concept, so reporting 0 would suggest a
* capability that does not exist rather than an empty one.
*/
export function listRunningOpenCodeTurns(): { sessionKey: string }[] {
return Array.from(running.keys()).map((sessionKey) => ({ sessionKey }));
}
export function killOpenCodeTurn(sessionKey: string): void {
const handle = running.get(sessionKey);
if (!handle) return;
+13
View File
@@ -11,6 +11,14 @@ export type SidecarMessage = SidecarCommand | SidecarEvent;
* together with `isGenerating` it is exactly what the agent's own idle GC consults before deciding a
* session may be collected, so a caller can tell "busy" from "merely open" the same way it does.
*/
/**
* An OpenCode turn in flight. Only ever the generating ones — see `listRunningOpenCodeTurns` for why
* this carries neither `isGenerating` (it is always true) nor `pendingTasks` (no such concept).
*/
export type LiveOpenCodeSession = {
sessionKey: string;
};
export type LiveClaudeSession = {
sessionKey: string;
/**
@@ -52,6 +60,10 @@ export type SidecarCommand =
// OpenCode — drive a turn via `opencode run … --format json` (tools re-anchored to cwd via --dir)
| { type: 'opencode:run-streaming'; id: string; params: OpenCodeRunParams }
| { type: 'opencode:kill'; id: string; sessionKey: string }
// Which OpenCode turns are running right now. The counterpart of `claude:list`, and thinner for a
// reason: OpenCode has no warm session between turns, so there is nothing to report but the running
// ones. See `listRunningOpenCodeTurns`.
| { type: 'opencode:list'; id: string }
// VNC
| { type: 'vnc:start'; id: string; params: VncStartParams }
// Provision the VNC password without starting a server — the UI needs it before it can connect
@@ -78,6 +90,7 @@ export type SidecarEvent =
| { type: 'claude:generating'; id: string; generating: boolean }
| { type: 'claude:session-key'; id: string; sessionKey: string | null }
| { type: 'claude:sessions'; id: string; sessions: LiveClaudeSession[] }
| { type: 'opencode:sessions'; id: string; sessions: LiveOpenCodeSession[] }
// VNC
| { type: 'vnc:started'; id: string; port: number; display: number }
| { type: 'vnc:password'; id: string; password: string }