add event debugging
This commit is contained in:
parent
2e52837bdb
commit
212bf1c705
@ -28,6 +28,8 @@ export type ToolResult = {
|
|||||||
htmlContent: string | null
|
htmlContent: string | null
|
||||||
isError: boolean
|
isError: boolean
|
||||||
timestamp: Date
|
timestamp: Date
|
||||||
|
executionTime: number // milliseconds
|
||||||
|
bundleSize: number | null // bytes, null if no UI
|
||||||
}
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
@ -79,7 +81,9 @@ function App() {
|
|||||||
textContent: err instanceof Error ? err.message : 'Unknown error',
|
textContent: err instanceof Error ? err.message : 'Unknown error',
|
||||||
htmlContent: null,
|
htmlContent: null,
|
||||||
isError: true,
|
isError: true,
|
||||||
timestamp: new Date()
|
timestamp: new Date(),
|
||||||
|
executionTime: 0,
|
||||||
|
bundleSize: null
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
setIsExecuting(false)
|
setIsExecuting(false)
|
||||||
|
|||||||
@ -34,7 +34,7 @@
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 12px 16px;
|
padding: 12px 12px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
border-bottom: 2px solid transparent;
|
border-bottom: 2px solid transparent;
|
||||||
@ -57,7 +57,7 @@
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 5px;
|
width: 5px;
|
||||||
height: 5px;
|
height: 5px;
|
||||||
margin-left: 5px;
|
margin-left: 2px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: rgba(255, 255, 255, 0.5);
|
background: rgba(255, 255, 255, 0.5);
|
||||||
}
|
}
|
||||||
@ -80,6 +80,37 @@
|
|||||||
color: var(--text-muted);
|
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 {
|
.icon-button {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -188,6 +219,100 @@
|
|||||||
word-break: break-word;
|
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) {
|
@media (max-width: 1200px) {
|
||||||
.results-pane {
|
.results-pane {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@ -220,7 +345,8 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timestamp {
|
.timestamp,
|
||||||
|
.metrics {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -242,4 +368,18 @@
|
|||||||
.text-output pre {
|
.text-output pre {
|
||||||
font-size: 12px;
|
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 { useEffect, useState, useRef, useCallback } from 'react'
|
||||||
import { Monitor, FileText, Clock, AlertTriangle, Maximize2, Minimize2, RotateCw } from 'lucide-react'
|
import { Monitor, FileText, AlertTriangle, Maximize2, Minimize2, RotateCw, Radio, Trash2, Timer, Package } from 'lucide-react'
|
||||||
import { Button } from './Button'
|
import { Button } from './Button'
|
||||||
import type { ToolResult } from '../App'
|
import type { ToolResult } from '../App'
|
||||||
import './ResultsPane.css'
|
import './ResultsPane.css'
|
||||||
@ -10,15 +10,61 @@ interface ResultsPaneProps {
|
|||||||
onReload?: () => void
|
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) {
|
export function ResultsPane({ result, isExecuting, onReload }: ResultsPaneProps) {
|
||||||
const [activeTab, setActiveTab] = useState<Tab>('ui')
|
const [activeTab, setActiveTab] = useState<Tab>('ui')
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false)
|
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 hasUI = result?.htmlContent != null
|
||||||
const hasText = result?.textContent != 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(() => {
|
useEffect(() => {
|
||||||
if (hasUI) {
|
if (hasUI) {
|
||||||
setActiveTab('ui')
|
setActiveTab('ui')
|
||||||
@ -47,14 +93,37 @@ export function ResultsPane({ result, isExecuting, onReload }: ResultsPaneProps)
|
|||||||
Text
|
Text
|
||||||
<span className={`tab-badge ${hasText ? 'active' : ''}`} />
|
<span className={`tab-badge ${hasText ? 'active' : ''}`} />
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
className={`tab ${activeTab === 'events' ? 'active' : ''}`}
|
||||||
|
onClick={() => setActiveTab('events')}
|
||||||
|
>
|
||||||
|
Events
|
||||||
|
{events.length > 0 && <span className="tab-count">{events.length}</span>}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="results-actions">
|
<div className="results-actions">
|
||||||
{result?.timestamp && (
|
{result && !result.isError && (
|
||||||
<span className="timestamp">
|
<div className="metrics">
|
||||||
<Clock size={12} />
|
<span className="metric" title="Execution time">
|
||||||
{result.timestamp.toLocaleTimeString()}
|
<Timer size={12} />
|
||||||
|
{result.executionTime}ms
|
||||||
</span>
|
</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 && (
|
{onReload && (
|
||||||
<Button
|
<Button
|
||||||
@ -94,6 +163,7 @@ export function ResultsPane({ result, isExecuting, onReload }: ResultsPaneProps)
|
|||||||
) : activeTab === 'ui' ? (
|
) : activeTab === 'ui' ? (
|
||||||
hasUI ? (
|
hasUI ? (
|
||||||
<iframe
|
<iframe
|
||||||
|
ref={iframeRef}
|
||||||
className="ui-frame"
|
className="ui-frame"
|
||||||
srcDoc={result.htmlContent!}
|
srcDoc={result.htmlContent!}
|
||||||
title="Tool UI Output"
|
title="Tool UI Output"
|
||||||
@ -106,10 +176,40 @@ export function ResultsPane({ result, isExecuting, onReload }: ResultsPaneProps)
|
|||||||
<span>This tool may only return text content</span>
|
<span>This tool may only return text content</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
) : (
|
) : activeTab === 'text' ? (
|
||||||
<div className="text-output">
|
<div className="text-output">
|
||||||
<pre>{result.textContent || 'No text content'}</pre>
|
<pre>{result.textContent || 'No text content'}</pre>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -16,6 +16,8 @@ interface UseMCPReturn {
|
|||||||
textContent: string
|
textContent: string
|
||||||
htmlContent: string | null
|
htmlContent: string | null
|
||||||
isError: boolean
|
isError: boolean
|
||||||
|
executionTime: number
|
||||||
|
bundleSize: number | null
|
||||||
}>
|
}>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -195,6 +197,8 @@ export function useMCP(): UseMCPReturn {
|
|||||||
throw new Error('Not connected')
|
throw new Error('Not connected')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const startTime = performance.now()
|
||||||
|
|
||||||
// Build headers - only include session ID if present (stateful server)
|
// Build headers - only include session ID if present (stateful server)
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@ -216,6 +220,8 @@ export function useMCP(): UseMCPReturn {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const text = await response.text()
|
const text = await response.text()
|
||||||
|
const executionTime = Math.round(performance.now() - startTime)
|
||||||
|
|
||||||
const result = parseSSE(text) as {
|
const result = parseSSE(text) as {
|
||||||
result?: {
|
result?: {
|
||||||
content: Array<{
|
content: Array<{
|
||||||
@ -232,6 +238,8 @@ export function useMCP(): UseMCPReturn {
|
|||||||
textContent: result.error.message,
|
textContent: result.error.message,
|
||||||
htmlContent: null,
|
htmlContent: null,
|
||||||
isError: true,
|
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 {
|
return {
|
||||||
textContent,
|
textContent,
|
||||||
htmlContent,
|
htmlContent,
|
||||||
isError: false,
|
isError: false,
|
||||||
|
executionTime,
|
||||||
|
bundleSize,
|
||||||
}
|
}
|
||||||
}, [isConnected, sessionId])
|
}, [isConnected, sessionId])
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user