add event debugging
This commit is contained in:
parent
2e52837bdb
commit
212bf1c705
@ -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)
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<Tab>('ui')
|
||||
const [isFullscreen, setIsFullscreen] = useState(false)
|
||||
const [events, setEvents] = useState<IframeEvent[]>([])
|
||||
const eventIdRef = useRef(0)
|
||||
const iframeRef = useRef<HTMLIFrameElement>(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
|
||||
<span className={`tab-badge ${hasText ? 'active' : ''}`} />
|
||||
</button>
|
||||
<button
|
||||
className={`tab ${activeTab === 'events' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('events')}
|
||||
>
|
||||
Events
|
||||
{events.length > 0 && <span className="tab-count">{events.length}</span>}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="results-actions">
|
||||
{result?.timestamp && (
|
||||
<span className="timestamp">
|
||||
<Clock size={12} />
|
||||
{result.timestamp.toLocaleTimeString()}
|
||||
{result && !result.isError && (
|
||||
<div className="metrics">
|
||||
<span className="metric" title="Execution time">
|
||||
<Timer size={12} />
|
||||
{result.executionTime}ms
|
||||
</span>
|
||||
{result.bundleSize && (
|
||||
<span className="metric" title="Bundle size">
|
||||
<Package size={12} />
|
||||
{formatBytes(result.bundleSize)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{activeTab === 'events' && events.length > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={clearEvents}
|
||||
title="Clear events"
|
||||
icon={<Trash2 size={14} />}
|
||||
/>
|
||||
)}
|
||||
{onReload && (
|
||||
<Button
|
||||
@ -94,6 +163,7 @@ export function ResultsPane({ result, isExecuting, onReload }: ResultsPaneProps)
|
||||
) : activeTab === 'ui' ? (
|
||||
hasUI ? (
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
className="ui-frame"
|
||||
srcDoc={result.htmlContent!}
|
||||
title="Tool UI Output"
|
||||
@ -106,10 +176,40 @@ export function ResultsPane({ result, isExecuting, onReload }: ResultsPaneProps)
|
||||
<span>This tool may only return text content</span>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
) : activeTab === 'text' ? (
|
||||
<div className="text-output">
|
||||
<pre>{result.textContent || 'No text content'}</pre>
|
||||
</div>
|
||||
) : (
|
||||
<div className="events-panel">
|
||||
{events.length === 0 ? (
|
||||
<div className="events-empty">
|
||||
<Radio size={32} strokeWidth={1} />
|
||||
<p>No events yet</p>
|
||||
<span>Events from the UI (sendPrompt, callTool, resize) will appear here</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="events-list">
|
||||
{events.map(event => (
|
||||
<div key={event.id} className={`event-item event-${event.type}`}>
|
||||
<div className="event-header">
|
||||
<span className="event-type">{event.type}</span>
|
||||
<span className="event-time">
|
||||
{event.timestamp.toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
})}.{event.timestamp.getMilliseconds().toString().padStart(3, '0')}
|
||||
</span>
|
||||
</div>
|
||||
<pre className="event-payload">
|
||||
{JSON.stringify(event.payload, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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<string, string> = {
|
||||
'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])
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user