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
|
||||
const isCollapsible = message.category === "thinking" || message.category === "tool_call" || message.category === "tool_result";
|
||||
const [collapsed, setCollapsed] = useState(isCollapsible);
|
||||
const [linkCopied, setLinkCopied] = useState(false);
|
||||
const [contentCopied, setContentCopied] = useState(false);
|
||||
|
||||
const renderedHtml = useMemo(() => {
|
||||
// Skip expensive rendering when content is collapsed and not visible
|
||||
@@ -46,7 +46,7 @@ export function MessageBubble({
|
||||
|
||||
if (msg.category === "tool_call") {
|
||||
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}`;
|
||||
return searchQuery ? highlightSearchText(html, searchQuery) : html;
|
||||
@@ -55,7 +55,7 @@ export function MessageBubble({
|
||||
// Structured data categories: render as preformatted text, not markdown.
|
||||
// Avoids expensive marked.parse() on large JSON/log blobs.
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -157,14 +157,14 @@ export function MessageBubble({
|
||||
? `${message.toolName || "Tool Call"}\n${message.toolInput || ""}`
|
||||
: message.content;
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
setLinkCopied(true);
|
||||
setTimeout(() => setLinkCopied(false), 1500);
|
||||
setContentCopied(true);
|
||||
setTimeout(() => setContentCopied(false), 1500);
|
||||
}).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"
|
||||
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}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||
</svg>
|
||||
@@ -260,3 +260,14 @@ function formatTimestamp(ts: string): string {
|
||||
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