@roj-ai/debug 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/components/debug/DebugContext.d.ts +10 -0
  2. package/dist/components/debug/DebugNavigation.d.ts +29 -0
  3. package/dist/components/debug/DebugShell.d.ts +18 -0
  4. package/dist/components/debug/LLMCallDetail.d.ts +7 -0
  5. package/dist/components/debug/TimelineDetailInspector.d.ts +6 -0
  6. package/dist/components/debug/communication/CommunicationDiagram.d.ts +9 -0
  7. package/dist/components/debug/communication/DiagramHeader.d.ts +7 -0
  8. package/dist/components/debug/communication/ParticipantLane.d.ts +7 -0
  9. package/dist/components/debug/communication/TimeAxis.d.ts +9 -0
  10. package/dist/components/debug/communication/elements/IdleGap.d.ts +9 -0
  11. package/dist/components/debug/communication/elements/LLMBlock.d.ts +9 -0
  12. package/dist/components/debug/communication/elements/MessageArrow.d.ts +10 -0
  13. package/dist/components/debug/communication/elements/ToolBlock.d.ts +9 -0
  14. package/dist/components/debug/communication/hooks/useDiagramData.d.ts +12 -0
  15. package/dist/components/debug/communication/hooks/useTimeCompression.d.ts +7 -0
  16. package/dist/components/debug/communication/hooks/useZoomPan.d.ts +11 -0
  17. package/dist/components/debug/communication/popovers/ElementPopover.d.ts +8 -0
  18. package/dist/components/debug/communication/types.d.ts +136 -0
  19. package/dist/components/debug/index.d.ts +11 -0
  20. package/dist/components/debug/pages/AgentDetailPage.d.ts +3 -0
  21. package/dist/components/debug/pages/AgentsPage.d.ts +1 -0
  22. package/dist/components/debug/pages/CommunicationPage.d.ts +1 -0
  23. package/dist/components/debug/pages/DashboardPage.d.ts +1 -0
  24. package/dist/components/debug/pages/EventsPage.d.ts +1 -0
  25. package/dist/components/debug/pages/FilesPage.d.ts +1 -0
  26. package/dist/components/debug/pages/LLMCallPage.d.ts +1 -0
  27. package/dist/components/debug/pages/LLMCallsPage.d.ts +1 -0
  28. package/dist/components/debug/pages/LogsPage.d.ts +1 -0
  29. package/dist/components/debug/pages/MailboxPage.d.ts +1 -0
  30. package/dist/components/debug/pages/ServicesPage.d.ts +1 -0
  31. package/dist/components/debug/pages/TimelinePage.d.ts +1 -0
  32. package/dist/components/debug/pages/UserChatPage.d.ts +1 -0
  33. package/dist/components/debug/pages/index.d.ts +13 -0
  34. package/dist/index.d.ts +9 -0
  35. package/dist/lib/domain-utils.d.ts +7 -0
  36. package/dist/providers/EventPollingProvider.d.ts +27 -0
  37. package/dist/stores/event-store.d.ts +93 -0
  38. package/dist/utils/format.d.ts +1 -0
  39. package/package.json +43 -0
  40. package/src/components/debug/DebugContext.tsx +18 -0
  41. package/src/components/debug/DebugNavigation.tsx +55 -0
  42. package/src/components/debug/DebugShell.tsx +321 -0
  43. package/src/components/debug/LLMCallDetail.tsx +740 -0
  44. package/src/components/debug/TimelineDetailInspector.tsx +204 -0
  45. package/src/components/debug/communication/CommunicationDiagram.tsx +260 -0
  46. package/src/components/debug/communication/DiagramHeader.tsx +113 -0
  47. package/src/components/debug/communication/ParticipantLane.tsx +60 -0
  48. package/src/components/debug/communication/TimeAxis.tsx +106 -0
  49. package/src/components/debug/communication/elements/IdleGap.tsx +90 -0
  50. package/src/components/debug/communication/elements/LLMBlock.tsx +107 -0
  51. package/src/components/debug/communication/elements/MessageArrow.tsx +119 -0
  52. package/src/components/debug/communication/elements/ToolBlock.tsx +99 -0
  53. package/src/components/debug/communication/hooks/useDiagramData.ts +294 -0
  54. package/src/components/debug/communication/hooks/useTimeCompression.ts +140 -0
  55. package/src/components/debug/communication/hooks/useZoomPan.ts +87 -0
  56. package/src/components/debug/communication/popovers/ElementPopover.tsx +158 -0
  57. package/src/components/debug/communication/types.ts +180 -0
  58. package/src/components/debug/index.ts +37 -0
  59. package/src/components/debug/pages/AgentDetailPage.tsx +1295 -0
  60. package/src/components/debug/pages/AgentsPage.tsx +297 -0
  61. package/src/components/debug/pages/CommunicationPage.tsx +89 -0
  62. package/src/components/debug/pages/DashboardPage.tsx +1504 -0
  63. package/src/components/debug/pages/EventsPage.tsx +276 -0
  64. package/src/components/debug/pages/FilesPage.tsx +366 -0
  65. package/src/components/debug/pages/LLMCallPage.tsx +32 -0
  66. package/src/components/debug/pages/LLMCallsPage.tsx +473 -0
  67. package/src/components/debug/pages/LogsPage.tsx +199 -0
  68. package/src/components/debug/pages/MailboxPage.tsx +232 -0
  69. package/src/components/debug/pages/ServicesPage.tsx +193 -0
  70. package/src/components/debug/pages/TimelinePage.tsx +569 -0
  71. package/src/components/debug/pages/UserChatPage.tsx +250 -0
  72. package/src/components/debug/pages/index.ts +13 -0
  73. package/src/index.ts +55 -0
  74. package/src/lib/domain-utils.ts +12 -0
  75. package/src/providers/EventPollingProvider.tsx +60 -0
  76. package/src/stores/event-store.ts +497 -0
  77. package/src/utils/format.ts +8 -0
