/** * EventDetailPanel — Side panel for full event details (Story 7.4) * * Features: * - Full JSON payload with syntax highlighting (react-json-view-lite) * - Collapsible tree viewer for nested objects * - Event metadata, timing, severity, hash * - Close button + Escape to close * - Click another event updates panel */ import React, { useCallback, useEffect } from 'react'; import { JsonView, darkStyles, allExpanded, collapseAllNested } from 'react-json-view-lite'; import 'react-json-view-lite/dist/index.css'; import type { AgentLensEvent, EventSeverity, LlmCallPayload, LlmResponsePayload, LlmMessage, } from '@agentkitai/agentlens-core'; import { otlpDetailFields } from '../lib/otlpEvent'; // ─── Types ────────────────────────────────────────────────────────── export interface EventDetailPanelProps { event: AgentLensEvent | null; onClose: () => void; /** All session events — used to find paired llm_response for llm_call events */ allEvents?: AgentLensEvent[]; } // ─── Severity badge ───────────────────────────────────────────────── const SEVERITY_COLORS: Record = { debug: 'bg-gray-100 text-gray-700', info: 'bg-blue-100 text-blue-700', warn: 'bg-yellow-100 text-yellow-800', error: 'bg-red-100 text-red-700', critical: 'bg-red-200 text-red-900', }; function SeverityBadge({ severity }: { severity: EventSeverity }) { return ( {severity} ); } // ─── Metadata row ─────────────────────────────────────────────────── function MetaRow({ label, value, mono = false }: { label: string; value: React.ReactNode; mono?: boolean }) { return (
{label} {value}
); } // ─── LLM helpers ──────────────────────────────────────────────────── function getMessageContentText(content: LlmMessage['content']): string { if (typeof content === 'string') return content; if (Array.isArray(content)) { return content .map((block) => ('text' in block && typeof block.text === 'string' ? block.text : '')) .filter(Boolean) .join('\n'); } return ''; } function formatTokenCount(n: number): string { if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`; if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`; return String(n); } function formatCost(usd: number): string { if (usd < 0.01) return `$${usd.toFixed(4)}`; return `$${usd.toFixed(2)}`; } function formatMs(ms: number): string { if (ms < 1000) return `${Math.round(ms)}ms`; if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`; return `${(ms / 60_000).toFixed(1)}m`; } function CopyButton({ text, label }: { text: string; label: string }) { const [copied, setCopied] = React.useState(false); const handleCopy = useCallback(() => { navigator.clipboard.writeText(text).then(() => { setCopied(true); setTimeout(() => setCopied(false), 2000); }); }, [text]); return ( ); } // ─── Message role colors ──────────────────────────────────────────── const ROLE_STYLES: Record = { system: { bg: 'bg-gray-100', text: 'text-gray-700', align: 'self-start', font: 'font-mono' }, user: { bg: 'bg-blue-50', text: 'text-blue-900', align: 'self-start', font: '' }, assistant: { bg: 'bg-green-50', text: 'text-green-900', align: 'self-end', font: '' }, tool: { bg: 'bg-gray-100', text: 'text-gray-700', align: 'self-start', font: 'font-mono' }, }; function getMessageRoleStyle(role: string) { return ROLE_STYLES[role] ?? ROLE_STYLES.user; } // ─── LLM Detail View ─────────────────────────────────────────────── function LlmDetailView({ callPayload, responsePayload, }: { callPayload: LlmCallPayload; responsePayload: LlmResponsePayload | null; }) { const isRedacted = callPayload.redacted || responsePayload?.redacted; // Build the full prompt text for copy-to-clipboard const promptText = React.useMemo(() => { if (isRedacted) return '[Content redacted]'; const parts: string[] = []; if (callPayload.systemPrompt) { parts.push(`[system]\n${callPayload.systemPrompt}`); } for (const msg of callPayload.messages) { parts.push(`[${msg.role}]\n${getMessageContentText(msg.content)}`); } return parts.join('\n\n'); }, [callPayload, isRedacted]); const completionText = React.useMemo(() => { if (!responsePayload) return ''; if (responsePayload.redacted) return '[Content redacted]'; return responsePayload.completion ?? ''; }, [responsePayload]); return (
{/* ── Prompt Section ───────────────────────────────── */}

Prompt

{isRedacted ? (
[Content redacted]
) : (
{/* System prompt (separate field) */} {callPayload.systemPrompt && (
system
{callPayload.systemPrompt}
)} {/* Messages in chat-bubble style */} {callPayload.messages.map((msg, i) => { const rs = getMessageRoleStyle(msg.role); const text = getMessageContentText(msg.content); return (
{msg.role}
{text || (empty)}
{msg.toolCalls && msg.toolCalls.length > 0 && (
Tool calls: {msg.toolCalls.map((tc) => tc.name).join(', ')}
)}
); })}
)}
{/* ── Completion Section ───────────────────────────── */} {responsePayload && (

Completion

{completionText && }
{responsePayload.redacted ? (
[Content redacted]
) : (
{responsePayload.completion ? (
{responsePayload.completion}
) : (
{responsePayload.toolCalls && responsePayload.toolCalls.length > 0 ? `(tool_use: ${responsePayload.toolCalls.map((tc) => tc.name).join(', ')})` : '(no completion text)'}
)} {/* Tool calls from response */} {responsePayload.toolCalls && responsePayload.toolCalls.length > 0 && (
Tool Calls
{responsePayload.toolCalls.map((tc, i) => (
{tc.name} ({tc.id})
))}
)}
)}
)} {/* ── Metadata Section ─────────────────────────────── */}

