@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,106 @@
1
+ import type { TimeSegment } from './types'
2
+ import { LAYOUT } from './types'
3
+
4
+ interface TimeAxisProps {
5
+ segments: TimeSegment[]
6
+ sessionStartTime: number
7
+ timestampToY: (timestamp: number) => number
8
+ totalHeight: number
9
+ }
10
+
11
+ export function TimeAxis({ segments, sessionStartTime, timestampToY, totalHeight }: TimeAxisProps) {
12
+ // Generate time markers every ~80px on active segments
13
+ const markers: Array<{ timestamp: number; y: number; label: string }> = []
14
+
15
+ for (const segment of segments) {
16
+ if (segment.type === 'idle') continue
17
+
18
+ const interval = calculateInterval(segment.actualDuration)
19
+ let current = segment.startTime
20
+
21
+ while (current <= segment.endTime) {
22
+ const y = timestampToY(current)
23
+ // Only add if not too close to previous marker
24
+ const lastMarker = markers[markers.length - 1]
25
+ if (!lastMarker || y - lastMarker.y > 40) {
26
+ markers.push({
27
+ timestamp: current,
28
+ y,
29
+ label: formatTime(current, sessionStartTime),
30
+ })
31
+ }
32
+ current += interval
33
+ }
34
+ }
35
+
36
+ return (
37
+ <g className="time-axis">
38
+ {/* Vertical axis line */}
39
+ <line
40
+ x1={LAYOUT.timeAxisWidth - 1}
41
+ y1={0}
42
+ x2={LAYOUT.timeAxisWidth - 1}
43
+ y2={totalHeight}
44
+ className="stroke-slate-100"
45
+ strokeWidth={1}
46
+ />
47
+
48
+ {/* Time markers */}
49
+ {markers.map((marker, idx) => (
50
+ <g key={idx}>
51
+ {/* Tick mark */}
52
+ <line
53
+ x1={LAYOUT.timeAxisWidth - 6}
54
+ y1={marker.y}
55
+ x2={LAYOUT.timeAxisWidth - 1}
56
+ y2={marker.y}
57
+ className="stroke-slate-200"
58
+ strokeWidth={1}
59
+ />
60
+
61
+ {/* Time label */}
62
+ <text
63
+ x={LAYOUT.timeAxisWidth - 10}
64
+ y={marker.y}
65
+ textAnchor="end"
66
+ dominantBaseline="middle"
67
+ className="text-[9px] text-slate-400 fill-current font-mono"
68
+ >
69
+ {marker.label}
70
+ </text>
71
+
72
+ {/* Subtle horizontal guide line */}
73
+ <line
74
+ x1={LAYOUT.timeAxisWidth}
75
+ y1={marker.y}
76
+ x2={LAYOUT.timeAxisWidth + 2000}
77
+ y2={marker.y}
78
+ className="stroke-slate-50"
79
+ strokeWidth={1}
80
+ />
81
+ </g>
82
+ ))}
83
+ </g>
84
+ )
85
+ }
86
+
87
+ function calculateInterval(durationMs: number): number {
88
+ // Choose interval based on duration to get ~5-10 markers per segment
89
+ if (durationMs < 10000) return 2000 // Every 2s for < 10s
90
+ if (durationMs < 60000) return 10000 // Every 10s for < 1min
91
+ if (durationMs < 300000) return 30000 // Every 30s for < 5min
92
+ if (durationMs < 3600000) return 60000 // Every 1min for < 1h
93
+ return 300000 // Every 5min for > 1h
94
+ }
95
+
96
+ function formatTime(timestamp: number, sessionStartTime: number): string {
97
+ const elapsed = timestamp - sessionStartTime
98
+ const seconds = Math.floor(elapsed / 1000)
99
+ const minutes = Math.floor(seconds / 60)
100
+ const hours = Math.floor(minutes / 60)
101
+
102
+ if (hours > 0) {
103
+ return `${hours}:${String(minutes % 60).padStart(2, '0')}:${String(seconds % 60).padStart(2, '0')}`
104
+ }
105
+ return `${minutes}:${String(seconds % 60).padStart(2, '0')}`
106
+ }
@@ -0,0 +1,90 @@
1
+ import type { DiagramParticipant, TimeSegment } from '../types'
2
+ import { COLORS, LAYOUT } from '../types'
3
+
4
+ interface IdleGapProps {
5
+ segment: TimeSegment
6
+ participants: DiagramParticipant[]
7
+ yPosition: number
8
+ formatDuration: (ms: number) => string
9
+ }
10
+
11
+ export function IdleGap({ segment, participants, yPosition, formatDuration }: IdleGapProps) {
12
+ const totalWidth = LAYOUT.timeAxisWidth + participants.length * (LAYOUT.participantWidth + LAYOUT.participantGap)
13
+ const height = segment.displayHeight
14
+ const centerX = (LAYOUT.timeAxisWidth + totalWidth) / 2
15
+
16
+ return (
17
+ <g className="idle-gap">
18
+ {/* Background fill with gradient effect */}
19
+ <rect
20
+ x={LAYOUT.timeAxisWidth}
21
+ y={yPosition + 2}
22
+ width={totalWidth - LAYOUT.timeAxisWidth}
23
+ height={height - 4}
24
+ className="fill-slate-50"
25
+ rx={2}
26
+ />
27
+
28
+ {/* Top dashed line */}
29
+ <line
30
+ x1={LAYOUT.timeAxisWidth}
31
+ y1={yPosition}
32
+ x2={totalWidth}
33
+ y2={yPosition}
34
+ className={COLORS.idle.stroke}
35
+ strokeWidth={1}
36
+ strokeDasharray="4 4"
37
+ />
38
+
39
+ {/* Bottom dashed line */}
40
+ <line
41
+ x1={LAYOUT.timeAxisWidth}
42
+ y1={yPosition + height}
43
+ x2={totalWidth}
44
+ y2={yPosition + height}
45
+ className={COLORS.idle.stroke}
46
+ strokeWidth={1}
47
+ strokeDasharray="4 4"
48
+ />
49
+
50
+ {/* Duration label in center */}
51
+ <g transform={`translate(${centerX}, ${yPosition + height / 2})`}>
52
+ <rect
53
+ x={-36}
54
+ y={-10}
55
+ width={72}
56
+ height={20}
57
+ rx={10}
58
+ className="fill-white stroke-slate-200"
59
+ strokeWidth={1}
60
+ />
61
+ <text
62
+ x={0}
63
+ y={0}
64
+ textAnchor="middle"
65
+ dominantBaseline="middle"
66
+ className={`text-[10px] ${COLORS.idle.text} fill-current`}
67
+ >
68
+ {formatDuration(segment.actualDuration)}
69
+ </text>
70
+ </g>
71
+
72
+ {/* Vertical lane continuations (subtle dots) */}
73
+ {participants.map((participant) => {
74
+ const laneX = LAYOUT.timeAxisWidth + participant.columnIndex * (LAYOUT.participantWidth + LAYOUT.participantGap) + LAYOUT.participantWidth / 2
75
+ return (
76
+ <line
77
+ key={participant.id}
78
+ x1={laneX}
79
+ y1={yPosition + 4}
80
+ x2={laneX}
81
+ y2={yPosition + height - 4}
82
+ className={COLORS.idle.stroke}
83
+ strokeWidth={1}
84
+ strokeDasharray="2 6"
85
+ />
86
+ )
87
+ })}
88
+ </g>
89
+ )
90
+ }
@@ -0,0 +1,107 @@
1
+ import { useDebugNavigate } from '../../DebugNavigation'
2
+ import type { DiagramLLMBlock, DiagramParticipant } from '../types'
3
+ import { COLORS, LAYOUT } from '../types'
4
+
5
+ interface LLMBlockProps {
6
+ block: DiagramLLMBlock
7
+ participants: DiagramParticipant[]
8
+ onHover?: (block: DiagramLLMBlock | null, x: number, y: number) => void
9
+ onClick?: (block: DiagramLLMBlock) => void
10
+ }
11
+
12
+ export function LLMBlock({ block, participants, onHover, onClick }: LLMBlockProps) {
13
+ const navigate = useDebugNavigate()
14
+ const participant = participants.find((p) => p.id === block.participantId)
15
+
16
+ if (!participant) {
17
+ return null
18
+ }
19
+
20
+ const laneX = LAYOUT.timeAxisWidth + participant.columnIndex * (LAYOUT.participantWidth + LAYOUT.participantGap) + LAYOUT.participantWidth / 2
21
+ const x = laneX - LAYOUT.blockWidth / 2
22
+ const y = block.yStart
23
+ const height = Math.max(LAYOUT.blockMinHeight, block.yEnd - block.yStart)
24
+
25
+ const isRunning = block.status === 'running'
26
+ const isError = block.status === 'error'
27
+
28
+ const content = (
29
+ <g
30
+ className="cursor-pointer group"
31
+ onMouseEnter={(e) => onHover?.(block, e.clientX, e.clientY)}
32
+ onMouseLeave={() => onHover?.(null, 0, 0)}
33
+ onClick={() => onClick?.(block)}
34
+ >
35
+ {/* Block rectangle */}
36
+ <rect
37
+ x={x}
38
+ y={y}
39
+ width={LAYOUT.blockWidth}
40
+ height={height}
41
+ rx={4}
42
+ ry={4}
43
+ className={`
44
+ ${isError ? COLORS.error.fill : isRunning ? COLORS.llm.fillRunning : COLORS.llm.fill}
45
+ ${isError ? COLORS.error.stroke : COLORS.llm.stroke}
46
+ stroke-1
47
+ group-hover:stroke-violet-400
48
+ transition-colors
49
+ ${isRunning ? 'animate-pulse' : ''}
50
+ `}
51
+ />
52
+
53
+ {/* Icon */}
54
+ <g transform={`translate(${x + 6}, ${y + height / 2 - 5})`}>
55
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" className={isError ? COLORS.error.text : COLORS.llm.text}>
56
+ <path
57
+ d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
58
+ stroke="currentColor"
59
+ strokeWidth="2"
60
+ strokeLinecap="round"
61
+ strokeLinejoin="round"
62
+ />
63
+ </svg>
64
+ </g>
65
+
66
+ {/* Label */}
67
+ <text
68
+ x={x + 20}
69
+ y={y + height / 2}
70
+ dominantBaseline="middle"
71
+ className={`text-[10px] font-medium ${isError ? COLORS.error.text : COLORS.llm.text} fill-current`}
72
+ >
73
+ LLM
74
+ </text>
75
+
76
+ {/* Status indicators */}
77
+ {isRunning && (
78
+ <circle
79
+ cx={x + LAYOUT.blockWidth - 8}
80
+ cy={y + 8}
81
+ r={3}
82
+ className="fill-violet-500 animate-pulse"
83
+ />
84
+ )}
85
+
86
+ {isError && (
87
+ <circle
88
+ cx={x + LAYOUT.blockWidth - 8}
89
+ cy={y + 8}
90
+ r={3}
91
+ className="fill-red-500"
92
+ />
93
+ )}
94
+ </g>
95
+ )
96
+
97
+ // Wrap in clickable group if we have an LLM call ID
98
+ if (block.llmCallId) {
99
+ return (
100
+ <g style={{ cursor: 'pointer' }} onClick={() => navigate(`llm-calls/${block.llmCallId}`)}>
101
+ {content}
102
+ </g>
103
+ )
104
+ }
105
+
106
+ return content
107
+ }
@@ -0,0 +1,119 @@
1
+ import type { DiagramMessage, DiagramParticipant } from '../types'
2
+ import { LAYOUT } from '../types'
3
+
4
+ interface MessageArrowProps {
5
+ message: DiagramMessage
6
+ participants: DiagramParticipant[]
7
+ showLabel?: boolean
8
+ onHover?: (message: DiagramMessage | null, x: number, y: number) => void
9
+ onClick?: (message: DiagramMessage) => void
10
+ }
11
+
12
+ export function MessageArrow({ message, participants, showLabel = false, onHover, onClick }: MessageArrowProps) {
13
+ const fromParticipant = participants.find((p) => p.id === message.fromId)
14
+ const toParticipant = participants.find((p) => p.id === message.toId)
15
+
16
+ if (!fromParticipant || !toParticipant) {
17
+ return null
18
+ }
19
+
20
+ const fromX = LAYOUT.timeAxisWidth + fromParticipant.columnIndex * (LAYOUT.participantWidth + LAYOUT.participantGap) + LAYOUT.participantWidth / 2
21
+ const toX = LAYOUT.timeAxisWidth + toParticipant.columnIndex * (LAYOUT.participantWidth + LAYOUT.participantGap) + LAYOUT.participantWidth / 2
22
+
23
+ const y = message.yPosition
24
+ const isLeftToRight = fromX < toX
25
+
26
+ // Arrow head size
27
+ const arrowSize = 6
28
+ const arrowDirection = isLeftToRight ? 1 : -1
29
+
30
+ // Calculate label position
31
+ const midX = (fromX + toX) / 2
32
+ const labelMaxWidth = Math.abs(toX - fromX) - 20
33
+
34
+ // Truncate content for label
35
+ const truncatedContent = message.content.length > 50
36
+ ? message.content.slice(0, 50) + '…'
37
+ : message.content
38
+
39
+ return (
40
+ <g
41
+ className="cursor-pointer group"
42
+ onMouseEnter={(e) => onHover?.(message, e.clientX, e.clientY)}
43
+ onMouseLeave={() => onHover?.(null, 0, 0)}
44
+ onClick={() => onClick?.(message)}
45
+ >
46
+ {/* Invisible wider hit area for easier hovering */}
47
+ <line
48
+ x1={fromX}
49
+ y1={y}
50
+ x2={toX}
51
+ y2={y}
52
+ stroke="transparent"
53
+ strokeWidth={16}
54
+ />
55
+
56
+ {/* Arrow line */}
57
+ <line
58
+ x1={fromX}
59
+ y1={y}
60
+ x2={toX - arrowDirection * arrowSize}
61
+ y2={y}
62
+ className="stroke-blue-300 group-hover:stroke-blue-500 transition-colors"
63
+ strokeWidth={1.5}
64
+ />
65
+
66
+ {/* Arrow head */}
67
+ <polygon
68
+ points={`
69
+ ${toX},${y}
70
+ ${toX - arrowDirection * arrowSize},${y - arrowSize / 2}
71
+ ${toX - arrowDirection * arrowSize},${y + arrowSize / 2}
72
+ `}
73
+ className="fill-blue-300 group-hover:fill-blue-500 transition-colors"
74
+ />
75
+
76
+ {/* Small dot at start */}
77
+ <circle
78
+ cx={fromX}
79
+ cy={y}
80
+ r={2.5}
81
+ className="fill-blue-300 group-hover:fill-blue-500 transition-colors"
82
+ />
83
+
84
+ {/* Label (optional) */}
85
+ {showLabel && labelMaxWidth > 40 && (
86
+ <g>
87
+ {/* Label background */}
88
+ <rect
89
+ x={midX - labelMaxWidth / 2}
90
+ y={y - 18}
91
+ width={labelMaxWidth}
92
+ height={14}
93
+ rx={3}
94
+ className="fill-white/90 stroke-blue-200 group-hover:stroke-blue-300"
95
+ strokeWidth={0.5}
96
+ />
97
+ {/* Label text */}
98
+ <text
99
+ x={midX}
100
+ y={y - 9}
101
+ textAnchor="middle"
102
+ dominantBaseline="middle"
103
+ className="text-[9px] fill-slate-600 group-hover:fill-slate-800"
104
+ style={{
105
+ fontSize: '9px',
106
+ clipPath: `inset(0 0 0 0)`,
107
+ }}
108
+ >
109
+ <tspan>
110
+ {truncatedContent.length > labelMaxWidth / 5
111
+ ? truncatedContent.slice(0, Math.floor(labelMaxWidth / 5)) + '…'
112
+ : truncatedContent}
113
+ </tspan>
114
+ </text>
115
+ </g>
116
+ )}
117
+ </g>
118
+ )
119
+ }
@@ -0,0 +1,99 @@
1
+ import type { DiagramParticipant, DiagramToolBlock } from '../types'
2
+ import { COLORS, LAYOUT } from '../types'
3
+
4
+ interface ToolBlockProps {
5
+ block: DiagramToolBlock
6
+ participants: DiagramParticipant[]
7
+ onHover?: (block: DiagramToolBlock | null, x: number, y: number) => void
8
+ onClick?: (block: DiagramToolBlock) => void
9
+ }
10
+
11
+ export function ToolBlock({ block, participants, onHover, onClick }: ToolBlockProps) {
12
+ const participant = participants.find((p) => p.id === block.participantId)
13
+
14
+ if (!participant) {
15
+ return null
16
+ }
17
+
18
+ const laneX = LAYOUT.timeAxisWidth + participant.columnIndex * (LAYOUT.participantWidth + LAYOUT.participantGap) + LAYOUT.participantWidth / 2
19
+ const x = laneX - LAYOUT.blockWidth / 2
20
+ const y = block.yStart
21
+ const height = Math.max(LAYOUT.blockMinHeight - 4, block.yEnd - block.yStart)
22
+
23
+ const isRunning = block.status === 'running'
24
+ const isError = block.status === 'error'
25
+
26
+ // Truncate tool name
27
+ const toolName = block.toolName.length > 8
28
+ ? block.toolName.slice(0, 8) + '…'
29
+ : block.toolName
30
+
31
+ return (
32
+ <g
33
+ className="cursor-pointer group"
34
+ onMouseEnter={(e) => onHover?.(block, e.clientX, e.clientY)}
35
+ onMouseLeave={() => onHover?.(null, 0, 0)}
36
+ onClick={() => onClick?.(block)}
37
+ >
38
+ {/* Block rectangle */}
39
+ <rect
40
+ x={x}
41
+ y={y}
42
+ width={LAYOUT.blockWidth}
43
+ height={height}
44
+ rx={4}
45
+ ry={4}
46
+ className={`
47
+ ${isError ? COLORS.error.fill : isRunning ? COLORS.tool.fillRunning : COLORS.tool.fill}
48
+ ${isError ? COLORS.error.stroke : COLORS.tool.stroke}
49
+ stroke-1
50
+ group-hover:stroke-teal-400
51
+ transition-colors
52
+ ${isRunning ? 'animate-pulse' : ''}
53
+ `}
54
+ />
55
+
56
+ {/* Tool icon */}
57
+ <g transform={`translate(${x + 6}, ${y + height / 2 - 5})`}>
58
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" className={isError ? COLORS.error.text : COLORS.tool.text}>
59
+ <path
60
+ d="M14.7 6.3a1 1 0 000 1.4l1.6 1.6a1 1 0 001.4 0l3.77-3.77a6 6 0 01-7.94 7.94l-6.91 6.91a2.12 2.12 0 01-3-3l6.91-6.91a6 6 0 017.94-7.94l-3.76 3.76z"
61
+ stroke="currentColor"
62
+ strokeWidth="2"
63
+ strokeLinecap="round"
64
+ strokeLinejoin="round"
65
+ />
66
+ </svg>
67
+ </g>
68
+
69
+ {/* Tool name */}
70
+ <text
71
+ x={x + 20}
72
+ y={y + height / 2}
73
+ dominantBaseline="middle"
74
+ className={`text-[9px] font-mono ${isError ? COLORS.error.text : COLORS.tool.text} fill-current`}
75
+ >
76
+ {toolName}
77
+ </text>
78
+
79
+ {/* Status indicators */}
80
+ {isRunning && (
81
+ <circle
82
+ cx={x + LAYOUT.blockWidth - 8}
83
+ cy={y + 8}
84
+ r={3}
85
+ className="fill-teal-500 animate-pulse"
86
+ />
87
+ )}
88
+
89
+ {isError && (
90
+ <circle
91
+ cx={x + LAYOUT.blockWidth - 8}
92
+ cy={y + 8}
93
+ r={3}
94
+ className="fill-red-500"
95
+ />
96
+ )}
97
+ </g>
98
+ )
99
+ }