@@ -6,6 +6,7 @@ import { api, HttpError } from "../api/client";
66import type { APIResponseAttempt , EventLogEntry , UsageEventRecord } from "../api/types" ;
77import { formatBytes , formatCost , formatLatency , formatNumber , formatTimestamp } from "../lib/utils" ;
88import {
9+ displayableGeneratedImageURLFromPart ,
910 displayableImageURLFromPart ,
1011 extractRequestTurns ,
1112 extractRequestToolDeclarations ,
@@ -1015,11 +1016,10 @@ function StructuredContentParts({
10151016function StructuredContentPart ( { part, index } : { part : Record < string , unknown > ; index : number } ) {
10161017 const type = stringRecordValue ( part . type ) ;
10171018 if ( type === "text" || type === "input_text" || type === "output_text" ) {
1018- const text = stringRecordValue ( part . text ) ;
1019- return text ? < MarkdownContent text = { text } className = "chat-part-text prose-md" /> : null ;
1019+ return < TextPart part = { part } /> ;
10201020 }
10211021 if ( type === "thinking" || type === "reasoning" ) {
1022- return < ThinkingPart part = { part } type = { type } /> ;
1022+ return type === "reasoning" ? < ReasoningPart part = { part } /> : < ThinkingPart part = { part } type = { type } /> ;
10231023 }
10241024 if ( type === "redacted_thinking" ) {
10251025 return (
@@ -1029,21 +1029,48 @@ function StructuredContentPart({ part, index }: { part: Record<string, unknown>;
10291029 </ div >
10301030 ) ;
10311031 }
1032- if ( type === "tool_use" ) {
1033- return < ToolUsePart part = { part } index = { index } /> ;
1032+ if ( type === "tool_use" || type === "mcp_tool_use" || type === "server_tool_use" ) {
1033+ return < ToolUsePart part = { part } index = { index } type = { type } /> ;
10341034 }
10351035 if ( type === "function_call" || type === "custom_tool_call" ) {
10361036 return < FunctionCallPart part = { part } type = { type } /> ;
10371037 }
1038- if ( type === "tool_result" ) {
1039- return < ToolResultPart part = { part } /> ;
1038+ if ( type === "function_call_output" || type === "custom_tool_call_output" ) {
1039+ return < FunctionCallOutputPart part = { part } type = { type } /> ;
1040+ }
1041+ if ( type === "tool_result" || type === "mcp_tool_result" || type === "web_search_tool_result" ) {
1042+ return < ToolResultPart part = { part } type = { type } /> ;
1043+ }
1044+ if ( type === "input_file" || type === "input_audio" || type === "file" || type === "document" ) {
1045+ return < FilePart part = { part } type = { type || "file" } /> ;
10401046 }
10411047 if ( isImageVisualPart ( type ) ) {
10421048 return < ImagePart part = { part } index = { index } /> ;
10431049 }
1050+ if ( type === "image_generation_call" ) {
1051+ return < ImageGenerationPart part = { part } /> ;
1052+ }
1053+ if ( type === "web_search_call" ) {
1054+ return < WebSearchCallPart part = { part } /> ;
1055+ }
10441056 return < GenericPart part = { part } type = { type || `part ${ index + 1 } ` } /> ;
10451057}
10461058
1059+ function TextPart ( { part } : { part : Record < string , unknown > } ) {
1060+ const text = stringRecordValue ( part . text ) ;
1061+ const annotations = Array . isArray ( part . annotations ) ? part . annotations : [ ] ;
1062+ return (
1063+ < div >
1064+ { text ? < MarkdownContent text = { text } className = "chat-part-text prose-md" /> : null }
1065+ { annotations . length > 0 && (
1066+ < div className = "part-meta" >
1067+ < span className = "part-chip" > { annotations . length } annotation{ annotations . length === 1 ? "" : "s" } </ span >
1068+ </ div >
1069+ ) }
1070+ </ div >
1071+ ) ;
1072+ }
1073+
10471074function ImagePart ( { part, index } : { part : Record < string , unknown > ; index : number } ) {
10481075 const type = stringRecordValue ( part . type ) || "image" ;
10491076 const url = displayableImageURLFromPart ( part ) ;
@@ -1077,17 +1104,42 @@ function ThinkingPart({ part, type }: { part: Record<string, unknown>; type: str
10771104 ) ;
10781105}
10791106
1080- function ToolUsePart ( { part, index } : { part : Record < string , unknown > ; index : number } ) {
1107+ function ReasoningPart ( { part } : { part : Record < string , unknown > } ) {
1108+ const text = reasoningText ( part ) ;
1109+ const encrypted = ! ! stringRecordValue ( part . encrypted_content ) || part . encrypted === true ;
1110+ return (
1111+ < div className = "thinking-part" >
1112+ < div className = "part-header" >
1113+ < div className = "tool-use-title" >
1114+ < span className = "part-type-badge" > reasoning</ span >
1115+ { stringRecordValue ( part . status ) && < span className = "part-chip" > { stringRecordValue ( part . status ) } </ span > }
1116+ </ div >
1117+ { stringRecordValue ( part . id ) && < span className = "tool-id" > { stringRecordValue ( part . id ) } </ span > }
1118+ </ div >
1119+ { text ? < div className = "thinking-text" > { text } </ div > : null }
1120+ { ! text && encrypted ? (
1121+ < div className = "chat-hidden-part" >
1122+ < span > Encrypted reasoning block</ span >
1123+ </ div >
1124+ ) : null }
1125+ { ! text && ! encrypted ? < JsonValueBlock label = "Payload" value = { part } /> : null }
1126+ </ div >
1127+ ) ;
1128+ }
1129+
1130+ function ToolUsePart ( { part, index, type } : { part : Record < string , unknown > ; index : number ; type : string } ) {
10811131 const name = stringRecordValue ( part . name ) || `tool_${ index + 1 } ` ;
10821132 const id = stringRecordValue ( part . id ) || stringRecordValue ( part . tool_use_id ) || stringRecordValue ( part . call_id ) ;
1133+ const serverName = stringRecordValue ( part . server_name ) ;
10831134 const input = part . input ?? { } ;
10841135 const questions = askUserQuestions ( input ) ;
10851136 return (
10861137 < div className = "tool-use-card" >
10871138 < div className = "tool-use-header" >
10881139 < div className = "tool-use-title" >
1089- < span className = "part-type-badge" > tool_use </ span >
1140+ < span className = "part-type-badge" > { type } </ span >
10901141 < span className = "tool-name" > { name } </ span >
1142+ { serverName && < span className = "part-chip" > { serverName } </ span > }
10911143 </ div >
10921144 { id && < span className = "tool-id" > { id } </ span > }
10931145 </ div >
@@ -1118,15 +1170,33 @@ function FunctionCallPart({ part, type }: { part: Record<string, unknown>; type:
11181170 ) ;
11191171}
11201172
1121- function ToolResultPart ( { part } : { part : Record < string , unknown > } ) {
1173+ function FunctionCallOutputPart ( { part, type } : { part : Record < string , unknown > ; type : string } ) {
1174+ const id = stringRecordValue ( part . call_id ) || stringRecordValue ( part . id ) ;
1175+ return (
1176+ < div className = "tool-result-card" >
1177+ < div className = "tool-use-header" >
1178+ < div className = "tool-use-title" >
1179+ < span className = "part-type-badge" > { type } </ span >
1180+ </ div >
1181+ { id && < span className = "tool-id" > { id } </ span > }
1182+ </ div >
1183+ < JsonValueBlock label = "Output" value = { part . output ?? part . content ?? null } />
1184+ </ div >
1185+ ) ;
1186+ }
1187+
1188+ function ToolResultPart ( { part, type } : { part : Record < string , unknown > ; type : string } ) {
11221189 const id = stringRecordValue ( part . tool_use_id ) || stringRecordValue ( part . id ) || stringRecordValue ( part . call_id ) ;
1190+ const serverName = stringRecordValue ( part . server_name ) ;
11231191 return (
11241192 < div className = "tool-result-card" >
11251193 < div className = "tool-use-header" >
11261194 < div className = "tool-use-title" >
1127- < span className = "part-type-badge" > tool_result</ span >
1128- { id && < span className = "tool-id" > { id } </ span > }
1195+ < span className = "part-type-badge" > { type } </ span >
1196+ { part . is_error === true && < span className = "part-chip part-chip-danger" > error</ span > }
1197+ { serverName && < span className = "part-chip" > { serverName } </ span > }
11291198 </ div >
1199+ { id && < span className = "tool-id" > { id } </ span > }
11301200 </ div >
11311201 < ToolResultContent value = { part . content } />
11321202 </ div >
@@ -1146,6 +1216,91 @@ function ToolResultContent({ value }: { value: unknown }) {
11461216 return < JsonValueBlock label = "Content" value = { value ?? null } /> ;
11471217}
11481218
1219+ function FilePart ( { part, type } : { part : Record < string , unknown > ; type : string } ) {
1220+ const title =
1221+ stringRecordValue ( part . filename ) ||
1222+ stringRecordValue ( part . name ) ||
1223+ stringRecordValue ( part . title ) ||
1224+ stringRecordValue ( part . file_id ) ||
1225+ stringRecordValue ( part . fileId ) ||
1226+ stringRecordValue ( part . file_url ) ||
1227+ "file" ;
1228+ const source = isRecord ( part . source ) ? part . source : null ;
1229+ const sourceType = stringRecordValue ( source ?. type ) ;
1230+ return (
1231+ < div className = "generic-part" >
1232+ < div className = "part-header" >
1233+ < div className = "tool-use-title" >
1234+ < span className = "part-type-badge" > { type } </ span >
1235+ < span className = "tool-name" > { title } </ span >
1236+ </ div >
1237+ { stringRecordValue ( part . media_type ?? part . mime_type ) && (
1238+ < span className = "part-muted" > { stringRecordValue ( part . media_type ?? part . mime_type ) } </ span >
1239+ ) }
1240+ </ div >
1241+ < div className = "part-meta" >
1242+ { stringRecordValue ( part . file_id ?? part . fileId ) && < span className = "part-chip" > { stringRecordValue ( part . file_id ?? part . fileId ) } </ span > }
1243+ { sourceType && < span className = "part-chip" > source: { sourceType } </ span > }
1244+ { Array . isArray ( part . citations ) && < span className = "part-chip" > { part . citations . length } citations</ span > }
1245+ { inlineDataLength ( part ) > 0 && < span className = "part-chip" > { formatNumber ( inlineDataLength ( part ) ) } inline chars</ span > }
1246+ </ div >
1247+ < JsonValueBlock label = "Details" value = { filePartSummary ( part ) } />
1248+ </ div >
1249+ ) ;
1250+ }
1251+
1252+ function ImageGenerationPart ( { part } : { part : Record < string , unknown > } ) {
1253+ const url = displayableGeneratedImageURLFromPart ( part ) ;
1254+ return (
1255+ < div className = "image-part" >
1256+ < div className = "part-header" >
1257+ < div className = "tool-use-title" >
1258+ < span className = "part-type-badge" > image_generation_call</ span >
1259+ { stringRecordValue ( part . status ) && < span className = "part-chip" > { stringRecordValue ( part . status ) } </ span > }
1260+ </ div >
1261+ { stringRecordValue ( part . id ) && < span className = "tool-id" > { stringRecordValue ( part . id ) } </ span > }
1262+ </ div >
1263+ < div className = "part-meta" >
1264+ { [ "action" , "size" , "quality" , "background" , "output_format" ] . map ( ( key ) =>
1265+ stringRecordValue ( part [ key ] ) ? < span key = { key } className = "part-chip" > { key } : { stringRecordValue ( part [ key ] ) } </ span > : null ,
1266+ ) }
1267+ </ div >
1268+ { url ? < img className = "chat-image" src = { url } alt = "generated image" loading = "lazy" /> : null }
1269+ { stringRecordValue ( part . revised_prompt ) && (
1270+ < div className = "tool-json-wrap" >
1271+ < div className = "tool-json-label" > Revised prompt</ div >
1272+ < div className = "tool-json-block" > { stringRecordValue ( part . revised_prompt ) } </ div >
1273+ </ div >
1274+ ) }
1275+ { ! url && < JsonValueBlock label = "Payload" value = { part } /> }
1276+ </ div >
1277+ ) ;
1278+ }
1279+
1280+ function WebSearchCallPart ( { part } : { part : Record < string , unknown > } ) {
1281+ const action = isRecord ( part . action ) ? part . action : null ;
1282+ const actionType = stringRecordValue ( action ?. type ) || stringRecordValue ( part . status ) || "web_search" ;
1283+ const sources = Array . isArray ( action ?. sources ) ? action . sources . length : 0 ;
1284+ return (
1285+ < div className = "generic-part" >
1286+ < div className = "part-header" >
1287+ < div className = "tool-use-title" >
1288+ < span className = "part-type-badge" > web_search_call</ span >
1289+ < span className = "tool-name" > { actionType } </ span >
1290+ </ div >
1291+ { stringRecordValue ( part . id ) && < span className = "tool-id" > { stringRecordValue ( part . id ) } </ span > }
1292+ </ div >
1293+ < div className = "part-meta" >
1294+ { stringRecordValue ( part . status ) && < span className = "part-chip" > { stringRecordValue ( part . status ) } </ span > }
1295+ { stringRecordValue ( action ?. query ) && < span className = "part-chip" > query</ span > }
1296+ { stringRecordValue ( action ?. url ) && < span className = "part-chip" > url</ span > }
1297+ { sources > 0 && < span className = "part-chip" > { sources } sources</ span > }
1298+ </ div >
1299+ { action ? < JsonValueBlock label = "Action" value = { action } /> : < JsonValueBlock label = "Payload" value = { part } /> }
1300+ </ div >
1301+ ) ;
1302+ }
1303+
11491304interface VisualQuestion {
11501305 header : string ;
11511306 question : string ;
@@ -1274,6 +1429,76 @@ function askUserQuestions(input: unknown): VisualQuestion[] | null {
12741429 return questions . length > 0 ? questions : null ;
12751430}
12761431
1432+ function reasoningText ( part : Record < string , unknown > ) : string {
1433+ const direct = stringRecordValue ( part . text ) || stringRecordValue ( part . thinking ) ;
1434+ if ( direct ) return direct ;
1435+ const summary = Array . isArray ( part . summary )
1436+ ? part . summary
1437+ . map ( ( item ) => ( isRecord ( item ) ? stringRecordValue ( item . text ) : "" ) )
1438+ . filter ( Boolean )
1439+ . join ( "\n" )
1440+ : "" ;
1441+ if ( summary ) return summary ;
1442+ if ( Array . isArray ( part . content ) ) {
1443+ return part . content
1444+ . map ( ( item ) => {
1445+ if ( ! isRecord ( item ) ) return "" ;
1446+ return stringRecordValue ( item . text ) || stringRecordValue ( item . thinking ) ;
1447+ } )
1448+ . filter ( Boolean )
1449+ . join ( "\n" ) ;
1450+ }
1451+ return "" ;
1452+ }
1453+
1454+ function filePartSummary ( part : Record < string , unknown > ) : Record < string , unknown > {
1455+ const source = isRecord ( part . source ) ? part . source : null ;
1456+ const summary : Record < string , unknown > = { } ;
1457+ for ( const key of [
1458+ "type" ,
1459+ "filename" ,
1460+ "name" ,
1461+ "title" ,
1462+ "media_type" ,
1463+ "mime_type" ,
1464+ "file_id" ,
1465+ "fileId" ,
1466+ "file_url" ,
1467+ "format" ,
1468+ ] ) {
1469+ if ( part [ key ] != null ) summary [ key ] = part [ key ] ;
1470+ }
1471+ if ( source ) {
1472+ summary . source = summarizeInlinePayload ( source ) ;
1473+ }
1474+ const dataKeys = [ "file_data" , "data" ] ;
1475+ for ( const key of dataKeys ) {
1476+ const data = stringRecordValue ( part [ key ] ) ;
1477+ if ( data ) summary [ key ] = `${ data . length . toLocaleString ( ) } chars` ;
1478+ }
1479+ if ( Array . isArray ( part . citations ) ) summary . citations = `${ part . citations . length } item${ part . citations . length === 1 ? "" : "s" } ` ;
1480+ return summary ;
1481+ }
1482+
1483+ function summarizeInlinePayload ( value : Record < string , unknown > ) : Record < string , unknown > {
1484+ const out : Record < string , unknown > = { } ;
1485+ for ( const [ key , raw ] of Object . entries ( value ) ) {
1486+ if ( ( key === "data" || key === "file_data" ) && typeof raw === "string" ) {
1487+ out [ key ] = `${ raw . length . toLocaleString ( ) } chars` ;
1488+ } else {
1489+ out [ key ] = raw ;
1490+ }
1491+ }
1492+ return out ;
1493+ }
1494+
1495+ function inlineDataLength ( part : Record < string , unknown > ) : number {
1496+ const own = stringRecordValue ( part . file_data ) . length || stringRecordValue ( part . data ) . length ;
1497+ if ( own ) return own ;
1498+ const source = isRecord ( part . source ) ? part . source : null ;
1499+ return source ? stringRecordValue ( source . data ) . length || stringRecordValue ( source . file_data ) . length : 0 ;
1500+ }
1501+
12771502function isRecord ( value : unknown ) : value is Record < string , unknown > {
12781503 return ! ! value && typeof value === "object" && ! Array . isArray ( value ) ;
12791504}
0 commit comments