From 9b7e4dd6e9403865119ff2d4a407e1ba2c333fd2 Mon Sep 17 00:00:00 2001 From: David Kaya Date: Tue, 31 Mar 2026 10:43:28 +0200 Subject: [PATCH] feat: format and syntax-highlight JSON arguments in approval popup - Add deepParseJsonStrings utility to recursively unwrap JSON-encoded string values inside tool-call args before display - Apply to McpDetail, CustomToolDetail, and HookDetail renderers - Add lightweight regex-based JSON syntax highlighting (keys, strings, numbers, booleans, punctuation) in CollapsibleCode when content is JSON - Remove unused Terminal import Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../components/chat/PermissionDetailView.tsx | 84 +++++++++++++++++-- 1 file changed, 78 insertions(+), 6 deletions(-) diff --git a/src/renderer/components/chat/PermissionDetailView.tsx b/src/renderer/components/chat/PermissionDetailView.tsx index 0306ef2..cfde39b 100644 --- a/src/renderer/components/chat/PermissionDetailView.tsx +++ b/src/renderer/components/chat/PermissionDetailView.tsx @@ -8,7 +8,6 @@ import { FileText, Globe, Server, - Terminal, } from 'lucide-react'; import type { PermissionDetail } from '@shared/contracts/sidecar'; @@ -61,6 +60,37 @@ export function permissionDetailSummary(detail: PermissionDetail): string | unde } } +/* ── Display helpers ─────────────────────────────────────────── */ + +/** Recursively parse string values that contain JSON objects or arrays (display-time only). */ +function deepParseJsonStrings(value: unknown): unknown { + if (typeof value === 'string') { + const trimmed = value.trim(); + if ( + (trimmed.startsWith('{') && trimmed.endsWith('}')) || + (trimmed.startsWith('[') && trimmed.endsWith(']')) + ) { + try { + return deepParseJsonStrings(JSON.parse(trimmed)); + } catch { + return value; + } + } + return value; + } + if (Array.isArray(value)) { + return value.map(deepParseJsonStrings); + } + if (value !== null && typeof value === 'object') { + const result: Record = {}; + for (const [k, v] of Object.entries(value as Record)) { + result[k] = deepParseJsonStrings(v); + } + return result; + } + return value; +} + /* ── Kind-specific renderers ────────────────────────────────── */ function ShellDetail({ detail }: { detail: PermissionDetail }) { @@ -134,7 +164,7 @@ function McpDetail({ detail }: { detail: PermissionDetail }) { )} {detail.args && Object.keys(detail.args).length > 0 && ( - + )} ); @@ -185,7 +215,7 @@ function CustomToolDetail({ detail }: { detail: PermissionDetail }) {

{detail.toolDescription}

)} {detail.args && Object.keys(detail.args).length > 0 && ( - + )} ); @@ -201,13 +231,13 @@ function HookDetail({ detail }: { detail: PermissionDetail }) { )} {detail.args && Object.keys(detail.args).length > 0 && ( - + )} ); } -/* ── Shared primitives ──────────────────────────────────────── */ +/* ── Shared primitives──────────────────────────────────────── */ function IntentionLine({ text }: { text: string }) { return

{text}

; @@ -242,6 +272,47 @@ function DiffBlock({ text }: { text: string }) { ); } +/* ── JSON syntax highlighting ───────────────────────────────── */ + +const jsonTokenPattern = + /("(?:[^"\\]|\\.)*")(\s*:)?|\b(true|false|null)\b|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)|([{}\[\],])/g; + +function JsonHighlighted({ json }: { json: string }) { + const elements: React.ReactNode[] = []; + let lastIndex = 0; + let key = 0; + + for (const match of json.matchAll(jsonTokenPattern)) { + const idx = match.index ?? 0; + if (idx > lastIndex) elements.push(json.slice(lastIndex, idx)); + + if (match[1] && match[2]) { + // Object key + colon + elements.push( + {match[1]}, + {match[2]}, + ); + } else if (match[1]) { + // String value + elements.push({match[1]}); + } else if (match[3]) { + // true / false / null + elements.push({match[3]}); + } else if (match[4]) { + // Number + elements.push({match[4]}); + } else if (match[5]) { + // Structural punctuation + elements.push({match[5]}); + } + + lastIndex = idx + match[0].length; + } + + if (lastIndex < json.length) elements.push(json.slice(lastIndex)); + return <>{elements}; +} + function CollapsibleCode({ label, text, @@ -254,6 +325,7 @@ function CollapsibleCode({ defaultExpanded?: boolean; }) { const [expanded, setExpanded] = useState(defaultExpanded); + const isJson = text.trimStart().startsWith('{') || text.trimStart().startsWith('['); return (
@@ -272,7 +344,7 @@ function CollapsibleCode({
{children ?? (
-              {text}
+              {isJson ?  : text}
             
)}