Skip to content

Commit 6e810dc

Browse files
committed
fix: preserve opencode thinking status in live stream updates
Only update OpenCode status line when the parsed phase is a detailed Thinking message and pass provider context through runtime and harness renderers to keep replay behavior consistent. Also include CLI version in ode status output for easier diagnostics.
1 parent 2816067 commit 6e810dc

File tree

7 files changed

+111
-12
lines changed

7 files changed

+111
-12
lines changed

packages/agents/test/session-inspector.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,69 @@ describe("session inspector", () => {
117117
expect(state.phaseStatus).toBe("Thinking: Planning repo exploration strategy");
118118
});
119119

120+
it("only updates opencode phase when thinking details are present", () => {
121+
const now = Date.now();
122+
const state = buildSessionMessageState([
123+
{
124+
timestamp: now,
125+
type: "message.part.updated",
126+
data: {
127+
payload: {
128+
type: "message.part.updated",
129+
properties: {
130+
part: {
131+
id: "tool_1",
132+
sessionID: "ses_1",
133+
type: "tool",
134+
tool: "Read",
135+
state: {
136+
status: "running",
137+
input: { filePath: "/tmp/repo/README.md" },
138+
},
139+
},
140+
},
141+
},
142+
},
143+
},
144+
{
145+
timestamp: now + 1,
146+
type: "message.part.updated",
147+
data: {
148+
payload: {
149+
type: "message.part.updated",
150+
properties: {
151+
part: {
152+
id: "reasoning_1",
153+
sessionID: "ses_1",
154+
type: "reasoning",
155+
text: "Planning response sections",
156+
},
157+
},
158+
},
159+
},
160+
},
161+
{
162+
timestamp: now + 2,
163+
type: "message.part.updated",
164+
data: {
165+
payload: {
166+
type: "message.part.updated",
167+
properties: {
168+
part: {
169+
id: "text_1",
170+
sessionID: "ses_1",
171+
type: "text",
172+
text: "Draft message body",
173+
},
174+
},
175+
},
176+
},
177+
},
178+
], { provider: "opencode" });
179+
180+
expect(state.phaseStatus).toBe("Thinking: Planning response sections");
181+
});
182+
120183
it("renders non-empty preview details from wrapped events", () => {
121184
const startedAt = Date.now();
122185
const state = buildSessionMessageState([

packages/core/cli.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,7 @@ function showLogs(commandArgs: string[]): void {
340340
async function showStatus(): Promise<void> {
341341
const state = daemonState();
342342
const daemonIsRunning = managerRunning(state);
343+
console.log(`Version: ${CURRENT_VERSION}`);
343344
console.log(`Daemon: ${daemonIsRunning ? "running" : "stopped"}`);
344345
if (state.pendingUpgradeRestart) {
345346
console.log(

packages/core/runtime/event-stream.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export async function startEventStreamWatcher(
6969
const existingState = liveParsedState.get(messageKey);
7070
const parsedState = buildSessionMessageState(eventHistory, {
7171
workingDirectory: workingPath,
72+
provider: providerId,
7273
baseState: {
7374
startedAt: request.startedAt,
7475
sessionTitle: existingState?.sessionTitle,

packages/live-status-harness/renderer.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export function renderStatusesFromRun(meta: HarnessRunMeta, events: HarnessCaptu
3939
const state = buildSessionMessageState(sessionEvents, {
4040
endIndex: index,
4141
workingDirectory: meta.cwd,
42+
provider: meta.provider,
4243
baseState: { startedAt: meta.startedAt },
4344
});
4445
const text = buildStatusMessageByProvider(meta.provider, request, meta.cwd, state, "medium");

packages/live-status-harness/scripts/generate-report.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ async function runProvider(
258258
}),
259259
{
260260
workingDirectory: options.cwd,
261+
provider: meta.provider,
261262
baseState: { startedAt: meta.startedAt },
262263
}
263264
);

packages/utils/session-inspector.ts

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
type StreamStateMaps,
1515
type StreamToolState,
1616
} from "@/agents/session-state/shared";
17+
import type { AgentProviderId } from "@/shared/agent-provider";
1718

1819
export type SessionEvent = {
1920
timestamp: number;
@@ -64,6 +65,7 @@ export type SessionStateOptions = {
6465
workingDirectory?: string;
6566
endIndex?: number;
6667
baseState?: Partial<SessionMessageState>;
68+
provider?: AgentProviderId;
6769
};
6870

6971
type ProviderParser = {
@@ -305,7 +307,31 @@ function applyMessageUpdatedEvent(state: SessionMessageState, eventProps: Record
305307
applyMetadataFromRecord(state, info);
306308
}
307309

308-
function applySessionStatusEvent(state: SessionMessageState, eventProps: Record<string, unknown>): void {
310+
function isOpencodeThinkingStatusWithContent(status: string): boolean {
311+
if (!status.startsWith("Thinking:")) return false;
312+
return status.slice("Thinking:".length).trim().length > 0;
313+
}
314+
315+
function updatePhaseStatus(
316+
state: SessionMessageState,
317+
nextStatus: string | undefined,
318+
provider?: AgentProviderId
319+
): void {
320+
if (!nextStatus) return;
321+
if (provider === "opencode") {
322+
if (isOpencodeThinkingStatusWithContent(nextStatus)) {
323+
state.phaseStatus = nextStatus;
324+
}
325+
return;
326+
}
327+
state.phaseStatus = nextStatus;
328+
}
329+
330+
function applySessionStatusEvent(
331+
state: SessionMessageState,
332+
eventProps: Record<string, unknown>,
333+
provider?: AgentProviderId
334+
): void {
309335
const statusValue = (eventProps as { status?: unknown }).status;
310336
const formattedStatus = formatSessionStatus(statusValue);
311337
if (!formattedStatus) return;
@@ -317,7 +343,7 @@ function applySessionStatusEvent(state: SessionMessageState, eventProps: Record<
317343
) {
318344
return;
319345
}
320-
state.phaseStatus = formattedStatus;
346+
updatePhaseStatus(state, formattedStatus, provider);
321347
}
322348

323349
function normalizeReasoningStatus(text: string): string {
@@ -331,7 +357,11 @@ function normalizeReasoningStatus(text: string): string {
331357
return `Thinking: ${truncated}`;
332358
}
333359

334-
function applyMessagePartUpdatedEvent(state: SessionMessageState, eventProps: Record<string, unknown>): void {
360+
function applyMessagePartUpdatedEvent(
361+
state: SessionMessageState,
362+
eventProps: Record<string, unknown>,
363+
provider?: AgentProviderId
364+
): void {
335365
const part = (eventProps as { part?: Record<string, unknown> }).part;
336366
if (!part) return;
337367
const isSessionScopedPart = typeof part.sessionID === "string";
@@ -363,35 +393,35 @@ function applyMessagePartUpdatedEvent(state: SessionMessageState, eventProps: Re
363393
}
364394

365395
if (toolInfo.status === "running" || toolInfo.status === "pending") {
366-
state.phaseStatus = `Running tool: ${toolInfo.name}`;
396+
updatePhaseStatus(state, `Running tool: ${toolInfo.name}`, provider);
367397
} else if (toolInfo.status === "completed") {
368-
state.phaseStatus = `Finished tool: ${toolInfo.name}`;
398+
updatePhaseStatus(state, `Finished tool: ${toolInfo.name}`, provider);
369399
} else if (toolInfo.status === "error") {
370-
state.phaseStatus = `Tool failed: ${toolInfo.name}`;
400+
updatePhaseStatus(state, `Tool failed: ${toolInfo.name}`, provider);
371401
}
372402
return;
373403
}
374404

375405
if (part.type === "text" && typeof part.text === "string") {
376406
state.currentText = part.text;
377407
if (isSessionScopedPart) {
378-
state.phaseStatus = "Drafting response";
408+
updatePhaseStatus(state, "Drafting response", provider);
379409
}
380410
return;
381411
}
382412

383413
if (part.type === "reasoning" && typeof part.text === "string") {
384414
state.thinkingText = part.text;
385415
if (isSessionScopedPart) {
386-
state.phaseStatus = normalizeReasoningStatus(part.text);
416+
updatePhaseStatus(state, normalizeReasoningStatus(part.text), provider);
387417
}
388418
return;
389419
}
390420

391421
if (part.type === "thinking" && typeof part.text === "string") {
392422
state.thinkingText = part.text;
393423
if (isSessionScopedPart) {
394-
state.phaseStatus = normalizeReasoningStatus(part.text);
424+
updatePhaseStatus(state, normalizeReasoningStatus(part.text), provider);
395425
}
396426
}
397427
}
@@ -472,7 +502,7 @@ export function buildSessionMessageState(
472502
events: SessionEvent[],
473503
options: SessionStateOptions = {}
474504
): SessionMessageState {
475-
const { endIndex, baseState } = options;
505+
const { endIndex, baseState, provider } = options;
476506
const startTime = events[0]?.timestamp ?? Date.now();
477507
const state: SessionMessageState = {
478508
sessionTitle: baseState?.sessionTitle,
@@ -606,11 +636,11 @@ export function buildSessionMessageState(
606636
}
607637

608638
if (type === "session.status") {
609-
applySessionStatusEvent(state, eventProps);
639+
applySessionStatusEvent(state, eventProps, provider);
610640
}
611641

612642
if (type === "message.part.updated") {
613-
applyMessagePartUpdatedEvent(state, eventProps);
643+
applyMessagePartUpdatedEvent(state, eventProps, provider);
614644
}
615645

616646
if (type === "todo.updated") {

packages/web-ui/src/lib/session-inspector/IMPreview.svelte

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
...buildSessionMessageState(events, {
3333
endIndex: selectedEventIndex,
3434
workingDirectory,
35+
provider,
3536
}),
3637
currentStatus: "Starting",
3738
currentStep: undefined,
@@ -40,6 +41,7 @@
4041
const finalState = $derived(({
4142
...buildSessionMessageState(events, {
4243
workingDirectory,
44+
provider,
4345
}),
4446
currentStatus: "Completed",
4547
currentStep: undefined,

0 commit comments

Comments
 (0)