Skip to content

Commit 6ae9752

Browse files
authored
Merge branch 'main' into codex/knowledge-synthesis-participant-roles
2 parents e20c5fb + 7b13a18 commit 6ae9752

23 files changed

Lines changed: 1839 additions & 166 deletions

interface/src/api/client.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,16 +195,31 @@ export interface ToolStartedEvent {
195195
channel_id: string | null;
196196
process_type: ProcessType;
197197
process_id: string;
198+
call_id: string;
198199
tool_name: string;
199200
args: string;
200201
}
201202

203+
export interface ToolOutputEvent {
204+
type: "tool_output";
205+
agent_id: string;
206+
channel_id: string | null;
207+
process_type: ProcessType;
208+
process_id: string;
209+
/** Stable identifier matching the tool_call that initiated this stream. */
210+
call_id: string;
211+
tool_name: string;
212+
line: string;
213+
stream: "stdout" | "stderr";
214+
}
215+
202216
export interface ToolCompletedEvent {
203217
type: "tool_completed";
204218
agent_id: string;
205219
channel_id: string | null;
206220
process_type: ProcessType;
207221
process_id: string;
222+
call_id: string;
208223
tool_name: string;
209224
result: string;
210225
}
@@ -267,6 +282,7 @@ export type ApiEvent =
267282
| BranchCompletedEvent
268283
| ToolStartedEvent
269284
| ToolCompletedEvent
285+
| ToolOutputEvent
270286
| OpenCodePartUpdatedEvent
271287
| WorkerTextEvent
272288
| CortexChatMessageEvent;

interface/src/components/ToolCall.tsx

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,24 @@
11
import {useState} from "react";
22
import {cx} from "class-variance-authority";
3-
import type {TranscriptStep, OpenCodePart} from "@/api/client";
3+
import type {OpenCodePart} from "@/api/client";
4+
import type { TranscriptStep as SchemaTranscriptStep } from "@/api/types";
5+
6+
// Extended TranscriptStep with live_output for streaming shell output
7+
type ToolResultStatus = "pending" | "final" | "waiting_for_input";
8+
9+
type ExtendedTranscriptStep = SchemaTranscriptStep & {
10+
live_output?: string;
11+
status?: ToolResultStatus;
12+
};
13+
14+
// Use the extended type for pairing
15+
type TranscriptStep = ExtendedTranscriptStep;
416

517
// ---------------------------------------------------------------------------
618
// Types
719
// ---------------------------------------------------------------------------
820

9-
export type ToolCallStatus = "running" | "completed" | "error";
21+
export type ToolCallStatus = "running" | "completed" | "error" | "waiting_for_input";
1022

