Skip to content

Commit 2c56441

Browse files
committed
Add raw JSON toggle to event chat
1 parent bdecb98 commit 2c56441

3 files changed

Lines changed: 90 additions & 21 deletions

File tree

web/src/components/EventLogModal.tsx

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,7 @@ function BodyView({ raw, kind }: { raw: string; kind: "request" | "response" })
607607
text: streamToMarkdown(stream),
608608
encrypted: stream.encrypted,
609609
hiddenType: stream.hiddenType,
610+
raw,
610611
},
611612
],
612613
label: "stream",
@@ -623,6 +624,7 @@ function BodyView({ raw, kind }: { raw: string; kind: "request" | "response" })
623624
text: streamToMarkdown(json),
624625
encrypted: json.encrypted,
625626
hiddenType: json.hiddenType,
627+
raw,
626628
},
627629
],
628630
label: "json",
@@ -755,7 +757,7 @@ function finalChatTurns(requestRaw: string, responseRaw: string): Turn[] {
755757
if (requestTurns?.length) {
756758
turns.push(...requestTurns);
757759
} else if (requestRaw.trim()) {
758-
turns.push({ role: "user", text: fenceMarkdown(requestRaw.trim()) });
760+
turns.push({ role: "user", text: fenceMarkdown(requestRaw.trim()), raw: requestRaw });
759761
}
760762

761763
const responseTurn = responseToTurn(responseRaw);
@@ -772,6 +774,7 @@ function responseToTurn(raw: string): Turn | null {
772774
text: streamToMarkdown(stream),
773775
encrypted: stream.encrypted,
774776
hiddenType: stream.hiddenType,
777+
raw,
775778
};
776779
}
777780
const json = extractResponseJSON(raw);
@@ -781,9 +784,10 @@ function responseToTurn(raw: string): Turn | null {
781784
text: streamToMarkdown(json),
782785
encrypted: json.encrypted,
783786
hiddenType: json.hiddenType,
787+
raw,
784788
};
785789
}
786-
return { role: "assistant", text: fenceMarkdown(raw.trim()) };
790+
return { role: "assistant", text: fenceMarkdown(raw.trim()), raw };
787791
}
788792

