@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.
- package/dist/components/debug/DebugContext.d.ts +10 -0
- package/dist/components/debug/DebugNavigation.d.ts +29 -0
- package/dist/components/debug/DebugShell.d.ts +18 -0
- package/dist/components/debug/LLMCallDetail.d.ts +7 -0
- package/dist/components/debug/TimelineDetailInspector.d.ts +6 -0
- package/dist/components/debug/communication/CommunicationDiagram.d.ts +9 -0
- package/dist/components/debug/communication/DiagramHeader.d.ts +7 -0
- package/dist/components/debug/communication/ParticipantLane.d.ts +7 -0
- package/dist/components/debug/communication/TimeAxis.d.ts +9 -0
- package/dist/components/debug/communication/elements/IdleGap.d.ts +9 -0
- package/dist/components/debug/communication/elements/LLMBlock.d.ts +9 -0
- package/dist/components/debug/communication/elements/MessageArrow.d.ts +10 -0
- package/dist/components/debug/communication/elements/ToolBlock.d.ts +9 -0
- package/dist/components/debug/communication/hooks/useDiagramData.d.ts +12 -0
- package/dist/components/debug/communication/hooks/useTimeCompression.d.ts +7 -0
- package/dist/components/debug/communication/hooks/useZoomPan.d.ts +11 -0
- package/dist/components/debug/communication/popovers/ElementPopover.d.ts +8 -0
- package/dist/components/debug/communication/types.d.ts +136 -0
- package/dist/components/debug/index.d.ts +11 -0
- package/dist/components/debug/pages/AgentDetailPage.d.ts +3 -0
- package/dist/components/debug/pages/AgentsPage.d.ts +1 -0
- package/dist/components/debug/pages/CommunicationPage.d.ts +1 -0
- package/dist/components/debug/pages/DashboardPage.d.ts +1 -0
- package/dist/components/debug/pages/EventsPage.d.ts +1 -0
- package/dist/components/debug/pages/FilesPage.d.ts +1 -0
- package/dist/components/debug/pages/LLMCallPage.d.ts +1 -0
- package/dist/components/debug/pages/LLMCallsPage.d.ts +1 -0
- package/dist/components/debug/pages/LogsPage.d.ts +1 -0
- package/dist/components/debug/pages/MailboxPage.d.ts +1 -0
- package/dist/components/debug/pages/ServicesPage.d.ts +1 -0
- package/dist/components/debug/pages/TimelinePage.d.ts +1 -0
- package/dist/components/debug/pages/UserChatPage.d.ts +1 -0
- package/dist/components/debug/pages/index.d.ts +13 -0
- package/dist/index.d.ts +9 -0
- package/dist/lib/domain-utils.d.ts +7 -0
- package/dist/providers/EventPollingProvider.d.ts +27 -0
- package/dist/stores/event-store.d.ts +93 -0
- package/dist/utils/format.d.ts +1 -0
- package/package.json +43 -0
- package/src/components/debug/DebugContext.tsx +18 -0
- package/src/components/debug/DebugNavigation.tsx +55 -0
- package/src/components/debug/DebugShell.tsx +321 -0
- package/src/components/debug/LLMCallDetail.tsx +740 -0
- package/src/components/debug/TimelineDetailInspector.tsx +204 -0
- package/src/components/debug/communication/CommunicationDiagram.tsx +260 -0
- package/src/components/debug/communication/DiagramHeader.tsx +113 -0
- package/src/components/debug/communication/ParticipantLane.tsx +60 -0
- package/src/components/debug/communication/TimeAxis.tsx +106 -0
- package/src/components/debug/communication/elements/IdleGap.tsx +90 -0
- package/src/components/debug/communication/elements/LLMBlock.tsx +107 -0
- package/src/components/debug/communication/elements/MessageArrow.tsx +119 -0
- package/src/components/debug/communication/elements/ToolBlock.tsx +99 -0
- package/src/components/debug/communication/hooks/useDiagramData.ts +294 -0
- package/src/components/debug/communication/hooks/useTimeCompression.ts +140 -0
- package/src/components/debug/communication/hooks/useZoomPan.ts +87 -0
- package/src/components/debug/communication/popovers/ElementPopover.tsx +158 -0
- package/src/components/debug/communication/types.ts +180 -0
- package/src/components/debug/index.ts +37 -0
- package/src/components/debug/pages/AgentDetailPage.tsx +1295 -0
- package/src/components/debug/pages/AgentsPage.tsx +297 -0
- package/src/components/debug/pages/CommunicationPage.tsx +89 -0
- package/src/components/debug/pages/DashboardPage.tsx +1504 -0
- package/src/components/debug/pages/EventsPage.tsx +276 -0
- package/src/components/debug/pages/FilesPage.tsx +366 -0
- package/src/components/debug/pages/LLMCallPage.tsx +32 -0
- package/src/components/debug/pages/LLMCallsPage.tsx +473 -0
- package/src/components/debug/pages/LogsPage.tsx +199 -0
- package/src/components/debug/pages/MailboxPage.tsx +232 -0
- package/src/components/debug/pages/ServicesPage.tsx +193 -0
- package/src/components/debug/pages/TimelinePage.tsx +569 -0
- package/src/components/debug/pages/UserChatPage.tsx +250 -0
- package/src/components/debug/pages/index.ts +13 -0
- package/src/index.ts +55 -0
- package/src/lib/domain-utils.ts +12 -0
- package/src/providers/EventPollingProvider.tsx +60 -0
- package/src/stores/event-store.ts +497 -0
- 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
|
+
}
|