name the live opencode rows
They were permanently unnamed, and the two halves needed to name them already existed on officer: the sidecar reports its own sessionKey because that is all it has, while the ses_ id arrives separately over opencode:session and is recorded in opencode/state.ts. Nothing joined them. /chat/live joins them now, so no protocol or sidecar change — widening LiveOpenCodeSession would have meant sending the sidecar a fact it told officer in the first place. One list call names every row rather than one transcript load each, and it is skipped when nothing is running or no id has been reported, so an idle Live panel never touches the serve. Verified against a real turn, which also showed the design working as intended: the first poll has no id yet and shows nothing, the next shows title and cwd. That window is real and short, and showing nothing beats showing a key the user has never seen. Worth knowing: opencode titles its own sessions "New session - <ISO timestamp>", so the row is located but not meaningfully named. That is genuinely its title, not a bug here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
||||
renameOpenCodeSession,
|
||||
isOpenCodeSessionId,
|
||||
} from './opencode-sessions';
|
||||
import { getOpenCodeSession } from './opencode/state';
|
||||
import { listChatModels } from './list-models';
|
||||
import { logger } from './logger';
|
||||
import { readSttConfig } from '../server-settings/stt';
|
||||
@@ -121,24 +122,41 @@ chatRouter.get('/live', async (ctx) => {
|
||||
});
|
||||
|
||||
// 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.
|
||||
// true because a subprocess exists only while it generates, and `pendingTasks` is 0 because
|
||||
// `opencode run` has no background-task concept — a number there would imply one.
|
||||
//
|
||||
// Title and cwd are ALWAYS null here, not merely null-until-known: naming a session means reading
|
||||
// OpenCode's store, which is keyed on the `ses_…` id, and `LiveOpenCodeSession` does not carry one —
|
||||
// the runner reports it separately over `opencode:session` and nothing correlates the two. So a
|
||||
// running OpenCode turn shows up unnamed, every time. Giving it a title means widening that type
|
||||
// first; guessing from `sessionKey` would be inventing one.
|
||||
const openCodeSessions = liveOpenCode.map((session) => ({
|
||||
// Naming them needs a second hop. The sidecar reports only its own `sessionKey`, because that is all it
|
||||
// has; OpenCode's store is keyed on `ses_…`, which the runner reports separately over `opencode:session`
|
||||
// and officer records in `opencode/state.ts`. So the correlation lives HERE, and it is the reason these
|
||||
// rows used to be permanently unnamed — the two halves existed and nothing joined them.
|
||||
const withIds = liveOpenCode.map((session) => ({
|
||||
sessionKey: session.sessionKey,
|
||||
claudeSessionId: null,
|
||||
isGenerating: true,
|
||||
pendingTasks: 0,
|
||||
harness: 'opencode' as const,
|
||||
title: null,
|
||||
cwd: null,
|
||||
openCodeId: getOpenCodeSession(session.sessionKey) ?? null,
|
||||
}));
|
||||
|
||||
// One list call names every row, rather than one transcript load each — `loadOpenCodeSession` rebuilds
|
||||
// a whole conversation to read its title. Skipped entirely when nothing is running or nothing has
|
||||
// reported an id yet, so an idle Live panel never touches the serve. Failure degrades to unnamed:
|
||||
// `listOpenCodeSessions` already swallows and returns [], which matches how the rest of this route
|
||||
// fails.
|
||||
const named = withIds.some((r) => r.openCodeId) ? await listOpenCodeSessions() : [];
|
||||
const byId = new Map(named.map((s) => [s.id, s]));
|
||||
|
||||
const openCodeSessions = withIds.map(({ sessionKey, openCodeId }) => {
|
||||
const meta = openCodeId ? byId.get(openCodeId) : undefined;
|
||||
return {
|
||||
sessionKey,
|
||||
claudeSessionId: openCodeId,
|
||||
isGenerating: true,
|
||||
pendingTasks: 0,
|
||||
harness: 'opencode' as const,
|
||||
// Still null for the first seconds of a turn: the id is only known once the subprocess prints it.
|
||||
// That window is real and short, and showing nothing beats showing a key the user has never seen.
|
||||
title: meta?.title ?? null,
|
||||
cwd: meta?.cwd || null,
|
||||
};
|
||||
});
|
||||
|
||||
return ctx.json({ sessions: [...sessions, ...openCodeSessions] });
|
||||
});
|
||||
|
||||
|
||||
@@ -29,9 +29,11 @@ export type LiveClaudeSession = {
|
||||
* 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).
|
||||
*
|
||||
* It also carries no id for OpenCode's own `ses_…` session, which is why a live OpenCode row cannot be
|
||||
* named: the runner reports that id separately, over `opencode:session`, and nothing correlates the two
|
||||
* back here. Adding it is what a titled OpenCode row in the Live panel would need.
|
||||
* It deliberately carries no id for OpenCode's own `ses_…` session, and does not need to: the sidecar
|
||||
* only ever knows its own `sessionKey`, while the `ses_…` is reported separately over `opencode:session`
|
||||
* and recorded by officer in `api/chat/opencode/state.ts`. `/chat/live` joins the two on officer's side,
|
||||
* which is where both halves already are — widening this type would mean sending the sidecar a fact it
|
||||
* had told officer in the first place.
|
||||
*/
|
||||
export type LiveOpenCodeSession = {
|
||||
sessionKey: string;
|
||||
|
||||
Reference in New Issue
Block a user