11import { useState } from "react" ;
22import { 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
1123export 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
4357export 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
9821016const 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 ) ;
0 commit comments