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:
@@ -105,16 +105,37 @@ chatRouter.get('/sessions/:id', async (ctx) => {
|
||||
// happens to be browsing — which is how the list ended up showing raw ids for anything running elsewhere.
|
||||
chatRouter.get('/live', async (ctx) => {
|
||||
const email = ctx.get('user').email;
|
||||
const live = await sidecar.listLiveClaudeSessions();
|
||||
// Both harnesses, asked in parallel. Either failing contributes nothing rather than failing the panel:
|
||||
// both registry calls swallow their errors and return [].
|
||||
const [live, liveOpenCode] = await Promise.all([
|
||||
sidecar.listLiveClaudeSessions(),
|
||||
sidecar.listLiveOpenCodeSessions(),
|
||||
]);
|
||||
const sessions = live.map((session) => {
|
||||
// Resolve by Claude's id, never by the session key — the key is officer's handle and the transcript
|
||||
// is named after Claude's. Null until the first turn reports one, which is a conversation that has
|
||||
// genuinely not been written yet.
|
||||
const transcriptId = session.claudeSessionId;
|
||||
const resolved = transcriptId && !isOpenCodeSessionId(transcriptId) ? liveSessionTitle(email, transcriptId) : null;
|
||||
return { ...session, title: resolved?.title ?? null, cwd: resolved?.cwd ?? null };
|
||||
return { ...session, harness: 'claude' as const, title: resolved?.title ?? null, cwd: resolved?.cwd ?? null };
|
||||
});
|
||||
return ctx.json({ sessions });
|
||||
|
||||
// OpenCode rows carry less, and the shape says so rather than faking parity. `isGenerating` is always
|
||||
// true because a subprocess exists only while it generates; `pendingTasks` is 0 because `opencode run`
|
||||
// has no background-task concept. Title and cwd come from the session store, which is keyed on the
|
||||
// `ses_…` id the runner reports — not on our sessionKey — so a turn whose id has not been reported yet
|
||||
// shows unnamed rather than guessing.
|
||||
const openCodeSessions = liveOpenCode.map((session) => ({
|
||||
sessionKey: session.sessionKey,
|
||||
claudeSessionId: null,
|
||||
isGenerating: true,
|
||||
pendingTasks: 0,
|
||||
harness: 'opencode' as const,
|
||||
title: null,
|
||||
cwd: null,
|
||||
}));
|
||||
|
||||
return ctx.json({ sessions: [...sessions, ...openCodeSessions] });
|
||||
});
|
||||
|
||||
// DELETE /chat/sessions/:id[?cwd=] — remove a conversation from the owning harness's store. For a
|
||||
|
||||
Reference in New Issue
Block a user