@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,276 @@
1
+ import type { BuiltinEvent, DomainEvent, InferenceCompletedEvent, InferenceFailedEvent } from '@roj-ai/sdk'
2
+ import { useCallback, useMemo, useState } from 'react'
3
+ import { DebugLink, useDebugSessionId } from '../DebugNavigation'
4
+ import { api, unwrap } from '@roj-ai/client'
5
+ import { useEvents, useEventStore } from '../../../stores/event-store'
6
+
7
+ const EVENT_TYPES = [
8
+ 'all',
9
+ 'session_created',
10
+ 'session_closed',
11
+ 'agent_spawned',
12
+ 'agent_state_changed',
13
+ 'mailbox_message',
14
+ 'mailbox_consumed',
15
+ 'inference_started',
16
+ 'inference_completed',
17
+ 'inference_failed',
18
+ 'tool_started',
19
+ 'tool_completed',
20
+ 'tool_failed',
21
+ 'context_compacted',
22
+ 'user_question_asked',
23
+ 'user_message_sent',
24
+ 'communicator_linked',
25
+ 'session_restarted',
26
+ ] as const
27
+
28
+ const PAGE_SIZE = 50
29
+
30
+ export function EventsPage() {
31
+ const sessionId = useDebugSessionId()
32
+
33
+ // Get events from event store (already loaded by DebugLayout)
34
+ const allEvents = useEvents()
35
+ const isLoading = useEventStore((s) => s.isLoading)
36
+ const error = useEventStore((s) => s.error)
37
+
38
+ const [typeFilter, setTypeFilter] = useState<string>('all')
39
+ const [offset, setOffset] = useState(0)
40
+
41
+ // Wrap events with original index for fork API
42
+ const indexedEvents = useMemo(() => allEvents.map((event, originalIndex) => ({ event, originalIndex })), [allEvents])
43
+
44
+ // Filter events locally
45
+ const filteredEvents = useMemo(() => {
46
+ if (typeFilter === 'all') return indexedEvents
47
+ return indexedEvents.filter((e) => e.event.type === typeFilter)
48
+ }, [indexedEvents, typeFilter])
49
+
50
+ // Apply pagination
51
+ const paginatedEvents = useMemo(() => {
52
+ return filteredEvents.slice(offset, offset + PAGE_SIZE)
53
+ }, [filteredEvents, offset])
54
+
55
+ const handleFork = useCallback(async (eventIndex: number) => {
56
+ if (!sessionId) return
57
+ const result = unwrap(await api.call('sessions.fork', { sessionId, eventIndex }))
58
+ const newUrl = window.location.pathname.replace(sessionId, result.sessionId)
59
+ window.open(newUrl, '_blank')
60
+ }, [sessionId])
61
+
62
+ const total = filteredEvents.length
63
+ const totalPages = Math.ceil(total / PAGE_SIZE)
64
+ const currentPage = Math.floor(offset / PAGE_SIZE) + 1
65
+
66
+ return (
67
+ <div className="space-y-4">
68
+ {/* Filter */}
69
+ <div className="flex items-center gap-4">
70
+ <label className="text-sm text-slate-600">
71
+ Filter by type:
72
+ <select
73
+ value={typeFilter}
74
+ onChange={(e) => {
75
+ setTypeFilter(e.target.value)
76
+ setOffset(0) // Reset pagination when filter changes
77
+ }}
78
+ className="ml-2 border border-slate-300 rounded px-2 py-1 text-sm"
79
+ >
80
+ {EVENT_TYPES.map((type) => (
81
+ <option key={type} value={type}>
82
+ {type === 'all' ? 'All Events' : type}
83
+ </option>
84
+ ))}
85
+ </select>
86
+ </label>
87
+ <span className="text-sm text-slate-500">{total} events total</span>
88
+ </div>
89
+
90
+ {/* Error */}
91
+ {error && <div className="text-red-500 text-sm">{error}</div>}
92
+
93
+ {/* Events List */}
94
+ <div className="bg-white rounded-md border border-slate-200 overflow-hidden">
95
+ {isLoading && allEvents.length === 0
96
+ ? <div className="p-4 text-slate-500 text-sm">Loading...</div>
97
+ : paginatedEvents.length === 0
98
+ ? <div className="p-4 text-slate-500 text-sm">No events found</div>
99
+ : (
100
+ <div className="divide-y divide-slate-200">
101
+ {paginatedEvents.map(({ event, originalIndex }) => (
102
+ <EventRow
103
+ key={`${event.timestamp}-${originalIndex}`}
104
+ event={event}
105
+ eventIndex={originalIndex}
106
+ onFork={handleFork}
107
+ />
108
+ ))}
109
+ </div>
110
+ )}
111
+ </div>
112
+
113
+ {/* Pagination */}
114
+ {totalPages > 1 && (
115
+ <div className="flex items-center justify-between">
116
+ <button
117
+ onClick={() => setOffset(Math.max(0, offset - PAGE_SIZE))}
118
+ disabled={offset === 0}
119
+ className="px-3 py-1 text-sm border border-slate-300 rounded hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed"
120
+ >
121
+ Previous
122
+ </button>
123
+ <span className="text-sm text-slate-600">
124
+ Page {currentPage} of {totalPages}
125
+ </span>
126
+ <button
127
+ onClick={() => setOffset(offset + PAGE_SIZE)}
128
+ disabled={currentPage >= totalPages}
129
+ className="px-3 py-1 text-sm border border-slate-300 rounded hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed"
130
+ >
131
+ Next
132
+ </button>
133
+ </div>
134
+ )}
135
+ </div>
136
+ )
137
+ }
138
+
139
+ function EventRow({
140
+ event,
141
+ eventIndex,
142
+ onFork,
143
+ }: {
144
+ event: DomainEvent
145
+ eventIndex: number
146
+ onFork: (eventIndex: number) => Promise<void>
147
+ }) {
148
+ const [expanded, setExpanded] = useState(false)
149
+ const [forking, setForking] = useState(false)
150
+
151
+ const typeColors: Record<string, string> = {
152
+ session_created: 'bg-green-100 text-green-700',
153
+ session_closed: 'bg-slate-100 text-slate-700',
154
+ agent_spawned: 'bg-blue-100 text-blue-700',
155
+ agent_state_changed: 'bg-yellow-100 text-yellow-700',
156
+ mailbox_message: 'bg-purple-100 text-purple-700',
157
+ mailbox_consumed: 'bg-purple-50 text-purple-600',
158
+ inference_started: 'bg-orange-100 text-orange-700',
159
+ inference_completed: 'bg-green-100 text-green-700',
160
+ inference_failed: 'bg-red-100 text-red-700',
161
+ tool_started: 'bg-cyan-100 text-cyan-700',
162
+ tool_completed: 'bg-green-100 text-green-700',
163
+ tool_failed: 'bg-red-100 text-red-700',
164
+ context_compacted: 'bg-slate-100 text-slate-700',
165
+ user_question_asked: 'bg-yellow-100 text-yellow-700',
166
+ user_message_sent: 'bg-blue-100 text-blue-700',
167
+ }
168
+
169
+ const llmCallId = (event.type === 'inference_completed' || event.type === 'inference_failed')
170
+ && (event as InferenceCompletedEvent | InferenceFailedEvent).llmCallId
171
+
172
+ const agentId = 'agentId' in event ? (event as { agentId: string }).agentId : null
173
+
174
+ return (
175
+ <div className="text-sm">
176
+ <button
177
+ onClick={() => setExpanded(!expanded)}
178
+ className="w-full p-3 hover:bg-slate-50 flex items-center gap-3 text-left"
179
+ >
180
+ <span className="text-xs text-slate-400 font-mono w-20 shrink-0">
181
+ {new Date(event.timestamp).toLocaleTimeString()}
182
+ </span>
183
+ <span
184
+ className={`text-xs px-2 py-0.5 rounded-full font-medium shrink-0 ${typeColors[event.type] || 'bg-slate-100'}`}
185
+ >
186
+ {event.type}
187
+ </span>
188
+ <span className="text-slate-600 truncate flex-1">
189
+ {getEventSummary(event)}
190
+ </span>
191
+
192
+ {/* Links */}
193
+ <div className="flex items-center gap-2 shrink-0">
194
+ {agentId && (
195
+ <DebugLink
196
+ to={`agents/${agentId}`}
197
+ className="text-xs text-violet-600 hover:underline"
198
+ >
199
+ Agent
200
+ </DebugLink>
201
+ )}
202
+ {llmCallId && (
203
+ <DebugLink
204
+ to={`llm-calls/${llmCallId}`}
205
+ className="text-xs text-violet-600 hover:underline"
206
+ >
207
+ LLM Call
208
+ </DebugLink>
209
+ )}
210
+ <button
211
+ onClick={async (e) => {
212
+ e.stopPropagation()
213
+ setForking(true)
214
+ try {
215
+ await onFork(eventIndex)
216
+ } finally {
217
+ setForking(false)
218
+ }
219
+ }}
220
+ disabled={forking}
221
+ className="text-xs text-violet-600 hover:underline disabled:opacity-50"
222
+ >
223
+ {forking ? 'Forking...' : 'Fork'}
224
+ </button>
225
+ </div>
226
+
227
+ <span className="text-slate-400 shrink-0">{expanded ? '▼' : '▶'}</span>
228
+ </button>
229
+ {expanded && (
230
+ <div className="px-3 pb-3 bg-slate-50">
231
+ <pre className="text-xs overflow-x-auto p-3 bg-white border border-slate-200 rounded-md">
232
+ {JSON.stringify(event, null, 2)}
233
+ </pre>
234
+ </div>
235
+ )}
236
+ </div>
237
+ )
238
+ }
239
+
240
+ function getEventSummary(event: DomainEvent): string {
241
+ const e = event as BuiltinEvent
242
+ switch (e.type) {
243
+ case 'session_created':
244
+ return `Preset: ${e.presetId}`
245
+ case 'session_closed':
246
+ return 'Session closed'
247
+ case 'agent_spawned':
248
+ return `${e.definitionName} (${e.agentId.slice(0, 8)})`
249
+ case 'agent_state_changed':
250
+ return `${e.fromState} → ${e.toState}`
251
+ case 'mailbox_message':
252
+ return `To: ${e.toAgentId.slice(0, 8)}`
253
+ case 'mailbox_consumed':
254
+ return `${e.messageIds.length} messages`
255
+ case 'inference_started':
256
+ return `Agent: ${e.agentId.slice(0, 8)}`
257
+ case 'inference_completed':
258
+ return `${e.metrics.totalTokens} tokens, ${e.metrics.model}`
259
+ case 'inference_failed':
260
+ return e.error
261
+ case 'tool_started':
262
+ return `${e.toolName}`
263
+ case 'tool_completed':
264
+ return `${e.toolCallId.slice(0, 8)} completed`
265
+ case 'tool_failed':
266
+ return `${e.toolCallId.slice(0, 8)}: ${e.error}`
267
+ case 'context_compacted':
268
+ return `${e.originalTokens} → ${e.compactedTokens} tokens`
269
+ case 'user_question_asked':
270
+ return e.question.slice(0, 50) + (e.question.length > 50 ? '...' : '')
271
+ case 'user_message_sent':
272
+ return e.message.slice(0, 50) + (e.message.length > 50 ? '...' : '')
273
+ default:
274
+ return ''
275
+ }
276
+ }
@@ -0,0 +1,366 @@
1
+ import { useCallback, useEffect, useState } from 'react'
2
+ import { api, getApiBaseUrl } from '@roj-ai/client'
3
+ import { useDebugSessionId } from '../DebugNavigation'
4
+
5
+ type Root = 'session' | 'workspace'
6
+
7
+ interface DirectoryEntry {
8
+ name: string
9
+ type: 'file' | 'directory'
10
+ size: number
11
+ mimeType?: string
12
+ }
13
+
14
+ interface DirectoryListing {
15
+ entries: DirectoryEntry[]
16
+ path: string
17
+ root: string
18
+ }
19
+
20
+ function formatFileSize(bytes: number): string {
21
+ if (bytes === 0) return '0 B'
22
+ const units = ['B', 'KB', 'MB', 'GB']
23
+ const i = Math.floor(Math.log(bytes) / Math.log(1024))
24
+ const size = bytes / Math.pow(1024, i)
25
+ return `${size.toFixed(i > 0 ? 1 : 0)} ${units[i]}`
26
+ }
27
+
28
+ function isImageMime(mimeType: string | undefined): boolean {
29
+ return mimeType?.startsWith('image/') ?? false
30
+ }
31
+
32
+ function isTextMime(mimeType: string | undefined): boolean {
33
+ if (!mimeType) return false
34
+ return mimeType.startsWith('text/')
35
+ || mimeType === 'application/json'
36
+ || mimeType === 'application/javascript'
37
+ }
38
+
39
+ export function FilesPage() {
40
+ const sessionId = useDebugSessionId()
41
+ const [root, setRoot] = useState<Root>('session')
42
+ const [currentPath, setCurrentPath] = useState('')
43
+ const [listing, setListing] = useState<DirectoryListing | null>(null)
44
+ const [listingError, setListingError] = useState<string | null>(null)
45
+ const [listingLoading, setListingLoading] = useState(false)
46
+ const [selectedFile, setSelectedFile] = useState<{ path: string; entry: DirectoryEntry } | null>(null)
47
+ const [fileContent, setFileContent] = useState<string | null>(null)
48
+ const [fileUrl, setFileUrl] = useState<string | null>(null)
49
+ const [fileLoading, setFileLoading] = useState(false)
50
+ const [fileError, setFileError] = useState<string | null>(null)
51
+
52
+ const fetchListing = useCallback(async (r: Root, path: string) => {
53
+ if (!sessionId) return
54
+ setListingLoading(true)
55
+ setListingError(null)
56
+
57
+ try {
58
+ const method = r === 'session' ? 'filesystem.listSession' : 'filesystem.listWorkspace'
59
+ const result = await api.call(method, { sessionId, path: path || undefined })
60
+ if (!result.ok) {
61
+ setListingError(result.error.message)
62
+ setListing(null)
63
+ return
64
+ }
65
+ setListing(result.value)
66
+ } catch (e) {
67
+ setListingError(e instanceof Error ? e.message : 'Failed to fetch listing')
68
+ setListing(null)
69
+ } finally {
70
+ setListingLoading(false)
71
+ }
72
+ }, [sessionId])
73
+
74
+ // Fetch listing when root or path changes
75
+ useEffect(() => {
76
+ fetchListing(root, currentPath)
77
+ }, [root, currentPath, fetchListing])
78
+
79
+ // Fetch file content when a file is selected
80
+ useEffect(() => {
81
+ if (!selectedFile || !sessionId) {
82
+ setFileContent(null)
83
+ setFileUrl(null)
84
+ setFileError(null)
85
+ return
86
+ }
87
+
88
+ const baseUrl = getApiBaseUrl()
89
+ const routePrefix = root === 'session' ? 'files' : 'workspace'
90
+ const url = `${baseUrl}/sessions/${sessionId}/${routePrefix}/${selectedFile.path}`
91
+
92
+ if (isImageMime(selectedFile.entry.mimeType)) {
93
+ setFileUrl(url)
94
+ setFileContent(null)
95
+ setFileLoading(false)
96
+ setFileError(null)
97
+ return
98
+ }
99
+
100
+ if (isTextMime(selectedFile.entry.mimeType)) {
101
+ setFileLoading(true)
102
+ setFileError(null)
103
+ setFileUrl(null)
104
+ fetch(url)
105
+ .then(async (res) => {
106
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
107
+ return res.text()
108
+ })
109
+ .then((text) => {
110
+ setFileContent(text)
111
+ setFileLoading(false)
112
+ })
113
+ .catch((e) => {
114
+ setFileError(e instanceof Error ? e.message : 'Failed to fetch file')
115
+ setFileLoading(false)
116
+ })
117
+ return
118
+ }
119
+
120
+ // Non-text, non-image: just show info + download link
121
+ setFileUrl(url)
122
+ setFileContent(null)
123
+ setFileLoading(false)
124
+ setFileError(null)
125
+ }, [selectedFile, sessionId, root])
126
+
127
+ const handleNavigate = (entry: DirectoryEntry) => {
128
+ if (entry.type === 'directory') {
129
+ const newPath = currentPath ? `${currentPath}/${entry.name}` : entry.name
130
+ setCurrentPath(newPath)
131
+ setSelectedFile(null)
132
+ } else {
133
+ const filePath = currentPath ? `${currentPath}/${entry.name}` : entry.name
134
+ setSelectedFile({ path: filePath, entry })
135
+ }
136
+ }
137
+
138
+ const handleRootChange = (newRoot: Root) => {
139
+ setRoot(newRoot)
140
+ setCurrentPath('')
141
+ setSelectedFile(null)
142
+ }
143
+
144
+ const handleBreadcrumbClick = (index: number) => {
145
+ if (index === -1) {
146
+ setCurrentPath('')
147
+ } else {
148
+ const parts = currentPath.split('/')
149
+ setCurrentPath(parts.slice(0, index + 1).join('/'))
150
+ }
151
+ setSelectedFile(null)
152
+ }
153
+
154
+ if (!sessionId) return null
155
+
156
+ const pathParts = currentPath ? currentPath.split('/') : []
157
+
158
+ return (
159
+ <div className="h-full flex gap-4">
160
+ {/* Left panel - Directory browser */}
161
+ <div className="w-80 shrink-0 bg-white rounded-md border border-slate-200 flex flex-col">
162
+ {/* Root tabs */}
163
+ <div className="flex border-b border-slate-200">
164
+ <button
165
+ onClick={() => handleRootChange('session')}
166
+ className={`flex-1 px-4 py-2 text-sm font-medium transition-colors ${
167
+ root === 'session'
168
+ ? 'text-violet-700 border-b-2 border-violet-600 bg-violet-50'
169
+ : 'text-slate-500 hover:text-slate-700 hover:bg-slate-50'
170
+ }`}
171
+ >
172
+ Session
173
+ </button>
174
+ <button
175
+ onClick={() => handleRootChange('workspace')}
176
+ className={`flex-1 px-4 py-2 text-sm font-medium transition-colors ${
177
+ root === 'workspace'
178
+ ? 'text-violet-700 border-b-2 border-violet-600 bg-violet-50'
179
+ : 'text-slate-500 hover:text-slate-700 hover:bg-slate-50'
180
+ }`}
181
+ >
182
+ Workspace
183
+ </button>
184
+ </div>
185
+
186
+ {/* Breadcrumbs */}
187
+ <div className="px-3 py-2 border-b border-slate-100 flex items-center gap-1 text-sm overflow-x-auto">
188
+ <button
189
+ onClick={() => handleBreadcrumbClick(-1)}
190
+ className="text-violet-600 hover:underline shrink-0"
191
+ >
192
+ /
193
+ </button>
194
+ {pathParts.map((part, i) => (
195
+ <span key={i} className="flex items-center gap-1 shrink-0">
196
+ <span className="text-slate-400">/</span>
197
+ {i < pathParts.length - 1
198
+ ? (
199
+ <button
200
+ onClick={() => handleBreadcrumbClick(i)}
201
+ className="text-violet-600 hover:underline"
202
+ >
203
+ {part}
204
+ </button>
205
+ )
206
+ : <span className="text-slate-700 font-medium">{part}</span>}
207
+ </span>
208
+ ))}
209
+ </div>
210
+
211
+ {/* Directory listing */}
212
+ <div className="flex-1 overflow-auto">
213
+ {listingLoading
214
+ ? <div className="p-3 text-slate-500 text-sm">Loading...</div>
215
+ : listingError
216
+ ? <div className="p-3 text-red-500 text-sm">{listingError}</div>
217
+ : !listing || listing.entries.length === 0
218
+ ? <div className="p-3 text-slate-500 text-sm">Empty directory</div>
219
+ : (
220
+ <div className="divide-y divide-slate-100">
221
+ {currentPath && (
222
+ <button
223
+ onClick={() => {
224
+ const parts = currentPath.split('/')
225
+ parts.pop()
226
+ setCurrentPath(parts.join('/'))
227
+ setSelectedFile(null)
228
+ }}
229
+ className="w-full flex items-center gap-2 px-3 py-2 text-sm text-slate-500 hover:bg-slate-50 text-left"
230
+ >
231
+ <FolderUpIcon />
232
+ <span>..</span>
233
+ </button>
234
+ )}
235
+ {listing.entries.map((entry) => {
236
+ const isSelected = selectedFile
237
+ && entry.type === 'file'
238
+ && selectedFile.entry.name === entry.name
239
+ && selectedFile.path === (currentPath ? `${currentPath}/${entry.name}` : entry.name)
240
+
241
+ return (
242
+ <button
243
+ key={entry.name}
244
+ onClick={() => handleNavigate(entry)}
245
+ className={`w-full flex items-center gap-2 px-3 py-2 text-sm text-left transition-colors ${
246
+ isSelected
247
+ ? 'bg-violet-50 text-violet-700'
248
+ : 'text-slate-700 hover:bg-slate-50'
249
+ }`}
250
+ >
251
+ {entry.type === 'directory' ? <FolderIcon /> : <FileIcon />}
252
+ <span className="truncate flex-1">{entry.name}</span>
253
+ {entry.type === 'file' && (
254
+ <span className="text-xs text-slate-400 shrink-0">
255
+ {formatFileSize(entry.size)}
256
+ </span>
257
+ )}
258
+ </button>
259
+ )
260
+ })}
261
+ </div>
262
+ )}
263
+ </div>
264
+ </div>
265
+
266
+ {/* Right panel - File viewer */}
267
+ <div className="flex-1 bg-white rounded-md border border-slate-200 flex flex-col min-w-0">
268
+ <div className="p-3 border-b border-slate-200">
269
+ <h2 className="font-medium text-slate-900">
270
+ {selectedFile ? selectedFile.path : 'File Viewer'}
271
+ </h2>
272
+ {selectedFile && (
273
+ <div className="flex items-center gap-3 mt-1 text-xs text-slate-500">
274
+ <span>{selectedFile.entry.mimeType ?? 'unknown type'}</span>
275
+ <span>{formatFileSize(selectedFile.entry.size)}</span>
276
+ </div>
277
+ )}
278
+ </div>
279
+ <div className="flex-1 overflow-auto p-4">
280
+ {!selectedFile
281
+ ? (
282
+ <div className="text-slate-500 text-sm">
283
+ Select a file from the directory browser to view its contents
284
+ </div>
285
+ )
286
+ : fileLoading
287
+ ? <div className="text-slate-500 text-sm">Loading file...</div>
288
+ : fileError
289
+ ? <div className="text-red-500 text-sm">{fileError}</div>
290
+ : fileContent !== null
291
+ ? (
292
+ <pre className="text-xs font-mono whitespace-pre-wrap break-words bg-slate-50 p-4 rounded-md border border-slate-200 overflow-auto max-h-full">
293
+ {fileContent}
294
+ </pre>
295
+ )
296
+ : fileUrl && isImageMime(selectedFile.entry.mimeType)
297
+ ? (
298
+ <div className="flex items-center justify-center">
299
+ <img
300
+ src={fileUrl}
301
+ alt={selectedFile.entry.name}
302
+ className="max-w-full max-h-[70vh] object-contain rounded border border-slate-200"
303
+ />
304
+ </div>
305
+ )
306
+ : fileUrl
307
+ ? (
308
+ <div className="space-y-3">
309
+ <p className="text-sm text-slate-600">
310
+ This file type cannot be previewed.
311
+ </p>
312
+ <a
313
+ href={fileUrl}
314
+ download={selectedFile.entry.name}
315
+ className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-violet-700 bg-violet-50 rounded-md hover:bg-violet-100 transition-colors"
316
+ >
317
+ <DownloadIcon />
318
+ Download {selectedFile.entry.name}
319
+ </a>
320
+ </div>
321
+ )
322
+ : null}
323
+ </div>
324
+ </div>
325
+ </div>
326
+ )
327
+ }
328
+
329
+ // Icons
330
+
331
+ function FolderIcon() {
332
+ return (
333
+ <svg className="w-4 h-4 text-yellow-500 shrink-0" fill="currentColor" viewBox="0 0 20 20">
334
+ <path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" />
335
+ </svg>
336
+ )
337
+ }
338
+
339
+ function FolderUpIcon() {
340
+ return (
341
+ <svg className="w-4 h-4 text-slate-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
342
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h18M3 10l6-6m-6 6l6 6" />
343
+ </svg>
344
+ )
345
+ }
346
+
347
+ function FileIcon() {
348
+ return (
349
+ <svg className="w-4 h-4 text-slate-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
350
+ <path
351
+ strokeLinecap="round"
352
+ strokeLinejoin="round"
353
+ strokeWidth={2}
354
+ d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"
355
+ />
356
+ </svg>
357
+ )
358
+ }
359
+
360
+ function DownloadIcon() {
361
+ return (
362
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
363
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
364
+ </svg>
365
+ )
366
+ }
@@ -0,0 +1,32 @@
1
+ import { DebugLink, useDebugParams, useDebugSessionId } from '../DebugNavigation'
2
+ import { LLMCallDetail } from '../LLMCallDetail'
3
+
4
+ export function LLMCallPage() {
5
+ const sessionId = useDebugSessionId()
6
+ const { callId } = useDebugParams<{ callId: string }>()
7
+
8
+ if (!callId) {
9
+ return <div className="text-slate-500">Invalid URL</div>
10
+ }
11
+
12
+ return (
13
+ <div className="space-y-4">
14
+ {/* Breadcrumb */}
15
+ <div className="flex items-center gap-2 text-sm">
16
+ <DebugLink
17
+ to="llm-calls"
18
+ className="text-violet-600 hover:underline"
19
+ >
20
+ LLM Calls
21
+ </DebugLink>
22
+ <span className="text-slate-400">/</span>
23
+ <span className="text-slate-600 font-mono">{callId.slice(0, 12)}...</span>
24
+ </div>
25
+
26
+ {/* Detail */}
27
+ <div className="bg-white rounded-md border border-slate-200 p-6">
28
+ <LLMCallDetail sessionId={sessionId} callId={callId} />
29
+ </div>
30
+ </div>
31
+ )
32
+ }