Skip to content

Commit 288708d

Browse files
committed
Expand protocol log part rendering
1 parent f22283b commit 288708d

3 files changed

Lines changed: 260 additions & 12 deletions

File tree

web/src/components/EventLogModal.tsx

Lines changed: 237 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { api, HttpError } from "../api/client";
66
import type { APIResponseAttempt, EventLogEntry, UsageEventRecord } from "../api/types";
77
import { formatBytes, formatCost, formatLatency, formatNumber, formatTimestamp } from "../lib/utils";
88
import {
9+
displayableGeneratedImageURLFromPart,
910
displayableImageURLFromPart,
1011
extractRequestTurns,
1112
extractRequestToolDeclarations,
@@ -1015,11 +1016,10 @@ function StructuredContentParts({
10151016
function 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+
10471074
function 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+
11491304
interface 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+
12771502
function isRecord(value: unknown): value is Record<string, unknown> {
12781503
return !!value && typeof value === "object" && !Array.isArray(value);
12791504
}

web/src/lib/protocol.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,16 @@ export function displayableImageURLFromPart(part: Record<string, unknown>): stri
279279
return url && isDisplayableImageURL(url) ? url : null;
280280
}
281281

282+
export function displayableGeneratedImageURLFromPart(part: Record<string, unknown>): string | null {
283+
const data = stringValue(part.result ?? part.partial ?? part.partial_image_b64);
284+
if (!data) return null;
285+
const mimeType =
286+
stringValue(part.mime_type ?? part.media_type) ||
287+
imageMimeTypeFromOutputFormat(stringValue(part.output_format)) ||
288+
"image/png";
289+
return dataToImageURL(mimeType, data);
290+
}
291+
282292
function imageURLFromPart(part: Record<string, unknown>): string | null {
283293
const sourceURL = imageURLFromSource(part.source);
284294
if (sourceURL) return sourceURL;

web/src/styles.css

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,19 @@ body {
162162
.tool-use-title {
163163
display: flex; align-items: center; min-width: 0; gap: 0.45rem;
164164
}
165+
.part-meta {
166+
display: flex; flex-wrap: wrap; gap: 0.35rem; margin: 0.25rem 0 0.45rem;
167+
}
168+
.part-chip {
169+
display: inline-flex; align-items: center; min-width: 0; max-width: 100%;
170+
border: 1px solid #3b4454; border-radius: 4px; background: #171c26;
171+
color: #c9d0dc; padding: 0.08rem 0.35rem;
172+
font-family: "JetBrains Mono", Menlo, Consolas, monospace; font-size: 10px;
173+
line-height: 1.35; overflow-wrap: anywhere;
174+
}
175+
.part-chip-danger {
176+
color: #ff8f8f; border-color: #7a3030; background: #351719;
177+
}
165178
.part-type-badge {
166179
display: inline-flex; align-items: center; height: 19px; padding: 0 0.45rem;
167180
border: 1px solid #3b5f8f; border-radius: 4px; color: #9ec5ff;

0 commit comments

Comments
 (0)