LLM Metadata

{callPayload.provider} } /> {callPayload.model} } /> {responsePayload && ( <> {formatCost(responsePayload.costUsd)} } /> )} {/* Parameters */} {callPayload.parameters && Object.keys(callPayload.parameters).length > 0 && ( <>
Parameters
{callPayload.parameters.temperature !== undefined && ( )} {callPayload.parameters.maxTokens !== undefined && ( )} {callPayload.parameters.topP !== undefined && ( )} {callPayload.parameters.stopSequences && callPayload.parameters.stopSequences.length > 0 && ( )} )} {/* Token breakdown */} {responsePayload && ( <>
Token Usage
{responsePayload.usage.thinkingTokens !== undefined && responsePayload.usage.thinkingTokens > 0 && ( )} {responsePayload.usage.cacheReadTokens !== undefined && responsePayload.usage.cacheReadTokens > 0 && ( )} {responsePayload.usage.cacheWriteTokens !== undefined && responsePayload.usage.cacheWriteTokens > 0 && ( )} )}
{/* ── Tools Section (if tools provided in call) ──── */} {callPayload.tools && callPayload.tools.length > 0 && (

Tools ({callPayload.tools.length})

{callPayload.tools.map((tool, i) => (
{tool.name}
{tool.description && (
{tool.description}
)} {tool.parameters && Object.keys(tool.parameters).length > 0 && (
} style={darkStyles} shouldExpandNode={collapseAllNested} />
)}
))}
)}
); } // ─── Component ────────────────────────────────────────────────────── export function EventDetailPanel({ event, onClose, allEvents }: EventDetailPanelProps) { // Escape key handler const handleKeyDown = useCallback( (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }, [onClose], ); useEffect(() => { document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [handleKeyDown]); // Find paired LLM event const llmPair = React.useMemo(() => { if (!event || !allEvents) return null; if (event.eventType === 'llm_call') { const callPayload = event.payload as LlmCallPayload; const responseEvent = allEvents.find( (e) => e.eventType === 'llm_response' && (e.payload as LlmResponsePayload).callId === callPayload.callId, ); return { callPayload, responsePayload: responseEvent ? (responseEvent.payload as LlmResponsePayload) : null, }; } if (event.eventType === 'llm_response') { const responsePayload = event.payload as LlmResponsePayload; const callEvent = allEvents.find( (e) => e.eventType === 'llm_call' && (e.payload as LlmCallPayload).callId === responsePayload.callId, ); return { callPayload: callEvent ? (callEvent.payload as LlmCallPayload) : null, responsePayload, }; } return null; }, [event, allEvents]); if (!event) return null; const isLlmEvent = event.eventType === 'llm_call' || event.eventType === 'llm_response'; // OTLP-ingested custom events (Claude Code metrics/logs) → readable labeled // fields, instead of only the raw JSON blob. const otlpFields = otlpDetailFields(event); return ( <> {/* Backdrop (click to close) */}
{/* Panel */}
{/* Header */}

{isLlmEvent ? '🧠 LLM Call Detail' : 'Event Detail'}

{/* Scrollable content */}
{/* LLM-specific detail view (Story 4.3) */} {isLlmEvent && llmPair && llmPair.callPayload && ( )} {/* Orphan llm_response without paired llm_call — show response data directly */} {isLlmEvent && llmPair && !llmPair.callPayload && llmPair.responsePayload && (

LLM Response (no paired call found)

{llmPair.responsePayload.provider} } /> {llmPair.responsePayload.model} } /> {formatCost(llmPair.responsePayload.costUsd)} } />
Token Usage
{llmPair.responsePayload.completion && (

Completion

{llmPair.responsePayload.completion}
)}
)} {/* Standard metadata (shown for all events) */}

{isLlmEvent ? 'Event Metadata' : 'Metadata'}

{event.eventType} } /> } />
{/* Hash chain */}

Hash Chain

{/* Custom metadata */} {Object.keys(event.metadata).length > 0 && (

Custom Metadata

} style={darkStyles} shouldExpandNode={allExpanded} />
)} {/* OTLP custom event — readable labeled fields (Claude Code metrics/logs) */} {otlpFields.length > 0 && (

Details

{otlpFields.map(([k, v]) => ( ))}
)} {/* Payload (raw JSON — for non-LLM events or as fallback) */} {!isLlmEvent && (

{otlpFields.length > 0 ? 'Raw payload' : 'Payload'}

} style={darkStyles} shouldExpandNode={collapseAllNested} />
)}
); }