Pretty-print JSON in tool inputs and preformatted blocks
Tool call inputs and structured data categories (hook_progress,
file_snapshot) now attempt JSON.parse + JSON.stringify(_, null, 2)
before escaping to HTML. Non-JSON content passes through unchanged.
The detection fast-paths by checking the first non-whitespace
character for { or [ before attempting parse.
Also renames the copy state variable from linkCopied to contentCopied
to match the current behavior of copying message content rather than
anchor links.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -36,7 +36,7 @@ export function MessageBubble({
|
|||||||
// Collapsible state for thinking blocks and tool calls/results
|
// Collapsible state for thinking blocks and tool calls/results
|
||||||
const isCollapsible = message.category === "thinking" || message.category === "tool_call" || message.category === "tool_result";
|
const isCollapsible = message.category === "thinking" || message.category === "tool_call" || message.category === "tool_result";
|
||||||
const [collapsed, setCollapsed] = useState(isCollapsible);
|
const [collapsed, setCollapsed] = useState(isCollapsible);
|
||||||
const [linkCopied, setLinkCopied] = useState(false);
|
const [contentCopied, setContentCopied] = useState(false);
|
||||||
|
|
||||||
const renderedHtml = useMemo(() => {
|
const renderedHtml = useMemo(() => {
|
||||||
// Skip expensive rendering when content is collapsed and not visible
|
// Skip expensive rendering when content is collapsed and not visible
|
||||||
@@ -46,7 +46,7 @@ export function MessageBubble({
|
|||||||
|
|
||||||
if (msg.category === "tool_call") {
|
if (msg.category === "tool_call") {
|
||||||
const inputHtml = msg.toolInput
|
const inputHtml = msg.toolInput
|
||||||
? `<pre class="hljs mt-2"><code>${escapeHtml(msg.toolInput)}</code></pre>`
|
? `<pre class="hljs mt-2"><code>${escapeHtml(tryPrettyJson(msg.toolInput))}</code></pre>`
|
||||||
: "";
|
: "";
|
||||||
const html = `<div class="font-medium ${colors.text}">${escapeHtml(msg.toolName || "Unknown Tool")}</div>${inputHtml}`;
|
const html = `<div class="font-medium ${colors.text}">${escapeHtml(msg.toolName || "Unknown Tool")}</div>${inputHtml}`;
|
||||||
return searchQuery ? highlightSearchText(html, searchQuery) : html;
|
return searchQuery ? highlightSearchText(html, searchQuery) : html;
|
||||||
@@ -55,7 +55,7 @@ export function MessageBubble({
|
|||||||
// Structured data categories: render as preformatted text, not markdown.
|
// Structured data categories: render as preformatted text, not markdown.
|
||||||
// Avoids expensive marked.parse() on large JSON/log blobs.
|
// Avoids expensive marked.parse() on large JSON/log blobs.
|
||||||
if (msg.category === "hook_progress" || msg.category === "file_snapshot") {
|
if (msg.category === "hook_progress" || msg.category === "file_snapshot") {
|
||||||
const html = `<pre class="hljs"><code>${escapeHtml(msg.content)}</code></pre>`;
|
const html = `<pre class="hljs"><code>${escapeHtml(tryPrettyJson(msg.content))}</code></pre>`;
|
||||||
return searchQuery ? highlightSearchText(html, searchQuery) : html;
|
return searchQuery ? highlightSearchText(html, searchQuery) : html;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,14 +157,14 @@ export function MessageBubble({
|
|||||||
? `${message.toolName || "Tool Call"}\n${message.toolInput || ""}`
|
? `${message.toolName || "Tool Call"}\n${message.toolInput || ""}`
|
||||||
: message.content;
|
: message.content;
|
||||||
navigator.clipboard.writeText(text).then(() => {
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
setLinkCopied(true);
|
setContentCopied(true);
|
||||||
setTimeout(() => setLinkCopied(false), 1500);
|
setTimeout(() => setContentCopied(false), 1500);
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}}
|
}}
|
||||||
className="flex items-center justify-center w-7 h-7 rounded-md text-foreground-muted hover:text-foreground hover:bg-surface-overlay/60 transition-colors"
|
className="flex items-center justify-center w-7 h-7 rounded-md text-foreground-muted hover:text-foreground hover:bg-surface-overlay/60 transition-colors"
|
||||||
title="Copy message content"
|
title="Copy message content"
|
||||||
>
|
>
|
||||||
{linkCopied ? (
|
{contentCopied ? (
|
||||||
<svg className="w-4 h-4 text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
<svg className="w-4 h-4 text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -260,3 +260,14 @@ function formatTimestamp(ts: string): string {
|
|||||||
second: "2-digit",
|
second: "2-digit",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** If the string is valid JSON, return it pretty-printed; otherwise return as-is. */
|
||||||
|
function tryPrettyJson(text: string): string {
|
||||||
|
const trimmed = text.trimStart();
|
||||||
|
if (trimmed[0] !== "{" && trimmed[0] !== "[") return text;
|
||||||
|
try {
|
||||||
|
return JSON.stringify(JSON.parse(text), null, 2);
|
||||||
|
} catch {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user