@@ -0,0 +1,204 @@
1
+ import type { TimelineItem } from '@roj-ai/shared'
2
+ import { LLMCallDetail } from './LLMCallDetail'
3
+
4
+ export function TimelineDetailInspector({
5
+ sessionId,
6
+ item,
7
+ onNavigate,
8
+ }: {
9
+ sessionId: string
10
+ item: TimelineItem
11
+ onNavigate?: (path: string) => void
12
+ }) {
13
+ const agentLink = (agentId: string, name: string) =>
14
+ onNavigate
15
+ ? (
16
+ <button type="button" onClick={() => onNavigate(`agents/${agentId}`)} className="text-violet-600 hover:underline cursor-pointer">
17
+ {name}
18
+ </button>
19
+ )
20
+ : <span>{name}</span>
21
+
22
+ // LLM with detailed call log
23
+ if (item.type === 'llm' && item.llmCallId) {
24
+ return (
25
+ <div className="space-y-4">
26
+ <div className="flex items-center gap-3">
27
+ <TypeBadge type={item.type} />
28
+ <span className="font-medium">{item.model?.split('/').pop() ?? 'LLM Call'}</span>
29
+ <StatusBadge status={item.status} />
30
+ </div>
31
+ <LLMCallDetail sessionId={sessionId} callId={item.llmCallId} />
32
+ </div>
33
+ )
34
+ }
35
+
36
+ // LLM without detailed log
37
+ if (item.type === 'llm') {
38
+ return (
39
+ <div className="space-y-4">
40
+ <div className="flex items-center gap-3">
41
+ <TypeBadge type={item.type} />
42
+ <span className="font-medium">{item.model?.split('/').pop() ?? 'LLM Call'}</span>
43
+ <StatusBadge status={item.status} />
44
+ </div>
45
+
46
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm">
47
+ <MetricCard label="Agent">{agentLink(item.agentId, item.agentName)}</MetricCard>
48
+ {item.model && <MetricCard label="Model"><span className="font-mono text-xs">{item.model}</span></MetricCard>}
49
+ {item.durationMs !== undefined && <MetricCard label="Duration">{(item.durationMs / 1000).toFixed(2)}s</MetricCard>}
50
+ {item.cost !== undefined && <MetricCard label="Cost"><span className="text-green-600">${item.cost.toFixed(6)}</span></MetricCard>}
51
+ </div>
52
+
53
+ {(item.promptTokens !== undefined || item.completionTokens !== undefined) && (
54
+ <div className="bg-violet-50 border border-violet-200 rounded-md p-4">
55
+ <h3 className="font-medium text-violet-800 mb-3">Token Usage</h3>
56
+ <div className="grid grid-cols-3 gap-4 text-sm">
57
+ <div>
58
+ <div className="text-xs text-violet-600 mb-1">Prompt</div>
59
+ <div className="font-mono font-medium text-green-600">{(item.promptTokens ?? 0).toLocaleString()}</div>
60
+ </div>
61
+ <div>
62
+ <div className="text-xs text-violet-600 mb-1">Completion</div>
63
+ <div className="font-mono font-medium text-violet-600">{(item.completionTokens ?? 0).toLocaleString()}</div>
64
+ </div>
65
+ <div>
66
+ <div className="text-xs text-violet-600 mb-1">Total</div>
67
+ <div className="font-mono font-medium">{((item.promptTokens ?? 0) + (item.completionTokens ?? 0)).toLocaleString()}</div>
68
+ </div>
69
+ </div>
70
+ </div>
71
+ )}
72
+
73
+ <div className="text-xs text-slate-500 space-y-1">
74
+ <div>Started: {new Date(item.startedAt).toLocaleString()}</div>
75
+ {item.completedAt && <div>Completed: {new Date(item.completedAt).toLocaleString()}</div>}
76
+ </div>
77
+
78
+ {item.error && (
79
+ <div className="bg-red-50 border border-red-200 rounded-md p-4">
80
+ <h3 className="font-semibold text-red-800 mb-2">Error</h3>
81
+ <div className="text-red-700 font-mono text-sm whitespace-pre-wrap">{item.error}</div>
82
+ </div>
83
+ )}
84
+ </div>
85
+ )
86
+ }
87
+
88
+ // Tool
89
+ if (item.type === 'tool') {
90
+ return (
91
+ <div className="space-y-4">
92
+ <div className="flex items-center gap-3">
93
+ <TypeBadge type={item.type} />
94
+ <span className="font-medium font-mono">{item.toolName ?? 'Tool'}</span>
95
+ <StatusBadge status={item.status} />
96
+ </div>
97
+
98
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm">
99
+ <MetricCard label="Agent">{agentLink(item.agentId, item.agentName)}</MetricCard>
100
+ {item.durationMs !== undefined && <MetricCard label="Duration">{(item.durationMs / 1000).toFixed(2)}s</MetricCard>}
101
+ <MetricCard label="Started">{new Date(item.startedAt).toLocaleTimeString()}</MetricCard>
102
+ {item.completedAt && <MetricCard label="Completed">{new Date(item.completedAt).toLocaleTimeString()}</MetricCard>}
103
+ </div>
104
+
105
+ {item.toolInput !== undefined && (
106
+ <div className="border border-slate-200 rounded-md overflow-hidden">
107
+ <div className="px-4 py-2 bg-slate-50 border-b border-slate-200">
108
+ <h3 className="text-sm font-medium text-slate-700">Input</h3>
109
+ </div>
110
+ <div className="p-4 max-h-64 overflow-auto">
111
+ <pre className="font-mono text-xs whitespace-pre-wrap break-words">{JSON.stringify(item.toolInput, null, 2)}</pre>
112
+ </div>
113
+ </div>
114
+ )}
115
+
116
+ {item.toolResult !== undefined && (
117
+ <div className="border border-emerald-200 rounded-md overflow-hidden">
118
+ <div className="px-4 py-2 bg-emerald-50 border-b border-emerald-200">
119
+ <h3 className="text-sm font-medium text-emerald-700">Result</h3>
120
+ </div>
121
+ <div className="p-4 max-h-64 overflow-auto">
122
+ <pre className="font-mono text-xs whitespace-pre-wrap break-words">{JSON.stringify(item.toolResult, null, 2)}</pre>
123
+ </div>
124
+ </div>
125
+ )}
126
+
127
+ {item.error && (
128
+ <div className="bg-red-50 border border-red-200 rounded-md p-4">
129
+ <h3 className="font-semibold text-red-800 mb-2">Error</h3>
130
+ <div className="text-red-700 font-mono text-sm whitespace-pre-wrap">{item.error}</div>
131
+ </div>
132
+ )}
133
+ </div>
134
+ )
135
+ }
136
+
137
+ // Compaction
138
+ if (item.type === 'compaction') {
139
+ return (
140
+ <div className="space-y-4">
141
+ <div className="flex items-center gap-3">
142
+ <TypeBadge type={item.type} />
143
+ <span className="font-medium">Context Compaction</span>
144
+ </div>
145
+
146
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm">
147
+ <MetricCard label="Agent">{agentLink(item.agentId, item.agentName)}</MetricCard>
148
+ {item.originalTokens !== undefined && <MetricCard label="Original Tokens">{item.originalTokens.toLocaleString()}</MetricCard>}
149
+ {item.compactedTokens !== undefined && <MetricCard label="Compacted Tokens">{item.compactedTokens.toLocaleString()}</MetricCard>}
150
+ {item.messagesRemoved !== undefined && <MetricCard label="Messages Removed">{item.messagesRemoved}</MetricCard>}
151
+ </div>
152
+
153
+ {item.originalTokens !== undefined && item.compactedTokens !== undefined && (
154
+ <div className="bg-amber-50 border border-amber-200 rounded-md p-4">
155
+ <h3 className="font-medium text-amber-800 mb-3">Compression</h3>
156
+ <div className="flex items-center gap-4">
157
+ <div className="text-2xl font-bold text-amber-600">
158
+ {Math.round((1 - item.compactedTokens / item.originalTokens) * 100)}%
159
+ </div>
160
+ <div className="text-sm text-amber-700">
161
+ reduced ({item.originalTokens.toLocaleString()} &rarr; {item.compactedTokens.toLocaleString()} tokens)
162
+ </div>
163
+ </div>
164
+ </div>
165
+ )}
166
+
167
+ <div className="text-xs text-slate-500">
168
+ Compacted at: {new Date(item.startedAt).toLocaleString()}
169
+ </div>
170
+ </div>
171
+ )
172
+ }
173
+
174
+ return <div className="text-slate-500 text-sm">Unknown item type</div>
175
+ }
176
+
177
+ function StatusBadge({ status }: { status: string }) {
178
+ if (status === 'error') return <span className="text-xs bg-red-100 text-red-700 px-2 py-0.5 rounded-full font-medium">Error</span>
179
+ if (status === 'running') return <span className="text-xs bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full font-medium animate-pulse">Running</span>
180
+ return null
181
+ }
182
+
183
+ function TypeBadge({ type }: { type: TimelineItem['type'] }) {
184
+ const config: Record<TimelineItem['type'], string> = {
185
+ llm: 'bg-violet-100 text-violet-700',
186
+ tool: 'bg-emerald-100 text-emerald-700',
187
+ compaction: 'bg-amber-100 text-amber-700',
188
+ }
189
+ const labels: Record<TimelineItem['type'], string> = {
190
+ llm: 'LLM',
191
+ tool: 'Tool',
192
+ compaction: 'Compact',
193
+ }
194
+ return <span className={`text-xs px-1.5 py-0.5 rounded-full font-medium ${config[type]}`}>{labels[type]}</span>
195
+ }
196
+
197
+ function MetricCard({ label, children }: { label: string; children: React.ReactNode }) {
198
+ return (
199
+ <div className="bg-slate-50 rounded-md p-3">
200
+ <div className="text-xs text-slate-500 mb-1">{label}</div>
201
+ <div className="font-medium text-slate-900">{children}</div>
202
+ </div>
203
+ )
204
+ }
@@ -0,0 +1,260 @@
1
+ import { useState } from 'react'
2
+ import { DiagramHeader } from './DiagramHeader'
3
+ import { IdleGap } from './elements/IdleGap'
4
+ import { LLMBlock } from './elements/LLMBlock'
5
+ import { MessageArrow } from './elements/MessageArrow'
6
+ import { ToolBlock } from './elements/ToolBlock'
7
+ import { useZoomPan } from './hooks/useZoomPan'
8
+ import { ParticipantLane } from './ParticipantLane'
9
+ import { ElementPopover } from './popovers/ElementPopover'
10
+ import { TimeAxis } from './TimeAxis'
11
+ import type { DiagramData, DiagramLLMBlock, DiagramMessage, DiagramToolBlock, PopoverState, TimeSegment } from './types'
12
+ import { LAYOUT } from './types'
13
+
14
+ interface CommunicationDiagramProps {
15
+ data: DiagramData & {
16
+ timestampToY: (timestamp: number) => number
17
+ formatIdleDuration: (ms: number) => string
18
+ }
19
+ }
20
+
21
+ export function CommunicationDiagram({ data }: CommunicationDiagramProps) {
22
+ const [popover, setPopover] = useState<PopoverState>({ element: null, x: 0, y: 0 })
23
+ const [showLabels, setShowLabels] = useState(false)
24
+
25
+ const { state: zoomPan, containerRef, zoomIn, zoomOut, resetZoom, toggleAutoScroll, handleScroll, handleWheel } = useZoomPan(data.totalHeight)
26
+
27
+ // Calculate dimensions
28
+ const totalWidth = LAYOUT.timeAxisWidth + data.participants.length * (LAYOUT.participantWidth + LAYOUT.participantGap) + LAYOUT.padding
29
+ const scaledHeight = data.totalHeight * zoomPan.zoom
30
+ const scaledWidth = totalWidth * zoomPan.zoom
31
+
32
+ // Calculate cumulative Y positions for idle gaps
33
+ const idleGapPositions: Array<{ segment: TimeSegment; yPosition: number }> = []
34
+ let cumulativeY = 0
35
+ for (const segment of data.timeSegments) {
36
+ if (segment.type === 'idle') {
37
+ idleGapPositions.push({ segment, yPosition: cumulativeY })
38
+ }
39
+ cumulativeY += segment.displayHeight
40
+ }
41
+
42
+ const handleMessageHover = (message: DiagramMessage | null, x: number, y: number) => {
43
+ if (message) {
44
+ setPopover({ element: { type: 'message', data: message }, x, y })
45
+ } else {
46
+ setPopover({ element: null, x: 0, y: 0 })
47
+ }
48
+ }
49
+
50
+ const handleLLMHover = (block: DiagramLLMBlock | null, x: number, y: number) => {
51
+ if (block) {
52
+ setPopover({ element: { type: 'llm', data: block }, x, y })
53
+ } else {
54
+ setPopover({ element: null, x: 0, y: 0 })
55
+ }
56
+ }
57
+
58
+ const handleToolHover = (block: DiagramToolBlock | null, x: number, y: number) => {
59
+ if (block) {
60
+ setPopover({ element: { type: 'tool', data: block }, x, y })
61
+ } else {
62
+ setPopover({ element: null, x: 0, y: 0 })
63
+ }
64
+ }
65
+
66
+ return (
67
+ <div className="h-full flex flex-col">
68
+ {/* Toolbar */}
69
+ <div className="flex items-center gap-3 px-3 py-2 bg-slate-50/80 border-b border-slate-100 shrink-0">
70
+ {/* Zoom controls */}
71
+ <div className="flex items-center gap-1 bg-white rounded-md border border-slate-200 p-0.5">
72
+ <button
73
+ onClick={zoomOut}
74
+ className="w-7 h-7 flex items-center justify-center text-slate-500 hover:bg-slate-100 rounded-md transition-colors"
75
+ title="Zoom Out (Ctrl+-)"
76
+ >
77
+ <MinusIcon />
78
+ </button>
79
+ <span className="text-xs text-slate-600 w-12 text-center font-medium">
80
+ {Math.round(zoomPan.zoom * 100)}%
81
+ </span>
82
+ <button
83
+ onClick={zoomIn}
84
+ className="w-7 h-7 flex items-center justify-center text-slate-500 hover:bg-slate-100 rounded-md transition-colors"
85
+ title="Zoom In (Ctrl++)"
86
+ >
87
+ <PlusIcon />
88
+ </button>
89
+ </div>
90
+
91
+ <button
92
+ onClick={resetZoom}
93
+ className="px-2 py-1 text-xs text-slate-500 hover:text-slate-700 hover:bg-white rounded border border-transparent hover:border-slate-200 transition-colors"
94
+ >
95
+ Reset
96
+ </button>
97
+
98
+ <div className="h-4 w-px bg-slate-200" />
99
+
100
+ <button
101
+ onClick={toggleAutoScroll}
102
+ className={`flex items-center gap-1.5 px-2.5 py-1 text-xs rounded-md transition-colors ${
103
+ zoomPan.autoScroll
104
+ ? 'bg-violet-50 text-violet-600 border border-violet-200'
105
+ : 'bg-white text-slate-500 border border-slate-200 hover:border-slate-300'
106
+ }`}
107
+ >
108
+ <AutoScrollIcon active={zoomPan.autoScroll} />
109
+ Auto-scroll
110
+ </button>
111
+
112
+ <div className="h-4 w-px bg-slate-200" />
113
+
114
+ {/* Show labels toggle */}
115
+ <label className="flex items-center gap-2 cursor-pointer select-none">
116
+ <input
117
+ type="checkbox"
118
+ checked={showLabels}
119
+ onChange={(e) => setShowLabels(e.target.checked)}
120
+ className="w-3.5 h-3.5 rounded border-slate-300 text-violet-500 focus:ring-violet-500 focus:ring-offset-0"
121
+ />
122
+ <span className="text-xs text-slate-600">Show message text</span>
123
+ </label>
124
+
125
+ <div className="flex-1" />
126
+
127
+ {/* Legend */}
128
+ <div className="flex items-center gap-4 text-[10px] text-slate-400">
129
+ <span className="flex items-center gap-1.5">
130
+ <span className="w-4 h-px bg-blue-300" />
131
+ Messages
132
+ </span>
133
+ <span className="flex items-center gap-1.5">
134
+ <span className="w-3 h-3 bg-violet-50 border border-violet-200 rounded" />
135
+ LLM
136
+ </span>
137
+ <span className="flex items-center gap-1.5">
138
+ <span className="w-3 h-3 bg-teal-50 border border-teal-200 rounded" />
139
+ Tools
140
+ </span>
141
+ </div>
142
+ </div>
143
+
144
+ {/* Diagram container */}
145
+ <div className="flex-1 flex flex-col min-h-0 overflow-hidden">
146
+ {/* Header (sticky) - scales with zoom */}
147
+ <DiagramHeader participants={data.participants} zoom={zoomPan.zoom} />
148
+
149
+ {/* Scrollable SVG area */}
150
+ <div
151
+ ref={containerRef}
152
+ className="flex-1 overflow-auto bg-white"
153
+ onScroll={handleScroll}
154
+ onWheel={handleWheel}
155
+ >
156
+ <svg
157
+ width={scaledWidth}
158
+ height={scaledHeight + LAYOUT.padding * 2}
159
+ >
160
+ <g transform={`scale(${zoomPan.zoom})`}>
161
+ <g transform={`translate(0, ${LAYOUT.padding})`}>
162
+ {/* Background grid lines for participant lanes */}
163
+ {data.participants.map((participant) => (
164
+ <ParticipantLane
165
+ key={participant.id}
166
+ participant={participant}
167
+ totalHeight={data.totalHeight}
168
+ />
169
+ ))}
170
+
171
+ {/* Time axis */}
172
+ <TimeAxis
173
+ segments={data.timeSegments}
174
+ sessionStartTime={data.sessionStartTime}
175
+ timestampToY={data.timestampToY}
176
+ totalHeight={data.totalHeight}
177
+ />
178
+
179
+ {/* Idle gaps */}
180
+ {idleGapPositions.map((item, idx) => (
181
+ <IdleGap
182
+ key={idx}
183
+ segment={item.segment}
184
+ participants={data.participants}
185
+ yPosition={item.yPosition}
186
+ formatDuration={data.formatIdleDuration}
187
+ />
188
+ ))}
189
+
190
+ {/* LLM blocks */}
191
+ {data.llmBlocks.map((block) => (
192
+ <LLMBlock
193
+ key={block.id}
194
+ block={block}
195
+ participants={data.participants}
196
+ onHover={handleLLMHover}
197
+ />
198
+ ))}
199
+
200
+ {/* Tool blocks */}
201
+ {data.toolBlocks.map((block) => (
202
+ <ToolBlock
203
+ key={block.id}
204
+ block={block}
205
+ participants={data.participants}
206
+ onHover={handleToolHover}
207
+ />
208
+ ))}
209
+
210
+ {/* Messages (on top) */}
211
+ {data.messages.map((message) => (
212
+ <MessageArrow
213
+ key={message.id}
214
+ message={message}
215
+ participants={data.participants}
216
+ showLabel={showLabels}
217
+ onHover={handleMessageHover}
218
+ />
219
+ ))}
220
+ </g>
221
+ </g>
222
+ </svg>
223
+ </div>
224
+ </div>
225
+
226
+ {/* Popover */}
227
+ {popover.element && (
228
+ <ElementPopover
229
+ element={popover.element}
230
+ x={popover.x}
231
+ y={popover.y}
232
+ />
233
+ )}
234
+ </div>
235
+ )
236
+ }
237
+
238
+ function MinusIcon() {
239
+ return (
240
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
241
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 12H4" />
242
+ </svg>
243
+ )
244
+ }
245
+
246
+ function PlusIcon() {
247
+ return (
248
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
249
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
250
+ </svg>
251
+ )
252
+ }
253
+
254
+ function AutoScrollIcon({ active }: { active: boolean }) {
255
+ return (
256
+ <svg className={`w-3.5 h-3.5 ${active ? 'text-violet-500' : 'text-slate-400'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
257
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" />
258
+ </svg>
259
+ )
260
+ }
@@ -0,0 +1,113 @@
1
+ import { DebugLink } from '../DebugNavigation'
2
+ import type { DiagramParticipant } from './types'
3
+ import { COLORS, LAYOUT } from './types'
4
+
5
+ interface DiagramHeaderProps {
6
+ participants: DiagramParticipant[]
7
+ zoom: number
8
+ }
9
+
10
+ export function DiagramHeader({ participants, zoom }: DiagramHeaderProps) {
11
+ const scaledTimeAxisWidth = LAYOUT.timeAxisWidth * zoom
12
+ const scaledParticipantWidth = LAYOUT.participantWidth * zoom
13
+ const scaledParticipantGap = LAYOUT.participantGap * zoom
14
+
15
+ return (
16
+ <div
17
+ className="sticky top-0 z-10 bg-white border-b border-slate-200 flex shrink-0 overflow-hidden"
18
+ style={{ height: LAYOUT.headerHeight }}
19
+ >
20
+ {/* Time axis header */}
21
+ <div
22
+ className="flex items-center justify-center text-[10px] font-medium text-slate-400 uppercase tracking-wider border-r border-slate-200 shrink-0 bg-slate-50"
23
+ style={{ width: scaledTimeAxisWidth }}
24
+ >
25
+ Time
26
+ </div>
27
+
28
+ {/* Participant headers */}
29
+ {participants.map((participant, idx) => {
30
+ const isOdd = idx % 2 === 1
31
+ return (
32
+ <div
33
+ key={participant.id}
34
+ className={`flex flex-col items-center justify-center shrink-0 py-2 border-l border-slate-200 ${isOdd ? 'bg-slate-50/80' : 'bg-white'}`}
35
+ style={{
36
+ width: scaledParticipantWidth + scaledParticipantGap,
37
+ paddingLeft: scaledParticipantGap / 2,
38
+ paddingRight: scaledParticipantGap / 2,
39
+ }}
40
+ >
41
+ {participant.id === 'user'
42
+ ? (
43
+ <div className="flex items-center gap-1.5">
44
+ <UserIcon />
45
+ <span className={`text-xs font-semibold ${COLORS.participant.user}`}>
46
+ {participant.name}
47
+ </span>
48
+ </div>
49
+ )
50
+ : (
51
+ <DebugLink
52
+ to={`agents/${participant.id}`}
53
+ className={`text-xs font-semibold hover:underline truncate max-w-full ${COLORS.participant[participant.role]}`}
54
+ >
55
+ {participant.name}
56
+ </DebugLink>
57
+ )}
58
+
59
+ <div className="flex items-center gap-1.5 mt-1">
60
+ <RoleBadge role={participant.role} />
61
+ <StatusIndicator status={participant.status} />
62
+ </div>
63
+ </div>
64
+ )
65
+ })}
66
+ </div>
67
+ )
68
+ }
69
+
70
+ function UserIcon() {
71
+ return (
72
+ <svg className="w-3.5 h-3.5 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
73
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
74
+ </svg>
75
+ )
76
+ }
77
+
78
+ function RoleBadge({ role }: { role: DiagramParticipant['role'] }) {
79
+ const config: Record<DiagramParticipant['role'], { bg: string; text: string; label: string }> = {
80
+ user: { bg: 'bg-blue-100', text: 'text-blue-700', label: 'User' },
81
+ communicator: { bg: 'bg-emerald-100', text: 'text-emerald-700', label: 'Comm' },
82
+ orchestrator: { bg: 'bg-violet-100', text: 'text-violet-700', label: 'Orch' },
83
+ worker: { bg: 'bg-slate-100', text: 'text-slate-700', label: 'Worker' },
84
+ }
85
+
86
+ const { bg, text, label } = config[role]
87
+
88
+ return (
89
+ <span className={`text-[9px] px-1.5 py-0.5 rounded-full ${bg} ${text} font-medium`}>
90
+ {label}
91
+ </span>
92
+ )
93
+ }
94
+
95
+ function StatusIndicator({ status }: { status: DiagramParticipant['status'] }) {
96
+ const config: Record<DiagramParticipant['status'], { bg: string; ring?: string; animate?: boolean }> = {
97
+ idle: { bg: 'bg-slate-300' },
98
+ thinking: { bg: 'bg-amber-400', ring: 'ring-amber-200', animate: true },
99
+ responding: { bg: 'bg-blue-400', ring: 'ring-blue-200' },
100
+ waiting_for_user: { bg: 'bg-purple-400', ring: 'ring-purple-200' },
101
+ error: { bg: 'bg-red-400', ring: 'ring-red-200' },
102
+ paused: { bg: 'bg-amber-400', ring: 'ring-amber-200' },
103
+ }
104
+
105
+ const { bg, ring, animate } = config[status]
106
+
107
+ return (
108
+ <span
109
+ className={`w-1.5 h-1.5 rounded-full ${bg} ${ring ? `ring-2 ${ring}` : ''} ${animate ? 'animate-pulse' : ''}`}
110
+ title={status.replace(/_/g, ' ')}
111
+ />
112
+ )
113
+ }
@@ -0,0 +1,60 @@
1
+ import type { DiagramParticipant } from './types'
2
+ import { LAYOUT } from './types'
3
+
4
+ interface ParticipantLaneProps {
5
+ participant: DiagramParticipant
6
+ totalHeight: number
7
+ }
8
+
9
+ export function ParticipantLane({ participant, totalHeight }: ParticipantLaneProps) {
10
+ const x = LAYOUT.timeAxisWidth + participant.columnIndex * (LAYOUT.participantWidth + LAYOUT.participantGap)
11
+ const centerX = x + LAYOUT.participantWidth / 2
12
+ const isOdd = participant.columnIndex % 2 === 1
13
+ const columnWidth = LAYOUT.participantWidth + LAYOUT.participantGap
14
+
15
+ return (
16
+ <g>
17
+ {/* Column background - alternating */}
18
+ <rect
19
+ x={x - LAYOUT.participantGap / 2}
20
+ y={0}
21
+ width={columnWidth}
22
+ height={totalHeight}
23
+ className={isOdd ? 'fill-slate-50/80' : 'fill-white'}
24
+ />
25
+
26
+ {/* Column border - left side */}
27
+ <line
28
+ x1={x - LAYOUT.participantGap / 2}
29
+ y1={0}
30
+ x2={x - LAYOUT.participantGap / 2}
31
+ y2={totalHeight}
32
+ className="stroke-slate-200"
33
+ strokeWidth={1}
34
+ />
35
+
36
+ {/* Column border - right side (only for last column) */}
37
+ {participant.columnIndex === 0 && (
38
+ <line
39
+ x1={x - LAYOUT.participantGap / 2 + columnWidth}
40
+ y1={0}
41
+ x2={x - LAYOUT.participantGap / 2 + columnWidth}
42
+ y2={totalHeight}
43
+ className="stroke-slate-200"
44
+ strokeWidth={1}
45
+ />
46
+ )}
47
+
48
+ {/* Center line - dashed, subtle */}
49
+ <line
50
+ x1={centerX}
51
+ y1={0}
52
+ x2={centerX}
53
+ y2={totalHeight}
54
+ className="stroke-slate-300"
55
+ strokeWidth={1}
56
+ strokeDasharray="2 8"
57
+ />
58
+ </g>
59
+ )
60
+ }