From 212bf1c70583524a9edf3d760b5cbc63006f1b5d Mon Sep 17 00:00:00 2001 From: Fredrik Jensen Date: Sun, 7 Dec 2025 12:12:49 +0100 Subject: [PATCH] add event debugging --- packages/inspector/src/App.tsx | 6 +- .../inspector/src/components/ResultsPane.css | 146 +++++++++++++++++- .../inspector/src/components/ResultsPane.tsx | 118 ++++++++++++-- packages/inspector/src/hooks/useMCP.ts | 13 ++ 4 files changed, 270 insertions(+), 13 deletions(-) diff --git a/packages/inspector/src/App.tsx b/packages/inspector/src/App.tsx index 23527c5..c7bafc8 100644 --- a/packages/inspector/src/App.tsx +++ b/packages/inspector/src/App.tsx @@ -28,6 +28,8 @@ export type ToolResult = { htmlContent: string | null isError: boolean timestamp: Date + executionTime: number // milliseconds + bundleSize: number | null // bytes, null if no UI } function App() { @@ -79,7 +81,9 @@ function App() { textContent: err instanceof Error ? err.message : 'Unknown error', htmlContent: null, isError: true, - timestamp: new Date() + timestamp: new Date(), + executionTime: 0, + bundleSize: null }) } finally { setIsExecuting(false) diff --git a/packages/inspector/src/components/ResultsPane.css b/packages/inspector/src/components/ResultsPane.css index c34e16e..3b55e87 100644 --- a/packages/inspector/src/components/ResultsPane.css +++ b/packages/inspector/src/components/ResultsPane.css @@ -34,7 +34,7 @@ display: inline-flex; align-items: center; gap: 6px; - padding: 12px 16px; + padding: 12px 12px; background: transparent; border: none; border-bottom: 2px solid transparent; @@ -57,7 +57,7 @@ display: inline-block; width: 5px; height: 5px; - margin-left: 5px; + margin-left: 2px; border-radius: 50%; background: rgba(255, 255, 255, 0.5); } @@ -80,6 +80,37 @@ color: var(--text-muted); } +.metrics { + display: flex; + align-items: center; + gap: 12px; +} + +.metric { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 11px; + color: var(--text-secondary); + font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, + monospace; + font-weight: 400; +} + +.tab-count { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 18px; + height: 18px; + padding: 0 5px; + margin-left: 6px; + font-size: 10px; + background: var(--accent-blue); + color: var(--white); + border-radius: 9px; +} + .icon-button { display: inline-flex; align-items: center; @@ -188,6 +219,100 @@ word-break: break-word; } +/* Events Panel */ +.events-panel { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.events-empty { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 12px; + color: var(--text-muted); + text-align: center; + padding: 40px; +} + +.events-empty p { + font-size: 14px; +} + +.events-empty span { + font-size: 12px; + color: var(--text-muted); +} + +.events-list { + flex: 1; + overflow-y: auto; + padding: 12px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.event-item { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 8px; + overflow: hidden; +} + +.event-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + background: var(--bg-tertiary); + border-bottom: 1px solid var(--border-color); +} + +.event-type { + font-size: 12px; + font-weight: 600; + font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, + monospace; + color: var(--text-primary); +} + +.event-time { + font-size: 10px; + color: var(--text-muted); + font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, + monospace; +} + +.event-payload { + padding: 10px 12px; + font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, + monospace; + font-size: 11px; + line-height: 1.5; + color: var(--text-secondary); + margin: 0; + white-space: pre-wrap; + word-break: break-word; +} + +/* Event type colors */ +.event-message .event-type { + color: var(--accent-blue); +} + +.event-tool .event-type { + color: var(--accent-purple, #a855f7); +} + +.event-resize .event-type { + color: var(--accent-green, #22c55e); +} + @media (max-width: 1200px) { .results-pane { min-width: 0; @@ -220,7 +345,8 @@ gap: 8px; } - .timestamp { + .timestamp, + .metrics { display: none; } @@ -242,4 +368,18 @@ .text-output pre { font-size: 12px; } + + .events-list { + padding: 8px; + gap: 6px; + } + + .event-header { + padding: 6px 10px; + } + + .event-payload { + padding: 8px 10px; + font-size: 10px; + } } diff --git a/packages/inspector/src/components/ResultsPane.tsx b/packages/inspector/src/components/ResultsPane.tsx index 3f80ea9..9e4158e 100644 --- a/packages/inspector/src/components/ResultsPane.tsx +++ b/packages/inspector/src/components/ResultsPane.tsx @@ -1,5 +1,5 @@ -import { useEffect, useState } from 'react' -import { Monitor, FileText, Clock, AlertTriangle, Maximize2, Minimize2, RotateCw } from 'lucide-react' +import { useEffect, useState, useRef, useCallback } from 'react' +import { Monitor, FileText, AlertTriangle, Maximize2, Minimize2, RotateCw, Radio, Trash2, Timer, Package } from 'lucide-react' import { Button } from './Button' import type { ToolResult } from '../App' import './ResultsPane.css' @@ -10,15 +10,61 @@ interface ResultsPaneProps { onReload?: () => void } -type Tab = 'ui' | 'text' +type Tab = 'ui' | 'text' | 'events' + +interface IframeEvent { + id: number + timestamp: Date + type: string + payload: unknown +} + +function formatBytes(bytes: number): string { + if (bytes < 1024) return `${bytes}b` + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}kb` + return `${(bytes / (1024 * 1024)).toFixed(1)}mb` +} export function ResultsPane({ result, isExecuting, onReload }: ResultsPaneProps) { const [activeTab, setActiveTab] = useState('ui') const [isFullscreen, setIsFullscreen] = useState(false) + const [events, setEvents] = useState([]) + const eventIdRef = useRef(0) + const iframeRef = useRef(null) const hasUI = result?.htmlContent != null const hasText = result?.textContent != null + // Clear events when result changes (new tool execution) + useEffect(() => { + setEvents([]) + eventIdRef.current = 0 + }, [result]) + + // Listen to postMessage events from the iframe + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + // Only capture messages from our iframe + if (iframeRef.current && event.source === iframeRef.current.contentWindow) { + const newEvent: IframeEvent = { + id: ++eventIdRef.current, + timestamp: new Date(), + type: event.data?.type || 'unknown', + payload: event.data?.payload || event.data + } + setEvents(prev => [...prev, newEvent]) + } + } + + window.addEventListener('message', handleMessage) + return () => window.removeEventListener('message', handleMessage) + }, []) + + const clearEvents = useCallback(() => { + setEvents([]) + eventIdRef.current = 0 + }, []) + useEffect(() => { if (hasUI) { setActiveTab('ui') @@ -47,14 +93,37 @@ export function ResultsPane({ result, isExecuting, onReload }: ResultsPaneProps) Text +
- {result?.timestamp && ( - - - {result.timestamp.toLocaleTimeString()} - + {result && !result.isError && ( +
+ + + {result.executionTime}ms + + {result.bundleSize && ( + + + {formatBytes(result.bundleSize)} + + )} +
+ )} + {activeTab === 'events' && events.length > 0 && ( +
) - ) : ( + ) : activeTab === 'text' ? (
{result.textContent || 'No text content'}
+ ) : ( +
+ {events.length === 0 ? ( +
+ +

No events yet

+ Events from the UI (sendPrompt, callTool, resize) will appear here +
+ ) : ( +
+ {events.map(event => ( +
+
+ {event.type} + + {event.timestamp.toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + })}.{event.timestamp.getMilliseconds().toString().padStart(3, '0')} + +
+
+                      {JSON.stringify(event.payload, null, 2)}
+                    
+
+ ))} +
+ )} +
)} diff --git a/packages/inspector/src/hooks/useMCP.ts b/packages/inspector/src/hooks/useMCP.ts index 3963072..37168c3 100644 --- a/packages/inspector/src/hooks/useMCP.ts +++ b/packages/inspector/src/hooks/useMCP.ts @@ -16,6 +16,8 @@ interface UseMCPReturn { textContent: string htmlContent: string | null isError: boolean + executionTime: number + bundleSize: number | null }> } @@ -195,6 +197,8 @@ export function useMCP(): UseMCPReturn { throw new Error('Not connected') } + const startTime = performance.now() + // Build headers - only include session ID if present (stateful server) const headers: Record = { 'Content-Type': 'application/json', @@ -216,6 +220,8 @@ export function useMCP(): UseMCPReturn { }) const text = await response.text() + const executionTime = Math.round(performance.now() - startTime) + const result = parseSSE(text) as { result?: { content: Array<{ @@ -232,6 +238,8 @@ export function useMCP(): UseMCPReturn { textContent: result.error.message, htmlContent: null, isError: true, + executionTime, + bundleSize: null, } } @@ -247,10 +255,15 @@ export function useMCP(): UseMCPReturn { } } + // Calculate bundle size (in bytes) if there's HTML content + const bundleSize = htmlContent ? new Blob([htmlContent]).size : null + return { textContent, htmlContent, isError: false, + executionTime, + bundleSize, } }, [isConnected, sessionId])