1123
export interface ToolCallPair {
1224
/** The call_id linking tool_call to tool_result */
@@ -25,6 +37,8 @@ export interface ToolCallPair {
2537
status: ToolCallStatus;
2638
/** Human-readable summary provided by live opencode parts */
2739
title?: string | null;
40+
/** Live streaming output from tool_output SSE events (running tools only) */
41+
liveOutput?: string;
2842
}
2943

3044
// ---------------------------------------------------------------------------
@@ -42,12 +56,21 @@ export type TranscriptItem =
4256

4357
export function pairTranscriptSteps(steps: TranscriptStep[]): TranscriptItem[] {
4458
const items: TranscriptItem[] = [];
45-
const resultsById = new Map<string, {name: string; text: string}>();
59+
const resultsById = new Map<
60+
string,
61+
{name: string; text: string; status: ToolResultStatus; liveOutput?: string}
62+
>();
4663

4764
// First pass: index all tool_result steps by call_id
4865
for (const step of steps) {
4966
if (step.type === "tool_result") {
50-
resultsById.set(step.call_id, {name: step.name, text: step.text});
67+
const liveOutput = step.live_output;
68+
resultsById.set(step.call_id, {
69+
name: step.name,
70+
text: step.text,
71+
status: step.status ?? "final",
72+
liveOutput,
73+
});
5174
}
5275
}
5376

@@ -60,12 +83,21 @@ export function pairTranscriptSteps(steps: TranscriptStep[]): TranscriptItem[] {
6083
} else if (content.type === "tool_call") {
6184
const result = resultsById.get(content.id);
6285
const parsedArgs = tryParseJson(content.args);
63-
const parsedResult = result ? tryParseJson(result.text) : null;
86+
const resultStatus = result?.status ?? "final";
87+
const hasFinalResult = !!result && resultStatus !== "pending";
88+
const parsedResult = hasFinalResult ? tryParseJson(result.text) : null;
6489

6590
// Detect error: result text starts with "Error" or contains error indicators
66-
const isError = result
91+
const isError = hasFinalResult
6792
? isErrorResult(result.text, parsedResult)
6893
: false;
94+
const status: ToolCallStatus = resultStatus === "pending"
95+
? "running"
96+
: resultStatus === "waiting_for_input"
97+
? "waiting_for_input"
98+
: isError
99+
? "error"
100+
: "completed";
69101

70102
items.push({
71103
kind: "tool",
@@ -74,9 +106,10 @@ export function pairTranscriptSteps(steps: TranscriptStep[]): TranscriptItem[] {
74106
name: content.name,
75107
argsRaw: content.args,
76108
args: parsedArgs,
77-
resultRaw: result?.text ?? null,
109+
resultRaw: hasFinalResult ? result.text : null,
78110
result: parsedResult,
79-
status: result ? (isError ? "error" : "completed") : "running",
111+
status,
112+
liveOutput: result?.liveOutput,
80113
},
81114
});
82115
}
@@ -977,12 +1010,14 @@ const STATUS_ICONS: Record<ToolCallStatus, string> = {
9771010
running: "\u25B6", // ▶
9781011
completed: "\u2713", // ✓
9791012
error: "\u2717", // ✗
1013+
waiting_for_input: "!",
9801014
};
9811015

9821016
const STATUS_COLORS: Record<ToolCallStatus, string> = {
9831017
running: "text-accent",
9841018
completed: "text-status-success",
9851019
error: "text-status-error",
1020+
waiting_for_input: "text-blue-500",
9861021
};
9871022

9881023
/** Human-readable tool name: browser_navigate → Navigate */
@@ -1031,7 +1066,11 @@ export function ToolCall({pair}: {pair: ToolCallPair}) {
10311066
<div
10321067
className={cx(
10331068
"rounded-md border bg-app-dark-box/30",
1034-
pair.status === "error" ? "border-status-error/30" : "border-app-line/50",
1069+
pair.status === "error"
1070+
? "border-status-error/30"
1071+
: pair.status === "waiting_for_input"
1072+
? "border-blue-500/30"
1073+
: "border-app-line/50",
10351074
)}
10361075
>
10371076
{/* Header — always visible */}
@@ -1057,6 +1096,9 @@ export function ToolCall({pair}: {pair: ToolCallPair}) {
10571096
{pair.status === "running" && (
10581097
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-accent" />
10591098
)}
1099+
{pair.status === "waiting_for_input" && !expanded && (
1100+
<span className="text-tiny text-blue-500">Waiting for input</span>
1101+
)}
10601102
</button>
10611103

10621104
{/* Expanded body */}
@@ -1118,6 +1160,15 @@ function renderResult(
11181160
renderer: ToolRenderer,
11191161
): React.ReactNode {
11201162
if (pair.status === "running") {
1163+
if (pair.liveOutput) {
1164+
return (
1165+
<div className="px-3 py-2">
1166+
<pre className="max-h-60 overflow-auto whitespace-pre-wrap font-mono text-tiny text-ink-dull">
1167+
{pair.liveOutput}
1168+
</pre>
1169+
</div>
1170+
);
1171+
}
11211172
return (
11221173
<div className="flex items-center gap-2 px-3 py-2 text-tiny text-ink-faint">
11231174
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-accent" />
@@ -1126,6 +1177,15 @@ function renderResult(
11261177
);
11271178
}
11281179

1180+
if (pair.status === "waiting_for_input" && !pair.resultRaw) {
1181+
return (
1182+
<div className="flex items-center gap-2 px-3 py-2 text-tiny text-blue-500">
1183+
<span className="h-1.5 w-1.5 rounded-full bg-blue-500" />
1184+
Waiting for input
1185+
</div>
1186+
);
1187+
}
1188+
11291189
// Try custom result view first
11301190
if (renderer.resultView) {
11311191
const custom = renderer.resultView(pair);

interface/src/hooks/useChannelLiveState.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -614,7 +614,7 @@ export function useChannelLiveState(channels: ChannelInfo[]) {
614614
// Skip conversation/routing tools — they're infrastructure, not user-visible.
615615
if (HIDDEN_CHANNEL_TOOLS.has(event.tool_name)) return;
616616

617-
const toolCallId = `tool-${generateId()}`;
617+
const toolCallId = event.call_id || `tool-${generateId()}`;
618618
const queueKey = `${channelId}:${event.tool_name}`;
619619
const queue = pendingChannelToolCallsRef.current[queueKey] ?? [];
620620
pendingChannelToolCallsRef.current[queueKey] = [...queue, toolCallId];
@@ -710,9 +710,11 @@ export function useChannelLiveState(channels: ChannelInfo[]) {
710710
if (channelId && event.process_type === "channel") {
711711
const queueKey = `${channelId}:${event.tool_name}`;
712712
const queue = pendingChannelToolCallsRef.current[queueKey] ?? [];
713-
const toolCallId = queue[0];
713+
const toolCallId = event.call_id || queue[0];
714714
if (toolCallId) {
715-
pendingChannelToolCallsRef.current[queueKey] = queue.slice(1);
715+
pendingChannelToolCallsRef.current[queueKey] = queue.filter(
716+
(id) => id !== toolCallId,
717+
);
716718
updateItem(channelId, toolCallId, (item) => {
717719
if (item.type !== "tool_call_run") return item;
718720
return { ...item, result: event.result, status: "completed", completed_at: new Date().toISOString() };

0 commit comments

Comments
 (0)