mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-30 14:37:13 +02:00
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>
This commit is contained in:
@@ -8,7 +8,6 @@ import {
|
|||||||
FileText,
|
FileText,
|
||||||
Globe,
|
Globe,
|
||||||
Server,
|
Server,
|
||||||
Terminal,
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
import type { PermissionDetail } from '@shared/contracts/sidecar';
|
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<string, unknown> = {};
|
||||||
|
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
||||||
|
result[k] = deepParseJsonStrings(v);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Kind-specific renderers ────────────────────────────────── */
|
/* ── Kind-specific renderers ────────────────────────────────── */
|
||||||
|
|
||||||
function ShellDetail({ detail }: { detail: PermissionDetail }) {
|
function ShellDetail({ detail }: { detail: PermissionDetail }) {
|
||||||
@@ -134,7 +164,7 @@ function McpDetail({ detail }: { detail: PermissionDetail }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{detail.args && Object.keys(detail.args).length > 0 && (
|
{detail.args && Object.keys(detail.args).length > 0 && (
|
||||||
<CollapsibleCode label="Arguments" text={JSON.stringify(detail.args, null, 2)} />
|
<CollapsibleCode label="Arguments" text={JSON.stringify(deepParseJsonStrings(detail.args), null, 2)} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -185,7 +215,7 @@ function CustomToolDetail({ detail }: { detail: PermissionDetail }) {
|
|||||||
<p className="text-[11px] text-[var(--color-text-secondary)]">{detail.toolDescription}</p>
|
<p className="text-[11px] text-[var(--color-text-secondary)]">{detail.toolDescription}</p>
|
||||||
)}
|
)}
|
||||||
{detail.args && Object.keys(detail.args).length > 0 && (
|
{detail.args && Object.keys(detail.args).length > 0 && (
|
||||||
<CollapsibleCode label="Arguments" text={JSON.stringify(detail.args, null, 2)} />
|
<CollapsibleCode label="Arguments" text={JSON.stringify(deepParseJsonStrings(detail.args), null, 2)} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -201,7 +231,7 @@ function HookDetail({ detail }: { detail: PermissionDetail }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{detail.args && Object.keys(detail.args).length > 0 && (
|
{detail.args && Object.keys(detail.args).length > 0 && (
|
||||||
<CollapsibleCode label="Arguments" text={JSON.stringify(detail.args, null, 2)} />
|
<CollapsibleCode label="Arguments" text={JSON.stringify(deepParseJsonStrings(detail.args), null, 2)} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -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(
|
||||||
|
<span key={key++} className="text-[var(--color-text-accent)]">{match[1]}</span>,
|
||||||
|
<span key={key++} className="text-[var(--color-text-muted)]">{match[2]}</span>,
|
||||||
|
);
|
||||||
|
} else if (match[1]) {
|
||||||
|
// String value
|
||||||
|
elements.push(<span key={key++} className="text-[var(--color-status-success)]">{match[1]}</span>);
|
||||||
|
} else if (match[3]) {
|
||||||
|
// true / false / null
|
||||||
|
elements.push(<span key={key++} className="text-[var(--color-accent-sky)]">{match[3]}</span>);
|
||||||
|
} else if (match[4]) {
|
||||||
|
// Number
|
||||||
|
elements.push(<span key={key++} className="text-[var(--color-accent-sky)]">{match[4]}</span>);
|
||||||
|
} else if (match[5]) {
|
||||||
|
// Structural punctuation
|
||||||
|
elements.push(<span key={key++} className="text-[var(--color-text-muted)]">{match[5]}</span>);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastIndex = idx + match[0].length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastIndex < json.length) elements.push(json.slice(lastIndex));
|
||||||
|
return <>{elements}</>;
|
||||||
|
}
|
||||||
|
|
||||||
function CollapsibleCode({
|
function CollapsibleCode({
|
||||||
label,
|
label,
|
||||||
text,
|
text,
|
||||||
@@ -254,6 +325,7 @@ function CollapsibleCode({
|
|||||||
defaultExpanded?: boolean;
|
defaultExpanded?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const [expanded, setExpanded] = useState(defaultExpanded);
|
const [expanded, setExpanded] = useState(defaultExpanded);
|
||||||
|
const isJson = text.trimStart().startsWith('{') || text.trimStart().startsWith('[');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface-1)]">
|
<div className="rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface-1)]">
|
||||||
@@ -272,7 +344,7 @@ function CollapsibleCode({
|
|||||||
<div className="border-t border-[var(--color-border-subtle)] px-2.5 py-1.5">
|
<div className="border-t border-[var(--color-border-subtle)] px-2.5 py-1.5">
|
||||||
{children ?? (
|
{children ?? (
|
||||||
<pre className="max-h-48 overflow-auto font-mono text-[10px] leading-relaxed text-[var(--color-text-primary)]">
|
<pre className="max-h-48 overflow-auto font-mono text-[10px] leading-relaxed text-[var(--color-text-primary)]">
|
||||||
{text}
|
{isJson ? <JsonHighlighted json={text} /> : text}
|
||||||
</pre>
|
</pre>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user