789793
function finalResponseBody(entry: EventLogEntry): string {
@@ -836,6 +840,7 @@ function ToggleButton({
836840

837841
function ChatView({ turns }: { turns: Turn[] }) {
838842
const [expandedTools, setExpandedTools] = useState<Set<number>>(() => new Set());
843+
const [expandedRaw, setExpandedRaw] = useState<Set<number>>(() => new Set());
839844
const toggleTool = (index: number) => {
840845
setExpandedTools((current) => {
841846
const next = new Set(current);
@@ -844,12 +849,23 @@ function ChatView({ turns }: { turns: Turn[] }) {
844849
return next;
845850
});
846851
};
852+
const toggleRaw = (index: number) => {
853+
setExpandedRaw((current) => {
854+
const next = new Set(current);
855+
if (next.has(index)) next.delete(index);
856+
else next.add(index);
857+
return next;
858+
});
859+
};
847860

848861
return (
849862
<div className="bg-panel border border-border rounded p-3 chat-view">
850863
{turns.map((turn, index) => {
851864
const isTool = isToolTurn(turn);
852865
const collapsed = isTool && !expandedTools.has(index);
866+
const rawText = turnRawText(turn);
867+
const rawExpanded = rawText != null && expandedRaw.has(index);
868+
const hasActions = isTool || rawText != null;
853869
const bubbleClass = isTool ? "chat-bubble-tool" : chatBubbleClass(turn.role);
854870
return (
855871
<div
@@ -865,13 +881,29 @@ function ChatView({ turns }: { turns: Turn[] }) {
865881
{turn.role}
866882
</span>
867883
<span className="chat-turn">#{index + 1}</span>
868-
{isTool && (
869-
<button className="chat-toggle" onClick={() => toggleTool(index)}>
870-
{collapsed ? "Expand" : "Collapse"}
871-
</button>
884+
{hasActions && (
885+
<div className="chat-actions">
886+
{isTool && (
887+
<button className="chat-toggle" onClick={() => toggleTool(index)}>
888+
{collapsed ? "Expand" : "Collapse"}
889+
</button>
890+
)}
891+
{rawText != null && (
892+
<button
893+
className={clsx("chat-toggle", rawExpanded && "chat-toggle-active")}
894+
onClick={() => toggleRaw(index)}
895+
aria-pressed={rawExpanded}
896+
title="Show original JSON"
897+
>
898+
{rawExpanded ? "Hide" : "JSON"}
899+
</button>
900+
)}
901+
</div>
872902
)}
873903
</div>
874-
{collapsed ? (
904+
{rawExpanded ? (
905+
<pre className="chat-raw-json">{rawText}</pre>
906+
) : collapsed ? (
875907
<div className="chat-preview">{chatPreview(turn)}</div>
876908
) : (
877909
<>
@@ -897,6 +929,24 @@ function ChatView({ turns }: { turns: Turn[] }) {
897929
);
898930
}
899931

932+
function turnRawText(turn: Turn): string | null {
933+
if (!Object.prototype.hasOwnProperty.call(turn, "raw")) return null;
934+
return rawValueText(turn.raw);
935+
}
936+
937+
function rawValueText(value: unknown): string {
938+
if (typeof value === "string") {
939+
const pretty = tryPrettyJson(value);
940+
return pretty || JSON.stringify(value);
941+
}
942+
if (value === undefined) return "undefined";
943+
try {
944+
return JSON.stringify(value, null, 2) ?? String(value);
945+
} catch {
946+
return String(value);
947+
}
948+
}
949+
900950
function isToolTurn(turn: Turn): boolean {
901951
if (turn.role.toLowerCase() === "tool") return true;
902952
const text = turnText(turn).trimStart();

web/src/lib/protocol.ts

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export interface Turn {
1919
attachments?: string[];
2020
encrypted?: boolean;
2121
hiddenType?: string;
22+
raw?: unknown;
2223
}
2324

2425
export function extractRequestTurns(rawJson: string): Turn[] | null {
@@ -33,11 +34,11 @@ export function extractRequestTurns(rawJson: string): Turn[] | null {
3334

3435
const turns: Turn[] = [];
3536

36-
appendInstructionTurn(turns, "system", o.system);
37-
appendInstructionTurn(turns, "system", o.instructions);
38-
appendInstructionTurn(turns, "system", o.systemInstruction);
39-
appendInstructionTurn(turns, "system", o.system_instruction);
40-
appendInstructionTurn(turns, "developer", o.developer);
37+
appendInstructionTurn(turns, "system", "system", o.system);
38+
appendInstructionTurn(turns, "system", "instructions", o.instructions);
39+
appendInstructionTurn(turns, "system", "systemInstruction", o.systemInstruction);
40+
appendInstructionTurn(turns, "system", "system_instruction", o.system_instruction);
41+
appendInstructionTurn(turns, "developer", "developer", o.developer);
4142

4243
if (Array.isArray(o.messages)) {
4344
for (const m of o.messages) turns.push(messageToTurn(m));
@@ -68,9 +69,9 @@ export function extractRequestToolDeclarations(rawJson: string): Turn | null {
6869
return toolDeclarationTurn(o.tools);
6970
}
7071

71-
function appendInstructionTurn(turns: Turn[], role: string, raw: unknown) {
72+
function appendInstructionTurn(turns: Turn[], role: string, key: string, raw: unknown) {
7273
const text = instructionToText(raw);
73-
if (text.trim()) turns.push({ role, text });
74+
if (text.trim()) turns.push({ role, text, raw: { [key]: raw } });
7475
}
7576

7677
function instructionToText(raw: unknown): string {
@@ -150,9 +151,9 @@ function messageToTurn(raw: unknown): Turn {
150151
}
151152
if (!text && attachments.length === 0) {
152153
const fallback = messageFallbackMarkdown(m, type || "message");
153-
if (fallback) return { role, text: fallback };
154+
if (fallback) return { role, text: fallback, raw };
154155
}
155-
return { role, text, attachments: attachments.length ? attachments : undefined };
156+
return { role, text, attachments: attachments.length ? attachments : undefined, raw };
156157
}
157158

158159
function isImagePartType(type: string): boolean {
@@ -203,6 +204,7 @@ function toolDeclarationTurn(raw: unknown): Turn | null {
203204
return {
204205
role: "tool",
205206
text: toolDeclarationsMarkdown(raw),
207+
raw,
206208
};
207209
}
208210

@@ -404,6 +406,7 @@ function responsesInputToTurn(raw: unknown): Turn {
404406
return {
405407
role: "assistant",
406408
text: toolUseMarkdown(name, m.call_id, json, "json"),
409+
raw: m,
407410
};
408411
}
409412
if (type === "custom_tool_call") {
@@ -412,13 +415,15 @@ function responsesInputToTurn(raw: unknown): Turn {
412415
return {
413416
role: "assistant",
414417
text: toolUseMarkdown(name, m.call_id, input, customToolLanguage(name, input)),
418+
raw: m,
415419
};
416420
}
417421
if (type === "function_call_output" || type === "custom_tool_call_output") {
418422
const out = typeof m.output === "string" ? m.output : JSON.stringify(m.output ?? "", null, 2);
419423
return {
420424
role: "tool",
421425
text: toolResultMarkdown(m.call_id, out),
426+
raw: m,
422427
};
423428
}
424429
if (type === "reasoning") {
@@ -430,17 +435,18 @@ function responsesInputToTurn(raw: unknown): Turn {
430435
: "";
431436
if (summary) {
432437
const quoted = summary.split("\n").map((l) => "> " + l).join("\n");
433-
return { role: "assistant", text: "_thinking_\n" + quoted };
438+
return { role: "assistant", text: "_thinking_\n" + quoted, raw: m };
434439
}
435440
if (hasEncryptedReasoning(m)) {
436-
return { role: "assistant", text: hiddenTypeText(type), encrypted: true, hiddenType: type };
441+
return { role: "assistant", text: hiddenTypeText(type), encrypted: true, hiddenType: type, raw: m };
437442
}
438-
return { role: "assistant", text: "", attachments: ["reasoning"] };
443+
return { role: "assistant", text: "", attachments: ["reasoning"], raw: m };
439444
}
440445
if (type === "web_search_call") {
441446
return {
442447
role: "assistant",
443448
text: webSearchCallMarkdown(m),
449+
raw: m,
444450
};
445451
}
446452
return typedItemToTurn(m, type);
@@ -450,6 +456,7 @@ function typedItemToTurn(item: Record<string, unknown>, type: string): Turn {
450456
return {
451457
role: type,
452458
text: typedItemMarkdown(item, type),
459+
raw: item,
453460
};
454461
}
455462

@@ -570,7 +577,7 @@ function geminiContentToTurn(raw: unknown): Turn {
570577
else if (o.functionResponse) attachments.push("function_response");
571578
else attachments.push(Object.keys(o).join(",") || "part");
572579
}
573-
return { role, text, attachments: attachments.length ? attachments : undefined };
580+
return { role, text, attachments: attachments.length ? attachments : undefined, raw };
574581
}
575582

576583

web/src/styles.css

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,17 +98,29 @@ body {
9898
.chat-turn {
9999
color: #8a93a3; font-family: "JetBrains Mono", Menlo, Consolas, monospace; font-size: 10px;
100100
}
101+
.chat-actions {
102+
margin-left: auto; display: flex; align-items: center; gap: 0.35rem;
103+
}
101104
.chat-toggle {
102-
margin-left: auto; color: #c9d0dc; border: 1px solid #3b4454; border-radius: 4px;
105+
color: #c9d0dc; border: 1px solid #3b4454; border-radius: 4px;
103106
padding: 0.1rem 0.4rem; font-size: 10px; line-height: 1.2;
104107
}
105108
.chat-toggle:hover {
106109
color: #e7eaf0; background: #2a3140;
107110
}
111+
.chat-toggle-active {
112+
color: #e7eaf0; background: #2a3140;
113+
}
108114
.chat-preview {
109115
color: #c9d0dc; font-family: "JetBrains Mono", Menlo, Consolas, monospace;
110116
font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
111117
}
118+
.chat-raw-json {
119+
max-height: 360px; overflow: auto; margin: 0; padding: 0.6rem 0.75rem;
120+
color: #c9d0dc; background: #0b0d11; border: 1px solid #343c4d; border-radius: 4px;
121+
font-family: "JetBrains Mono", Menlo, Consolas, monospace; font-size: 11px;
122+
line-height: 1.45; white-space: pre-wrap; overflow-wrap: anywhere;
123+
}
112124
.chat-body.prose-md {
113125
font-size: 12px;
114126
}

0 commit comments

Comments
 (0)