@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,1295 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AssistantConversationMessageView,
|
|
3
|
+
ConversationMessageView,
|
|
4
|
+
ToolCallView,
|
|
5
|
+
ToolConversationMessageView,
|
|
6
|
+
} from "@roj-ai/shared";
|
|
7
|
+
import { AgentId } from "@roj-ai/shared";
|
|
8
|
+
import { type FormEvent, useCallback, useMemo, useState } from "react";
|
|
9
|
+
import { api, unwrap } from "@roj-ai/client";
|
|
10
|
+
import { useAgentDetail, useEventStore } from "../../../stores/event-store";
|
|
11
|
+
import { formatDuration } from "../../../utils/format";
|
|
12
|
+
import {
|
|
13
|
+
DebugLink,
|
|
14
|
+
useDebugParams,
|
|
15
|
+
useDebugSessionId,
|
|
16
|
+
} from "../DebugNavigation";
|
|
17
|
+
|
|
18
|
+
function formatTime(ts: number | undefined): string | null {
|
|
19
|
+
if (ts === undefined) return null;
|
|
20
|
+
return new Date(ts).toLocaleTimeString();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function Timestamp({ value }: { value: number | undefined }) {
|
|
24
|
+
const formatted = formatTime(value);
|
|
25
|
+
if (!formatted) return null;
|
|
26
|
+
return (
|
|
27
|
+
<span className="text-[10px] text-gray-400 tabular-nums ml-auto shrink-0">
|
|
28
|
+
{formatted}
|
|
29
|
+
</span>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function CopyButton({ text }: { text: string }) {
|
|
34
|
+
const [copied, setCopied] = useState(false);
|
|
35
|
+
|
|
36
|
+
const handleCopy = useCallback(
|
|
37
|
+
(e: React.MouseEvent) => {
|
|
38
|
+
e.stopPropagation();
|
|
39
|
+
navigator.clipboard.writeText(text);
|
|
40
|
+
setCopied(true);
|
|
41
|
+
setTimeout(() => setCopied(false), 1500);
|
|
42
|
+
},
|
|
43
|
+
[text],
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<button
|
|
48
|
+
type="button"
|
|
49
|
+
onClick={handleCopy}
|
|
50
|
+
className="absolute top-1.5 right-1.5 opacity-0 group-hover/code:opacity-100 transition-opacity p-1 rounded bg-white/80 hover:bg-gray-100 border border-gray-200 cursor-pointer"
|
|
51
|
+
title="Copy to clipboard"
|
|
52
|
+
>
|
|
53
|
+
{copied ? (
|
|
54
|
+
<svg
|
|
55
|
+
className="w-3.5 h-3.5 text-green-600"
|
|
56
|
+
fill="none"
|
|
57
|
+
stroke="currentColor"
|
|
58
|
+
viewBox="0 0 24 24"
|
|
59
|
+
>
|
|
60
|
+
<path
|
|
61
|
+
strokeLinecap="round"
|
|
62
|
+
strokeLinejoin="round"
|
|
63
|
+
strokeWidth={2}
|
|
64
|
+
d="M5 13l4 4L19 7"
|
|
65
|
+
/>
|
|
66
|
+
</svg>
|
|
67
|
+
) : (
|
|
68
|
+
<svg
|
|
69
|
+
className="w-3.5 h-3.5 text-gray-500"
|
|
70
|
+
fill="none"
|
|
71
|
+
stroke="currentColor"
|
|
72
|
+
viewBox="0 0 24 24"
|
|
73
|
+
>
|
|
74
|
+
<path
|
|
75
|
+
strokeLinecap="round"
|
|
76
|
+
strokeLinejoin="round"
|
|
77
|
+
strokeWidth={2}
|
|
78
|
+
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
|
|
79
|
+
/>
|
|
80
|
+
</svg>
|
|
81
|
+
)}
|
|
82
|
+
</button>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function CodeBlock({
|
|
87
|
+
children,
|
|
88
|
+
variant = "default",
|
|
89
|
+
}: {
|
|
90
|
+
children: string;
|
|
91
|
+
variant?: "default" | "success" | "error";
|
|
92
|
+
}) {
|
|
93
|
+
const styles = {
|
|
94
|
+
default: "bg-gray-50 border-gray-200 text-gray-800",
|
|
95
|
+
success: "bg-green-50 border-green-200 text-gray-800",
|
|
96
|
+
error: "bg-red-50 border-red-200 text-red-800",
|
|
97
|
+
};
|
|
98
|
+
return (
|
|
99
|
+
<div className="relative group/code">
|
|
100
|
+
<pre
|
|
101
|
+
className={`p-2 rounded-lg border overflow-x-auto text-[11px] font-mono whitespace-pre-wrap break-words ${styles[variant]}`}
|
|
102
|
+
>
|
|
103
|
+
{children}
|
|
104
|
+
</pre>
|
|
105
|
+
<CopyButton text={children} />
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function AgentDetailPage({
|
|
111
|
+
agentId: agentIdProp,
|
|
112
|
+
}: {
|
|
113
|
+
agentId?: string;
|
|
114
|
+
} = {}) {
|
|
115
|
+
const { agentId: agentIdParam } = useDebugParams<{ agentId: string }>();
|
|
116
|
+
const agentId = agentIdProp ?? agentIdParam ?? "";
|
|
117
|
+
const sessionId = useDebugSessionId();
|
|
118
|
+
|
|
119
|
+
const detail = useAgentDetail(AgentId(agentId));
|
|
120
|
+
const isLoading = useEventStore((s) => s.isLoading);
|
|
121
|
+
|
|
122
|
+
const handleRewind = useCallback(
|
|
123
|
+
async (messageIndex: number) => {
|
|
124
|
+
if (!sessionId || !agentId) return;
|
|
125
|
+
unwrap(
|
|
126
|
+
await api.call("agents.rewind", {
|
|
127
|
+
sessionId,
|
|
128
|
+
agentId: AgentId(agentId),
|
|
129
|
+
messageIndex,
|
|
130
|
+
}),
|
|
131
|
+
);
|
|
132
|
+
},
|
|
133
|
+
[sessionId, agentId],
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
if (isLoading) {
|
|
137
|
+
return <div className="text-gray-400 text-sm">Loading...</div>;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!detail) {
|
|
141
|
+
return <div className="text-gray-400 text-sm">Agent not found</div>;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const { counters } = detail;
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<div className="text-sm">
|
|
148
|
+
{/* Two-column layout */}
|
|
149
|
+
<div className="flex gap-5 items-start">
|
|
150
|
+
{/* Left column - Conversation (2/3) */}
|
|
151
|
+
<div className="flex-[2] min-w-0 space-y-5">
|
|
152
|
+
{/* Conversation History Section */}
|
|
153
|
+
<CollapsibleSection
|
|
154
|
+
title="Conversation History"
|
|
155
|
+
count={detail.conversationHistory.length}
|
|
156
|
+
defaultOpen
|
|
157
|
+
>
|
|
158
|
+
{detail.conversationHistory.length === 0 ? (
|
|
159
|
+
<div className="text-gray-400">Empty</div>
|
|
160
|
+
) : (
|
|
161
|
+
<ConversationGroups
|
|
162
|
+
messages={detail.conversationHistory}
|
|
163
|
+
onRewind={handleRewind}
|
|
164
|
+
/>
|
|
165
|
+
)}
|
|
166
|
+
</CollapsibleSection>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
{/* Right column - Sidebar (1/3) */}
|
|
170
|
+
<div className="flex-1 min-w-0 space-y-4 sticky top-0">
|
|
171
|
+
{/* Agent Header Card */}
|
|
172
|
+
<div className="bg-white rounded-2xl shadow-card p-4">
|
|
173
|
+
<div className="flex items-center gap-3 mb-4">
|
|
174
|
+
<div className="w-9 h-9 rounded-xl bg-accent-peri/15 flex items-center justify-center shrink-0">
|
|
175
|
+
<svg
|
|
176
|
+
className="w-4.5 h-4.5 text-accent-peri"
|
|
177
|
+
fill="none"
|
|
178
|
+
stroke="currentColor"
|
|
179
|
+
viewBox="0 0 24 24"
|
|
180
|
+
>
|
|
181
|
+
<path
|
|
182
|
+
strokeLinecap="round"
|
|
183
|
+
strokeLinejoin="round"
|
|
184
|
+
strokeWidth={1.5}
|
|
185
|
+
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"
|
|
186
|
+
/>
|
|
187
|
+
</svg>
|
|
188
|
+
</div>
|
|
189
|
+
<div className="flex-1 min-w-0">
|
|
190
|
+
<div className="font-bold text-gray-900">
|
|
191
|
+
{detail.definitionName}
|
|
192
|
+
</div>
|
|
193
|
+
<div className="text-[11px] text-gray-400 font-mono truncate">
|
|
194
|
+
{detail.id}
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
<StatusBadge status={detail.status} />
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
{detail.parentId && (
|
|
201
|
+
<div className="text-[11px] text-gray-400 mb-3">
|
|
202
|
+
Parent:{" "}
|
|
203
|
+
<DebugLink
|
|
204
|
+
to={`agents/${detail.parentId}`}
|
|
205
|
+
className="text-accent-peri hover:underline font-mono"
|
|
206
|
+
>
|
|
207
|
+
{detail.parentId.slice(0, 12)}...
|
|
208
|
+
</DebugLink>
|
|
209
|
+
</div>
|
|
210
|
+
)}
|
|
211
|
+
|
|
212
|
+
{detail.pauseReason && (
|
|
213
|
+
<div className="bg-red-50 border border-red-200 rounded-xl px-3 py-2 mb-3 flex items-center gap-2">
|
|
214
|
+
<div className="w-2 h-2 rounded-full bg-red-500 shrink-0" />
|
|
215
|
+
<span className="text-[11px] font-semibold text-red-700 flex-1">
|
|
216
|
+
Paused
|
|
217
|
+
{detail.pauseMessage
|
|
218
|
+
? `: ${detail.pauseMessage}`
|
|
219
|
+
: ` (${detail.pauseReason})`}
|
|
220
|
+
</span>
|
|
221
|
+
<ResumeAgentButton sessionId={sessionId} agentId={agentId!} />
|
|
222
|
+
</div>
|
|
223
|
+
)}
|
|
224
|
+
|
|
225
|
+
{/* Counters Grid */}
|
|
226
|
+
<div className="grid grid-cols-2 gap-2">
|
|
227
|
+
<CounterItem label="Inferences" value={counters.inferenceCount} />
|
|
228
|
+
<CounterItem label="Tool Calls" value={counters.toolCallCount} />
|
|
229
|
+
<CounterItem label="Spawned" value={counters.spawnedAgentCount} />
|
|
230
|
+
<CounterItem
|
|
231
|
+
label="Messages"
|
|
232
|
+
value={counters.messagesSentCount}
|
|
233
|
+
/>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
{/* Cost */}
|
|
237
|
+
{detail.cost > 0 && (
|
|
238
|
+
<div className="mt-2 bg-emerald-50 rounded-xl px-3 py-2.5 border border-emerald-100 flex items-center gap-2">
|
|
239
|
+
<svg
|
|
240
|
+
className="w-4 h-4 text-emerald-600 shrink-0"
|
|
241
|
+
fill="none"
|
|
242
|
+
stroke="currentColor"
|
|
243
|
+
viewBox="0 0 24 24"
|
|
244
|
+
>
|
|
245
|
+
<path
|
|
246
|
+
strokeLinecap="round"
|
|
247
|
+
strokeLinejoin="round"
|
|
248
|
+
strokeWidth={1.5}
|
|
249
|
+
d="M12 6v12m-3-2.818l.879.659c1.171.879 3.07.879 4.242 0 1.172-.879 1.172-2.303 0-3.182C13.536 12.219 12.768 12 12 12c-.725 0-1.45-.22-2.003-.659-1.106-.879-1.106-2.303 0-3.182s2.9-.879 4.006 0l.415.33M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
250
|
+
/>
|
|
251
|
+
</svg>
|
|
252
|
+
<span className="text-[10px] text-emerald-600 uppercase tracking-wider font-medium">
|
|
253
|
+
Cost
|
|
254
|
+
</span>
|
|
255
|
+
<span className="ml-auto text-sm font-bold tabular-nums text-emerald-700">
|
|
256
|
+
${detail.cost.toFixed(4)}
|
|
257
|
+
</span>
|
|
258
|
+
</div>
|
|
259
|
+
)}
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
{/* Pending Tool Calls */}
|
|
263
|
+
<CollapsibleSection
|
|
264
|
+
title="Pending Tool Calls"
|
|
265
|
+
count={detail.pendingToolCalls.length}
|
|
266
|
+
warnIfPositive
|
|
267
|
+
defaultOpen={detail.pendingToolCalls.length > 0}
|
|
268
|
+
>
|
|
269
|
+
{detail.pendingToolCalls.length === 0 ? (
|
|
270
|
+
<div className="text-gray-400">None</div>
|
|
271
|
+
) : (
|
|
272
|
+
<div className="space-y-2">
|
|
273
|
+
{detail.pendingToolCalls.map((tool) => (
|
|
274
|
+
<ToolCallCard key={tool.id} tool={tool} />
|
|
275
|
+
))}
|
|
276
|
+
</div>
|
|
277
|
+
)}
|
|
278
|
+
</CollapsibleSection>
|
|
279
|
+
|
|
280
|
+
{/* Mailbox */}
|
|
281
|
+
<CollapsibleSection
|
|
282
|
+
title="Mailbox"
|
|
283
|
+
count={detail.mailbox.length}
|
|
284
|
+
defaultOpen={detail.mailbox.some((m) => !m.consumed)}
|
|
285
|
+
>
|
|
286
|
+
{detail.mailbox.length === 0 ? (
|
|
287
|
+
<div className="text-gray-400">Empty</div>
|
|
288
|
+
) : (
|
|
289
|
+
<div className="space-y-2">
|
|
290
|
+
{detail.mailbox.map((msg) => (
|
|
291
|
+
<div
|
|
292
|
+
key={msg.id}
|
|
293
|
+
className={`rounded-xl px-3 py-2.5 border ${
|
|
294
|
+
msg.consumed
|
|
295
|
+
? "bg-gray-50 border-gray-100"
|
|
296
|
+
: "bg-amber-50 border-amber-200"
|
|
297
|
+
}`}
|
|
298
|
+
>
|
|
299
|
+
<div className="flex items-center gap-2 text-[11px] text-gray-400 mb-1.5 flex-wrap">
|
|
300
|
+
<span className="font-medium">From:</span>
|
|
301
|
+
{msg.from !== "user" &&
|
|
302
|
+
msg.from !== "orchestrator" &&
|
|
303
|
+
msg.from !== "communicator" ? (
|
|
304
|
+
<DebugLink
|
|
305
|
+
to={`agents/${msg.from}`}
|
|
306
|
+
className="text-accent-peri hover:underline font-mono"
|
|
307
|
+
>
|
|
308
|
+
{msg.from.slice(0, 8)}...
|
|
309
|
+
</DebugLink>
|
|
310
|
+
) : (
|
|
311
|
+
<span className="font-semibold text-gray-500">
|
|
312
|
+
{msg.from}
|
|
313
|
+
</span>
|
|
314
|
+
)}
|
|
315
|
+
{msg.consumed && (
|
|
316
|
+
<span className="text-[10px] bg-green-100 text-green-700 px-1.5 py-0.5 rounded-full font-medium">
|
|
317
|
+
consumed
|
|
318
|
+
</span>
|
|
319
|
+
)}
|
|
320
|
+
<span className="text-[10px] text-gray-400 tabular-nums ml-auto">
|
|
321
|
+
{new Date(msg.timestamp).toLocaleTimeString()}
|
|
322
|
+
</span>
|
|
323
|
+
</div>
|
|
324
|
+
<div className="whitespace-pre-wrap break-words text-gray-700 text-[12px] leading-relaxed">
|
|
325
|
+
{msg.content}
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
))}
|
|
329
|
+
</div>
|
|
330
|
+
)}
|
|
331
|
+
</CollapsibleSection>
|
|
332
|
+
|
|
333
|
+
{/* Loaded Skills */}
|
|
334
|
+
{detail.loadedSkills.length > 0 && (
|
|
335
|
+
<CollapsibleSection
|
|
336
|
+
title="Loaded Skills"
|
|
337
|
+
count={detail.loadedSkills.length}
|
|
338
|
+
defaultOpen={false}
|
|
339
|
+
>
|
|
340
|
+
<div className="flex flex-wrap gap-1.5">
|
|
341
|
+
{detail.loadedSkills.map((skill) => (
|
|
342
|
+
<div
|
|
343
|
+
key={skill.id}
|
|
344
|
+
className="bg-accent-peri/10 text-gray-700 px-2 py-0.5 rounded-full text-[11px] font-medium"
|
|
345
|
+
>
|
|
346
|
+
{skill.name}
|
|
347
|
+
<span className="text-gray-400 ml-1">
|
|
348
|
+
{formatDuration(Date.now() - skill.loadedAt)} ago
|
|
349
|
+
</span>
|
|
350
|
+
</div>
|
|
351
|
+
))}
|
|
352
|
+
</div>
|
|
353
|
+
</CollapsibleSection>
|
|
354
|
+
)}
|
|
355
|
+
|
|
356
|
+
{/* Typed Input */}
|
|
357
|
+
{detail.typedInput !== undefined && (
|
|
358
|
+
<CollapsibleSection title="Typed Input" defaultOpen={false}>
|
|
359
|
+
<pre className="bg-gray-50 rounded-xl px-3 py-2.5 text-[11px] font-mono overflow-x-auto text-gray-700">
|
|
360
|
+
{JSON.stringify(detail.typedInput, null, 2)}
|
|
361
|
+
</pre>
|
|
362
|
+
</CollapsibleSection>
|
|
363
|
+
)}
|
|
364
|
+
|
|
365
|
+
{/* Send Debug Message */}
|
|
366
|
+
<SendMessageForm sessionId={sessionId} agentId={agentId!} />
|
|
367
|
+
</div>
|
|
368
|
+
</div>
|
|
369
|
+
</div>
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ============================================================================
|
|
374
|
+
// CollapsibleSection
|
|
375
|
+
// ============================================================================
|
|
376
|
+
|
|
377
|
+
function CollapsibleSection({
|
|
378
|
+
title,
|
|
379
|
+
count,
|
|
380
|
+
warnIfPositive,
|
|
381
|
+
defaultOpen = true,
|
|
382
|
+
children,
|
|
383
|
+
}: {
|
|
384
|
+
title: string;
|
|
385
|
+
count?: number;
|
|
386
|
+
warnIfPositive?: boolean;
|
|
387
|
+
defaultOpen?: boolean;
|
|
388
|
+
children: React.ReactNode;
|
|
389
|
+
}) {
|
|
390
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
391
|
+
const isWarning = warnIfPositive && count !== undefined && count > 0;
|
|
392
|
+
|
|
393
|
+
return (
|
|
394
|
+
<div className="bg-white rounded-2xl shadow-card overflow-hidden">
|
|
395
|
+
<button
|
|
396
|
+
type="button"
|
|
397
|
+
onClick={() => setOpen(!open)}
|
|
398
|
+
className="w-full flex items-center gap-2 px-4 py-3 text-left hover:bg-gray-50 transition-colors cursor-pointer"
|
|
399
|
+
>
|
|
400
|
+
<svg
|
|
401
|
+
className={`w-3.5 h-3.5 text-gray-400 transition-transform ${open ? "rotate-90" : ""}`}
|
|
402
|
+
fill="none"
|
|
403
|
+
stroke="currentColor"
|
|
404
|
+
viewBox="0 0 24 24"
|
|
405
|
+
>
|
|
406
|
+
<path
|
|
407
|
+
strokeLinecap="round"
|
|
408
|
+
strokeLinejoin="round"
|
|
409
|
+
strokeWidth={2}
|
|
410
|
+
d="M9 5l7 7-7 7"
|
|
411
|
+
/>
|
|
412
|
+
</svg>
|
|
413
|
+
<span className="text-sm font-semibold text-gray-900">{title}</span>
|
|
414
|
+
{count !== undefined && (
|
|
415
|
+
<span
|
|
416
|
+
className={`text-[11px] px-2 py-0.5 rounded-full font-medium tabular-nums ${
|
|
417
|
+
isWarning
|
|
418
|
+
? "bg-amber-100 text-amber-700"
|
|
419
|
+
: "bg-gray-100 text-gray-500"
|
|
420
|
+
}`}
|
|
421
|
+
>
|
|
422
|
+
{count}
|
|
423
|
+
</span>
|
|
424
|
+
)}
|
|
425
|
+
</button>
|
|
426
|
+
{open && <div className="px-4 pb-4">{children}</div>}
|
|
427
|
+
</div>
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ============================================================================
|
|
432
|
+
// CounterItem
|
|
433
|
+
// ============================================================================
|
|
434
|
+
|
|
435
|
+
const counterIcons: Record<string, React.ReactNode> = {
|
|
436
|
+
Inferences: (
|
|
437
|
+
<svg
|
|
438
|
+
className="w-3.5 h-3.5"
|
|
439
|
+
fill="none"
|
|
440
|
+
stroke="currentColor"
|
|
441
|
+
viewBox="0 0 24 24"
|
|
442
|
+
>
|
|
443
|
+
<path
|
|
444
|
+
strokeLinecap="round"
|
|
445
|
+
strokeLinejoin="round"
|
|
446
|
+
strokeWidth={1.5}
|
|
447
|
+
d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.455 2.456L21.75 6l-1.036.259a3.375 3.375 0 00-2.455 2.456z"
|
|
448
|
+
/>
|
|
449
|
+
</svg>
|
|
450
|
+
),
|
|
451
|
+
"Tool Calls": (
|
|
452
|
+
<svg
|
|
453
|
+
className="w-3.5 h-3.5"
|
|
454
|
+
fill="none"
|
|
455
|
+
stroke="currentColor"
|
|
456
|
+
viewBox="0 0 24 24"
|
|
457
|
+
>
|
|
458
|
+
<path
|
|
459
|
+
strokeLinecap="round"
|
|
460
|
+
strokeLinejoin="round"
|
|
461
|
+
strokeWidth={1.5}
|
|
462
|
+
d="M11.42 15.17l-5.1-5.1a1.5 1.5 0 010-2.12l.88-.88a1.5 1.5 0 012.12 0l2.83 2.83 5.66-5.66a1.5 1.5 0 012.12 0l.88.88a1.5 1.5 0 010 2.12l-7.07 7.07a1.5 1.5 0 01-2.12-.14z"
|
|
463
|
+
/>
|
|
464
|
+
</svg>
|
|
465
|
+
),
|
|
466
|
+
Spawned: (
|
|
467
|
+
<svg
|
|
468
|
+
className="w-3.5 h-3.5"
|
|
469
|
+
fill="none"
|
|
470
|
+
stroke="currentColor"
|
|
471
|
+
viewBox="0 0 24 24"
|
|
472
|
+
>
|
|
473
|
+
<path
|
|
474
|
+
strokeLinecap="round"
|
|
475
|
+
strokeLinejoin="round"
|
|
476
|
+
strokeWidth={1.5}
|
|
477
|
+
d="M18 18.72a9.094 9.094 0 003.741-.479 3 3 0 00-4.682-2.72m.94 3.198l.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0112 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 016 18.719m12 0a5.971 5.971 0 00-.941-3.197m0 0A5.995 5.995 0 0012 12.75a5.995 5.995 0 00-5.058 2.772m0 0a3 3 0 00-4.681 2.72 8.986 8.986 0 003.74.477m.94-3.197a5.971 5.971 0 00-.94 3.197M15 6.75a3 3 0 11-6 0 3 3 0 016 0zm6 3a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0zm-13.5 0a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0z"
|
|
478
|
+
/>
|
|
479
|
+
</svg>
|
|
480
|
+
),
|
|
481
|
+
Messages: (
|
|
482
|
+
<svg
|
|
483
|
+
className="w-3.5 h-3.5"
|
|
484
|
+
fill="none"
|
|
485
|
+
stroke="currentColor"
|
|
486
|
+
viewBox="0 0 24 24"
|
|
487
|
+
>
|
|
488
|
+
<path
|
|
489
|
+
strokeLinecap="round"
|
|
490
|
+
strokeLinejoin="round"
|
|
491
|
+
strokeWidth={1.5}
|
|
492
|
+
d="M8.625 12a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H8.25m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H12m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 01-2.555-.337A5.972 5.972 0 015.41 20.97a5.969 5.969 0 01-.474-.065 4.48 4.48 0 00.978-2.025c.09-.457-.133-.901-.467-1.226C3.93 16.178 3 14.189 3 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25z"
|
|
493
|
+
/>
|
|
494
|
+
</svg>
|
|
495
|
+
),
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
function CounterItem({ label, value }: { label: string; value: number }) {
|
|
499
|
+
return (
|
|
500
|
+
<div className="bg-gray-50 rounded-xl px-3 py-2.5 border border-gray-100">
|
|
501
|
+
<div className="flex items-center gap-1.5 mb-1">
|
|
502
|
+
<span className="text-gray-400">{counterIcons[label]}</span>
|
|
503
|
+
<span className="text-[10px] text-gray-400 uppercase tracking-wider font-medium">
|
|
504
|
+
{label}
|
|
505
|
+
</span>
|
|
506
|
+
</div>
|
|
507
|
+
<div className="text-lg font-bold tabular-nums text-gray-900">
|
|
508
|
+
{value}
|
|
509
|
+
</div>
|
|
510
|
+
</div>
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// ============================================================================
|
|
515
|
+
// ToolCallCard
|
|
516
|
+
// ============================================================================
|
|
517
|
+
|
|
518
|
+
function ToolCallCard({ tool }: { tool: ToolCallView }) {
|
|
519
|
+
const [expanded, setExpanded] = useState(false);
|
|
520
|
+
|
|
521
|
+
return (
|
|
522
|
+
<div className="rounded-xl border border-gray-100 bg-gray-50 overflow-hidden">
|
|
523
|
+
<button
|
|
524
|
+
type="button"
|
|
525
|
+
onClick={() => setExpanded(!expanded)}
|
|
526
|
+
className="w-full flex items-center gap-2 px-3 py-2.5 text-left hover:bg-gray-100 transition-colors cursor-pointer"
|
|
527
|
+
>
|
|
528
|
+
<ToolStatusDot status={tool.status} />
|
|
529
|
+
<span className="font-mono font-medium text-sm text-gray-900">
|
|
530
|
+
{tool.name}
|
|
531
|
+
</span>
|
|
532
|
+
<ToolStatusBadge status={tool.status} />
|
|
533
|
+
<span className="text-[10px] text-gray-400 font-mono ml-auto">
|
|
534
|
+
{tool.id.slice(0, 12)}
|
|
535
|
+
</span>
|
|
536
|
+
<svg
|
|
537
|
+
className={`w-3 h-3 text-gray-400 transition-transform shrink-0 ${expanded ? "rotate-90" : ""}`}
|
|
538
|
+
fill="none"
|
|
539
|
+
stroke="currentColor"
|
|
540
|
+
viewBox="0 0 24 24"
|
|
541
|
+
>
|
|
542
|
+
<path
|
|
543
|
+
strokeLinecap="round"
|
|
544
|
+
strokeLinejoin="round"
|
|
545
|
+
strokeWidth={2}
|
|
546
|
+
d="M9 5l7 7-7 7"
|
|
547
|
+
/>
|
|
548
|
+
</svg>
|
|
549
|
+
</button>
|
|
550
|
+
{expanded && (
|
|
551
|
+
<div className="px-3 pb-3 space-y-2 border-t border-gray-100">
|
|
552
|
+
{tool.input !== undefined && (
|
|
553
|
+
<div className="mt-2">
|
|
554
|
+
<div className="text-[10px] text-gray-500 uppercase tracking-wider font-medium mb-1">
|
|
555
|
+
Input
|
|
556
|
+
</div>
|
|
557
|
+
<CodeBlock>{JSON.stringify(tool.input, null, 2)}</CodeBlock>
|
|
558
|
+
</div>
|
|
559
|
+
)}
|
|
560
|
+
{tool.result !== undefined && (
|
|
561
|
+
<div>
|
|
562
|
+
<div className="text-[10px] text-green-700 uppercase tracking-wider font-medium mb-1">
|
|
563
|
+
Result
|
|
564
|
+
</div>
|
|
565
|
+
<CodeBlock variant="success">
|
|
566
|
+
{typeof tool.result === "string"
|
|
567
|
+
? tool.result
|
|
568
|
+
: JSON.stringify(tool.result, null, 2)}
|
|
569
|
+
</CodeBlock>
|
|
570
|
+
</div>
|
|
571
|
+
)}
|
|
572
|
+
{tool.error && (
|
|
573
|
+
<div>
|
|
574
|
+
<div className="text-[10px] text-red-700 uppercase tracking-wider font-medium mb-1">
|
|
575
|
+
Error
|
|
576
|
+
</div>
|
|
577
|
+
<CodeBlock variant="error">{tool.error}</CodeBlock>
|
|
578
|
+
</div>
|
|
579
|
+
)}
|
|
580
|
+
</div>
|
|
581
|
+
)}
|
|
582
|
+
</div>
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// ============================================================================
|
|
587
|
+
// Conversation Grouping
|
|
588
|
+
// ============================================================================
|
|
589
|
+
|
|
590
|
+
interface ToolInteraction {
|
|
591
|
+
call: NonNullable<AssistantConversationMessageView["toolCalls"]>[number];
|
|
592
|
+
response?: ToolConversationMessageView;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
type ConversationGroup =
|
|
596
|
+
| { type: "message"; message: ConversationMessageView; messageIndex: number }
|
|
597
|
+
| {
|
|
598
|
+
type: "assistant-with-tools";
|
|
599
|
+
assistantMessage: AssistantConversationMessageView;
|
|
600
|
+
toolInteractions: ToolInteraction[];
|
|
601
|
+
messageIndex: number;
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
function groupConversation(
|
|
605
|
+
messages: ConversationMessageView[],
|
|
606
|
+
): ConversationGroup[] {
|
|
607
|
+
const groups: ConversationGroup[] = [];
|
|
608
|
+
let i = 0;
|
|
609
|
+
|
|
610
|
+
while (i < messages.length) {
|
|
611
|
+
const msg = messages[i];
|
|
612
|
+
|
|
613
|
+
if (msg.role === "assistant" && msg.toolCalls && msg.toolCalls.length > 0) {
|
|
614
|
+
const toolResponses = new Map<string, ToolConversationMessageView>();
|
|
615
|
+
let j = i + 1;
|
|
616
|
+
while (j < messages.length) {
|
|
617
|
+
const nextMsg = messages[j];
|
|
618
|
+
if (nextMsg.role !== "tool") break;
|
|
619
|
+
toolResponses.set(nextMsg.toolCallId, nextMsg);
|
|
620
|
+
j++;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
groups.push({
|
|
624
|
+
type: "assistant-with-tools",
|
|
625
|
+
assistantMessage: msg,
|
|
626
|
+
toolInteractions: msg.toolCalls.map((tc) => ({
|
|
627
|
+
call: tc,
|
|
628
|
+
response: toolResponses.get(tc.id),
|
|
629
|
+
})),
|
|
630
|
+
messageIndex: i,
|
|
631
|
+
});
|
|
632
|
+
i = j;
|
|
633
|
+
} else {
|
|
634
|
+
groups.push({ type: "message", message: msg, messageIndex: i });
|
|
635
|
+
i++;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return groups;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function ConversationGroups({
|
|
643
|
+
messages,
|
|
644
|
+
onRewind,
|
|
645
|
+
}: {
|
|
646
|
+
messages: ConversationMessageView[];
|
|
647
|
+
onRewind?: (messageIndex: number) => void;
|
|
648
|
+
}) {
|
|
649
|
+
const groups = useMemo(() => groupConversation(messages), [messages]);
|
|
650
|
+
|
|
651
|
+
// Compute previous assistant message's total input for cache validation.
|
|
652
|
+
// promptTokens (= Anthropic input_tokens) is the TOTAL input including cached/cache-write portions.
|
|
653
|
+
const prevTotalInputs = useMemo(() => {
|
|
654
|
+
const result: Array<number | undefined> = [];
|
|
655
|
+
let prevTotal: number | undefined;
|
|
656
|
+
for (const group of groups) {
|
|
657
|
+
result.push(prevTotal);
|
|
658
|
+
if (group.type === "assistant-with-tools") {
|
|
659
|
+
const msg = group.assistantMessage;
|
|
660
|
+
if (msg.promptTokens !== undefined) {
|
|
661
|
+
prevTotal = msg.promptTokens;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return result;
|
|
666
|
+
}, [groups]);
|
|
667
|
+
|
|
668
|
+
return (
|
|
669
|
+
<div className="space-y-3">
|
|
670
|
+
{groups.map((group, idx) => {
|
|
671
|
+
if (group.type === "message") {
|
|
672
|
+
return <ConversationMessage key={idx} msg={group.message} />;
|
|
673
|
+
}
|
|
674
|
+
return (
|
|
675
|
+
<AssistantToolGroup
|
|
676
|
+
key={idx}
|
|
677
|
+
group={group}
|
|
678
|
+
expectedCacheRead={prevTotalInputs[idx]}
|
|
679
|
+
onRewind={onRewind}
|
|
680
|
+
/>
|
|
681
|
+
);
|
|
682
|
+
})}
|
|
683
|
+
</div>
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// ============================================================================
|
|
688
|
+
// AssistantToolGroup
|
|
689
|
+
// ============================================================================
|
|
690
|
+
|
|
691
|
+
function formatTokens(n: number): string {
|
|
692
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
693
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`;
|
|
694
|
+
return n.toString();
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function CacheStatsLine({
|
|
698
|
+
msg,
|
|
699
|
+
expectedCacheRead,
|
|
700
|
+
}: {
|
|
701
|
+
msg: AssistantConversationMessageView;
|
|
702
|
+
expectedCacheRead?: number;
|
|
703
|
+
}) {
|
|
704
|
+
if (msg.promptTokens === undefined) return null;
|
|
705
|
+
|
|
706
|
+
// promptTokens (= Anthropic input_tokens) is TOTAL input including cached/cache-write.
|
|
707
|
+
// Uncached = total - cached - cache_write (the remainder billed at full price).
|
|
708
|
+
const totalInput = msg.promptTokens ?? 0;
|
|
709
|
+
const cached = msg.cachedTokens ?? 0;
|
|
710
|
+
const cacheWrite = msg.cacheWriteTokens ?? 0;
|
|
711
|
+
const uncached = totalInput - cached - cacheWrite;
|
|
712
|
+
|
|
713
|
+
// Cache validation: compare actual cache read with expected (prev call total input)
|
|
714
|
+
let cacheValidation: { ratio: number; ok: boolean } | undefined;
|
|
715
|
+
if (expectedCacheRead !== undefined && expectedCacheRead > 0 && cached > 0) {
|
|
716
|
+
const ratio = cached / expectedCacheRead;
|
|
717
|
+
cacheValidation = { ratio, ok: ratio >= 0.9 };
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
return (
|
|
721
|
+
<div className="flex items-center gap-1.5 text-[10px] tabular-nums flex-wrap">
|
|
722
|
+
{uncached > 0 && (
|
|
723
|
+
<span className="text-gray-400">{formatTokens(uncached)} in</span>
|
|
724
|
+
)}
|
|
725
|
+
{cached > 0 && (
|
|
726
|
+
<span className="text-blue-500">
|
|
727
|
+
{uncached > 0 ? "+ " : ""}
|
|
728
|
+
{formatTokens(cached)} cached
|
|
729
|
+
</span>
|
|
730
|
+
)}
|
|
731
|
+
{cacheWrite > 0 && (
|
|
732
|
+
<span className="text-amber-500">
|
|
733
|
+
+ {formatTokens(cacheWrite)} write
|
|
734
|
+
</span>
|
|
735
|
+
)}
|
|
736
|
+
{cacheValidation && (
|
|
737
|
+
<>
|
|
738
|
+
<span className="text-gray-300">|</span>
|
|
739
|
+
<span
|
|
740
|
+
className={cacheValidation.ok ? "text-green-600" : "text-red-500"}
|
|
741
|
+
>
|
|
742
|
+
prev {formatTokens(expectedCacheRead!)} →{" "}
|
|
743
|
+
{(cacheValidation.ratio * 100).toFixed(0)}% hit
|
|
744
|
+
</span>
|
|
745
|
+
</>
|
|
746
|
+
)}
|
|
747
|
+
</div>
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function AssistantToolGroup({
|
|
752
|
+
group,
|
|
753
|
+
expectedCacheRead,
|
|
754
|
+
onRewind,
|
|
755
|
+
}: {
|
|
756
|
+
group: {
|
|
757
|
+
assistantMessage: AssistantConversationMessageView;
|
|
758
|
+
toolInteractions: ToolInteraction[];
|
|
759
|
+
messageIndex: number;
|
|
760
|
+
};
|
|
761
|
+
expectedCacheRead?: number;
|
|
762
|
+
onRewind?: (messageIndex: number) => void;
|
|
763
|
+
}) {
|
|
764
|
+
const { assistantMessage, toolInteractions } = group;
|
|
765
|
+
const [textExpanded, setTextExpanded] = useState(false);
|
|
766
|
+
const hasText = assistantMessage.content.trim().length > 0;
|
|
767
|
+
const isTextTruncated =
|
|
768
|
+
assistantMessage.content !== assistantMessage.fullContent;
|
|
769
|
+
const displayText = textExpanded
|
|
770
|
+
? assistantMessage.fullContent
|
|
771
|
+
: assistantMessage.content;
|
|
772
|
+
|
|
773
|
+
return (
|
|
774
|
+
<div className="rounded-xl bg-white border border-gray-100 border-l-[3px] border-l-green-500 px-3.5 py-3">
|
|
775
|
+
<div className="flex items-center gap-2 mb-1">
|
|
776
|
+
<span className="text-[10px] font-bold uppercase tracking-wider text-green-700">
|
|
777
|
+
assistant
|
|
778
|
+
</span>
|
|
779
|
+
{toolInteractions.length > 0 && (
|
|
780
|
+
<span className="text-[10px] bg-green-100 text-green-700 px-1.5 py-0.5 rounded-full font-medium">
|
|
781
|
+
{toolInteractions.length} tool
|
|
782
|
+
{toolInteractions.length !== 1 ? "s" : ""}
|
|
783
|
+
</span>
|
|
784
|
+
)}
|
|
785
|
+
{assistantMessage.cost !== undefined && assistantMessage.cost > 0 && (
|
|
786
|
+
<span className="text-[10px] text-emerald-600 font-medium tabular-nums">
|
|
787
|
+
${assistantMessage.cost.toFixed(4)}
|
|
788
|
+
</span>
|
|
789
|
+
)}
|
|
790
|
+
{assistantMessage.llmCallId && (
|
|
791
|
+
<DebugLink
|
|
792
|
+
to={`llm-calls/${assistantMessage.llmCallId}`}
|
|
793
|
+
className="text-[10px] text-accent-peri hover:underline font-medium"
|
|
794
|
+
>
|
|
795
|
+
view llm call →
|
|
796
|
+
</DebugLink>
|
|
797
|
+
)}
|
|
798
|
+
{onRewind && (
|
|
799
|
+
<button
|
|
800
|
+
type="button"
|
|
801
|
+
onClick={() => onRewind(group.messageIndex)}
|
|
802
|
+
className="text-[10px] text-orange-600 hover:underline font-medium cursor-pointer"
|
|
803
|
+
>
|
|
804
|
+
retry from here
|
|
805
|
+
</button>
|
|
806
|
+
)}
|
|
807
|
+
<Timestamp value={assistantMessage.timestamp} />
|
|
808
|
+
</div>
|
|
809
|
+
|
|
810
|
+
<CacheStatsLine
|
|
811
|
+
msg={assistantMessage}
|
|
812
|
+
expectedCacheRead={expectedCacheRead}
|
|
813
|
+
/>
|
|
814
|
+
|
|
815
|
+
{hasText && (
|
|
816
|
+
<div className="mt-1">
|
|
817
|
+
<div className="whitespace-pre-wrap break-words text-gray-700 text-[12px] leading-relaxed">
|
|
818
|
+
{displayText}
|
|
819
|
+
</div>
|
|
820
|
+
{isTextTruncated && (
|
|
821
|
+
<button
|
|
822
|
+
type="button"
|
|
823
|
+
onClick={() => setTextExpanded(!textExpanded)}
|
|
824
|
+
className="text-[11px] text-accent-peri hover:underline mt-1 font-medium cursor-pointer"
|
|
825
|
+
>
|
|
826
|
+
{textExpanded ? "Show less" : "Show more"}
|
|
827
|
+
</button>
|
|
828
|
+
)}
|
|
829
|
+
</div>
|
|
830
|
+
)}
|
|
831
|
+
|
|
832
|
+
<div className={`space-y-1.5 ${hasText ? "mt-2" : ""}`}>
|
|
833
|
+
{toolInteractions.map(({ call, response }) => (
|
|
834
|
+
<ToolInteractionCard key={call.id} call={call} response={response} />
|
|
835
|
+
))}
|
|
836
|
+
</div>
|
|
837
|
+
</div>
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// ============================================================================
|
|
842
|
+
// ToolInteractionCard
|
|
843
|
+
// ============================================================================
|
|
844
|
+
|
|
845
|
+
function oneLinePreview(value: unknown, maxLen = 80): string {
|
|
846
|
+
const str = typeof value === "string" ? value : JSON.stringify(value);
|
|
847
|
+
const line = str.replace(/\n/g, " ").trim();
|
|
848
|
+
return line.length > maxLen ? line.slice(0, maxLen) + "…" : line;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function ToolInteractionCard({
|
|
852
|
+
call,
|
|
853
|
+
response,
|
|
854
|
+
}: {
|
|
855
|
+
call: { id: string; name: string; input: unknown };
|
|
856
|
+
response?: ToolConversationMessageView;
|
|
857
|
+
}) {
|
|
858
|
+
const [expanded, setExpanded] = useState(false);
|
|
859
|
+
const [resultExpanded, setResultExpanded] = useState(false);
|
|
860
|
+
const status = response
|
|
861
|
+
? response.isError
|
|
862
|
+
? "failed"
|
|
863
|
+
: "completed"
|
|
864
|
+
: "pending";
|
|
865
|
+
const isResultTruncated =
|
|
866
|
+
response !== undefined && response.content !== response.fullContent;
|
|
867
|
+
const displayResult = resultExpanded
|
|
868
|
+
? response?.fullContent
|
|
869
|
+
: response?.content;
|
|
870
|
+
|
|
871
|
+
return (
|
|
872
|
+
<div
|
|
873
|
+
className={`rounded-lg border overflow-hidden ${
|
|
874
|
+
response?.isError
|
|
875
|
+
? "border-red-200 bg-red-50/50"
|
|
876
|
+
: "border-gray-200 bg-gray-50/50"
|
|
877
|
+
}`}
|
|
878
|
+
>
|
|
879
|
+
<button
|
|
880
|
+
type="button"
|
|
881
|
+
onClick={() => setExpanded(!expanded)}
|
|
882
|
+
className="w-full flex items-start gap-2 px-2.5 py-2 text-left hover:bg-white/60 transition-colors cursor-pointer"
|
|
883
|
+
>
|
|
884
|
+
<ToolStatusDot status={status} />
|
|
885
|
+
<div className="flex-1 min-w-0">
|
|
886
|
+
<div className="flex items-center gap-1.5">
|
|
887
|
+
<span className="font-mono font-medium text-[11px] text-gray-800">
|
|
888
|
+
{call.name}
|
|
889
|
+
</span>
|
|
890
|
+
{response?.isError && (
|
|
891
|
+
<span className="text-[9px] bg-red-100 text-red-700 px-1.5 py-0.5 rounded-full font-semibold">
|
|
892
|
+
error
|
|
893
|
+
</span>
|
|
894
|
+
)}
|
|
895
|
+
<Timestamp value={response?.timestamp} />
|
|
896
|
+
</div>
|
|
897
|
+
{!expanded && (
|
|
898
|
+
<div className="mt-0.5 space-y-0">
|
|
899
|
+
{call.input !== undefined && (
|
|
900
|
+
<div className="text-[10px] text-gray-500 font-mono truncate">
|
|
901
|
+
↑ {oneLinePreview(call.input)}
|
|
902
|
+
</div>
|
|
903
|
+
)}
|
|
904
|
+
{response && (
|
|
905
|
+
<div
|
|
906
|
+
className={`text-[10px] font-mono truncate ${response.isError ? "text-red-600" : "text-gray-500"}`}
|
|
907
|
+
>
|
|
908
|
+
↓ {oneLinePreview(response.content)}
|
|
909
|
+
</div>
|
|
910
|
+
)}
|
|
911
|
+
</div>
|
|
912
|
+
)}
|
|
913
|
+
</div>
|
|
914
|
+
<svg
|
|
915
|
+
className={`w-3 h-3 text-gray-400 transition-transform shrink-0 mt-0.5 ${expanded ? "rotate-90" : ""}`}
|
|
916
|
+
fill="none"
|
|
917
|
+
stroke="currentColor"
|
|
918
|
+
viewBox="0 0 24 24"
|
|
919
|
+
>
|
|
920
|
+
<path
|
|
921
|
+
strokeLinecap="round"
|
|
922
|
+
strokeLinejoin="round"
|
|
923
|
+
strokeWidth={2}
|
|
924
|
+
d="M9 5l7 7-7 7"
|
|
925
|
+
/>
|
|
926
|
+
</svg>
|
|
927
|
+
</button>
|
|
928
|
+
|
|
929
|
+
{expanded && (
|
|
930
|
+
<div className="px-2.5 pb-2.5 space-y-2 border-t border-gray-100">
|
|
931
|
+
{call.input !== undefined && (
|
|
932
|
+
<div className="mt-2">
|
|
933
|
+
<div className="text-[9px] text-gray-500 uppercase tracking-wider font-medium mb-1">
|
|
934
|
+
Input
|
|
935
|
+
</div>
|
|
936
|
+
<CodeBlock>{JSON.stringify(call.input, null, 2)}</CodeBlock>
|
|
937
|
+
</div>
|
|
938
|
+
)}
|
|
939
|
+
|
|
940
|
+
{response && !response.isError && (
|
|
941
|
+
<div>
|
|
942
|
+
<div className="text-[9px] text-green-700 uppercase tracking-wider font-medium mb-1">
|
|
943
|
+
Result
|
|
944
|
+
</div>
|
|
945
|
+
<CodeBlock variant="success">{displayResult ?? ""}</CodeBlock>
|
|
946
|
+
{isResultTruncated && (
|
|
947
|
+
<button
|
|
948
|
+
type="button"
|
|
949
|
+
onClick={(e) => {
|
|
950
|
+
e.stopPropagation();
|
|
951
|
+
setResultExpanded(!resultExpanded);
|
|
952
|
+
}}
|
|
953
|
+
className="text-[10px] text-accent-peri hover:underline mt-0.5 font-medium cursor-pointer"
|
|
954
|
+
>
|
|
955
|
+
{resultExpanded ? "Show less" : "Show more"}
|
|
956
|
+
</button>
|
|
957
|
+
)}
|
|
958
|
+
</div>
|
|
959
|
+
)}
|
|
960
|
+
|
|
961
|
+
{response?.isError && (
|
|
962
|
+
<div>
|
|
963
|
+
<div className="text-[9px] text-red-700 uppercase tracking-wider font-medium mb-1">
|
|
964
|
+
Error
|
|
965
|
+
</div>
|
|
966
|
+
<CodeBlock variant="error">{displayResult ?? ""}</CodeBlock>
|
|
967
|
+
{isResultTruncated && (
|
|
968
|
+
<button
|
|
969
|
+
type="button"
|
|
970
|
+
onClick={(e) => {
|
|
971
|
+
e.stopPropagation();
|
|
972
|
+
setResultExpanded(!resultExpanded);
|
|
973
|
+
}}
|
|
974
|
+
className="text-[10px] text-accent-peri hover:underline mt-0.5 font-medium cursor-pointer"
|
|
975
|
+
>
|
|
976
|
+
{resultExpanded ? "Show less" : "Show more"}
|
|
977
|
+
</button>
|
|
978
|
+
)}
|
|
979
|
+
</div>
|
|
980
|
+
)}
|
|
981
|
+
</div>
|
|
982
|
+
)}
|
|
983
|
+
</div>
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// ============================================================================
|
|
988
|
+
// ConversationMessage (user, system, orphan tool)
|
|
989
|
+
// ============================================================================
|
|
990
|
+
|
|
991
|
+
const roleStyles: Record<
|
|
992
|
+
string,
|
|
993
|
+
{ bg: string; leftBorder: string; label: string }
|
|
994
|
+
> = {
|
|
995
|
+
user: {
|
|
996
|
+
bg: "bg-white",
|
|
997
|
+
leftBorder: "border-l-blue-500",
|
|
998
|
+
label: "text-blue-600",
|
|
999
|
+
},
|
|
1000
|
+
assistant: {
|
|
1001
|
+
bg: "bg-white",
|
|
1002
|
+
leftBorder: "border-l-green-500",
|
|
1003
|
+
label: "text-green-700",
|
|
1004
|
+
},
|
|
1005
|
+
tool: {
|
|
1006
|
+
bg: "bg-white",
|
|
1007
|
+
leftBorder: "border-l-indigo-400",
|
|
1008
|
+
label: "text-indigo-600",
|
|
1009
|
+
},
|
|
1010
|
+
system: {
|
|
1011
|
+
bg: "bg-gray-50",
|
|
1012
|
+
leftBorder: "border-l-gray-300",
|
|
1013
|
+
label: "text-gray-500",
|
|
1014
|
+
},
|
|
1015
|
+
};
|
|
1016
|
+
|
|
1017
|
+
function ConversationMessage({ msg }: { msg: ConversationMessageView }) {
|
|
1018
|
+
const [expanded, setExpanded] = useState(false);
|
|
1019
|
+
const style = roleStyles[msg.role] ?? roleStyles.system;
|
|
1020
|
+
const isTruncated = msg.content !== msg.fullContent;
|
|
1021
|
+
const displayContent = expanded ? msg.fullContent : msg.content;
|
|
1022
|
+
const isError = msg.role === "tool" && msg.isError;
|
|
1023
|
+
|
|
1024
|
+
return (
|
|
1025
|
+
<div
|
|
1026
|
+
className={`rounded-xl border border-gray-100 border-l-[3px] px-3.5 py-3 ${
|
|
1027
|
+
isError
|
|
1028
|
+
? "bg-red-50 border-l-red-500"
|
|
1029
|
+
: `${style.bg} ${style.leftBorder}`
|
|
1030
|
+
}`}
|
|
1031
|
+
>
|
|
1032
|
+
<div className="flex items-center gap-2 mb-2">
|
|
1033
|
+
<span
|
|
1034
|
+
className={`text-[10px] font-bold uppercase tracking-wider ${isError ? "text-red-700" : style.label}`}
|
|
1035
|
+
>
|
|
1036
|
+
{msg.role}
|
|
1037
|
+
</span>
|
|
1038
|
+
{msg.role === "tool" && (
|
|
1039
|
+
<>
|
|
1040
|
+
<span className="text-[10px] text-gray-400 font-mono">
|
|
1041
|
+
{msg.toolCallId.slice(0, 12)}
|
|
1042
|
+
</span>
|
|
1043
|
+
{msg.isError && (
|
|
1044
|
+
<span className="text-[9px] bg-red-100 text-red-700 px-1.5 py-0.5 rounded-full font-semibold">
|
|
1045
|
+
error
|
|
1046
|
+
</span>
|
|
1047
|
+
)}
|
|
1048
|
+
</>
|
|
1049
|
+
)}
|
|
1050
|
+
{msg.role === "assistant" && msg.cost !== undefined && msg.cost > 0 && (
|
|
1051
|
+
<span className="text-[10px] text-emerald-600 font-medium tabular-nums">
|
|
1052
|
+
${msg.cost.toFixed(4)}
|
|
1053
|
+
</span>
|
|
1054
|
+
)}
|
|
1055
|
+
<Timestamp value={msg.timestamp} />
|
|
1056
|
+
</div>
|
|
1057
|
+
<div
|
|
1058
|
+
className={`whitespace-pre-wrap break-words text-[12px] leading-relaxed ${
|
|
1059
|
+
isError ? "text-red-800" : "text-gray-700"
|
|
1060
|
+
}`}
|
|
1061
|
+
>
|
|
1062
|
+
{displayContent}
|
|
1063
|
+
</div>
|
|
1064
|
+
{isTruncated && (
|
|
1065
|
+
<button
|
|
1066
|
+
type="button"
|
|
1067
|
+
onClick={() => setExpanded(!expanded)}
|
|
1068
|
+
className="text-[11px] text-accent-peri hover:underline mt-1.5 font-medium cursor-pointer"
|
|
1069
|
+
>
|
|
1070
|
+
{expanded ? "Show less" : "Show more"}
|
|
1071
|
+
</button>
|
|
1072
|
+
)}
|
|
1073
|
+
</div>
|
|
1074
|
+
);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// ============================================================================
|
|
1078
|
+
// SendMessageForm
|
|
1079
|
+
// ============================================================================
|
|
1080
|
+
|
|
1081
|
+
function SendMessageForm({
|
|
1082
|
+
sessionId,
|
|
1083
|
+
agentId,
|
|
1084
|
+
}: {
|
|
1085
|
+
sessionId: string;
|
|
1086
|
+
agentId: string;
|
|
1087
|
+
}) {
|
|
1088
|
+
const [content, setContent] = useState("");
|
|
1089
|
+
const [senderType, setSenderType] = useState<"user" | "debug" | "custom">(
|
|
1090
|
+
"user",
|
|
1091
|
+
);
|
|
1092
|
+
const [customSender, setCustomSender] = useState("");
|
|
1093
|
+
const [sending, setSending] = useState(false);
|
|
1094
|
+
const [error, setError] = useState<string | null>(null);
|
|
1095
|
+
|
|
1096
|
+
const handleSend = useCallback(async () => {
|
|
1097
|
+
if (!content.trim()) return;
|
|
1098
|
+
|
|
1099
|
+
setSending(true);
|
|
1100
|
+
setError(null);
|
|
1101
|
+
|
|
1102
|
+
try {
|
|
1103
|
+
if (senderType === "debug") {
|
|
1104
|
+
unwrap(
|
|
1105
|
+
await api.call("mailbox.send", {
|
|
1106
|
+
sessionId,
|
|
1107
|
+
toAgentId: AgentId(agentId),
|
|
1108
|
+
content: content.trim(),
|
|
1109
|
+
debug: true,
|
|
1110
|
+
}),
|
|
1111
|
+
);
|
|
1112
|
+
} else {
|
|
1113
|
+
unwrap(
|
|
1114
|
+
await api.call("user-chat.sendMessage", {
|
|
1115
|
+
sessionId,
|
|
1116
|
+
agentId: AgentId(agentId),
|
|
1117
|
+
content: content.trim(),
|
|
1118
|
+
}),
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
setContent("");
|
|
1122
|
+
} catch (e) {
|
|
1123
|
+
setError(e instanceof Error ? e.message : "Failed to send message");
|
|
1124
|
+
} finally {
|
|
1125
|
+
setSending(false);
|
|
1126
|
+
}
|
|
1127
|
+
}, [content, senderType, sessionId, agentId]);
|
|
1128
|
+
|
|
1129
|
+
const handleKeyDown = useCallback(
|
|
1130
|
+
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
1131
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
1132
|
+
e.preventDefault();
|
|
1133
|
+
handleSend();
|
|
1134
|
+
}
|
|
1135
|
+
},
|
|
1136
|
+
[handleSend],
|
|
1137
|
+
);
|
|
1138
|
+
|
|
1139
|
+
const handleSubmit = useCallback(
|
|
1140
|
+
(e: FormEvent) => {
|
|
1141
|
+
e.preventDefault();
|
|
1142
|
+
handleSend();
|
|
1143
|
+
},
|
|
1144
|
+
[handleSend],
|
|
1145
|
+
);
|
|
1146
|
+
|
|
1147
|
+
return (
|
|
1148
|
+
<div className="bg-white rounded-2xl shadow-card p-4">
|
|
1149
|
+
<h3 className="text-sm font-semibold text-gray-900 mb-3">
|
|
1150
|
+
Send Debug Message
|
|
1151
|
+
</h3>
|
|
1152
|
+
<form onSubmit={handleSubmit} className="space-y-3">
|
|
1153
|
+
<div className="flex items-center gap-2">
|
|
1154
|
+
<label className="text-[11px] text-gray-400 font-medium">From:</label>
|
|
1155
|
+
<select
|
|
1156
|
+
value={senderType}
|
|
1157
|
+
onChange={(e) =>
|
|
1158
|
+
setSenderType(e.target.value as "user" | "debug" | "custom")
|
|
1159
|
+
}
|
|
1160
|
+
className="text-[11px] border border-gray-200 rounded-lg px-2 py-1 bg-gray-50"
|
|
1161
|
+
>
|
|
1162
|
+
<option value="user">user</option>
|
|
1163
|
+
<option value="debug">debug</option>
|
|
1164
|
+
<option value="custom">custom...</option>
|
|
1165
|
+
</select>
|
|
1166
|
+
{senderType === "custom" && (
|
|
1167
|
+
<input
|
|
1168
|
+
type="text"
|
|
1169
|
+
value={customSender}
|
|
1170
|
+
onChange={(e) => setCustomSender(e.target.value)}
|
|
1171
|
+
placeholder="agent ID or role"
|
|
1172
|
+
className="text-[11px] border border-gray-200 rounded-lg px-2 py-1 flex-1 bg-gray-50"
|
|
1173
|
+
/>
|
|
1174
|
+
)}
|
|
1175
|
+
</div>
|
|
1176
|
+
<textarea
|
|
1177
|
+
value={content}
|
|
1178
|
+
onChange={(e) => setContent(e.target.value)}
|
|
1179
|
+
onKeyDown={handleKeyDown}
|
|
1180
|
+
placeholder="Message content... (Enter to send, Shift+Enter for new line)"
|
|
1181
|
+
rows={3}
|
|
1182
|
+
className="w-full border border-gray-200 rounded-xl px-3 py-2 text-sm resize-y bg-gray-50"
|
|
1183
|
+
/>
|
|
1184
|
+
{error && <div className="text-[11px] text-red-600">{error}</div>}
|
|
1185
|
+
<button
|
|
1186
|
+
type="submit"
|
|
1187
|
+
disabled={sending || !content.trim()}
|
|
1188
|
+
className={`px-3 py-1.5 text-[11px] font-semibold rounded-lg transition-all ${
|
|
1189
|
+
content.trim()
|
|
1190
|
+
? "text-white bg-accent-peri hover:brightness-110 cursor-pointer shadow-sm"
|
|
1191
|
+
: "text-gray-400 bg-gray-100 cursor-not-allowed"
|
|
1192
|
+
} disabled:opacity-50 disabled:cursor-not-allowed`}
|
|
1193
|
+
>
|
|
1194
|
+
{sending ? "Sending..." : "Send Message"}
|
|
1195
|
+
</button>
|
|
1196
|
+
</form>
|
|
1197
|
+
</div>
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// ============================================================================
|
|
1202
|
+
// ResumeAgentButton
|
|
1203
|
+
// ============================================================================
|
|
1204
|
+
|
|
1205
|
+
function ResumeAgentButton({
|
|
1206
|
+
sessionId,
|
|
1207
|
+
agentId,
|
|
1208
|
+
}: {
|
|
1209
|
+
sessionId: string;
|
|
1210
|
+
agentId: string;
|
|
1211
|
+
}) {
|
|
1212
|
+
const [resuming, setResuming] = useState(false);
|
|
1213
|
+
|
|
1214
|
+
const handleResume = useCallback(
|
|
1215
|
+
async (e: FormEvent) => {
|
|
1216
|
+
e.stopPropagation();
|
|
1217
|
+
setResuming(true);
|
|
1218
|
+
try {
|
|
1219
|
+
unwrap(
|
|
1220
|
+
await api.call("agents.resume", {
|
|
1221
|
+
sessionId,
|
|
1222
|
+
agentId: AgentId(agentId),
|
|
1223
|
+
}),
|
|
1224
|
+
);
|
|
1225
|
+
} catch {
|
|
1226
|
+
// Error is visible via state change (or lack thereof)
|
|
1227
|
+
} finally {
|
|
1228
|
+
setResuming(false);
|
|
1229
|
+
}
|
|
1230
|
+
},
|
|
1231
|
+
[sessionId, agentId],
|
|
1232
|
+
);
|
|
1233
|
+
|
|
1234
|
+
return (
|
|
1235
|
+
<button
|
|
1236
|
+
type="button"
|
|
1237
|
+
onClick={handleResume}
|
|
1238
|
+
disabled={resuming}
|
|
1239
|
+
className="text-[11px] font-semibold text-white bg-red-600 hover:bg-red-700 px-2.5 py-1 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed transition-colors shrink-0 cursor-pointer"
|
|
1240
|
+
>
|
|
1241
|
+
{resuming ? "Resuming..." : "Resume"}
|
|
1242
|
+
</button>
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// ============================================================================
|
|
1247
|
+
// Badges
|
|
1248
|
+
// ============================================================================
|
|
1249
|
+
|
|
1250
|
+
function StatusBadge({ status }: { status: string }) {
|
|
1251
|
+
const styles: Record<string, string> = {
|
|
1252
|
+
idle: "bg-gray-100 text-gray-600",
|
|
1253
|
+
thinking: "bg-accent-lime/30 text-gray-700",
|
|
1254
|
+
responding: "bg-accent-peri/30 text-gray-700",
|
|
1255
|
+
waiting_for_user: "bg-purple-100 text-purple-700",
|
|
1256
|
+
error: "bg-red-100 text-red-700",
|
|
1257
|
+
};
|
|
1258
|
+
return (
|
|
1259
|
+
<span
|
|
1260
|
+
className={`text-[11px] px-2.5 py-1 rounded-full font-semibold ${styles[status] ?? "bg-gray-100 text-gray-600"}`}
|
|
1261
|
+
>
|
|
1262
|
+
{status}
|
|
1263
|
+
</span>
|
|
1264
|
+
);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
function ToolStatusBadge({ status }: { status: string }) {
|
|
1268
|
+
const styles: Record<string, string> = {
|
|
1269
|
+
pending: "bg-gray-100 text-gray-600",
|
|
1270
|
+
executing: "bg-amber-100 text-amber-700",
|
|
1271
|
+
completed: "bg-green-100 text-green-700",
|
|
1272
|
+
failed: "bg-red-100 text-red-700",
|
|
1273
|
+
};
|
|
1274
|
+
return (
|
|
1275
|
+
<span
|
|
1276
|
+
className={`text-[10px] px-2 py-0.5 rounded-full font-semibold ${styles[status] ?? "bg-gray-100 text-gray-600"}`}
|
|
1277
|
+
>
|
|
1278
|
+
{status}
|
|
1279
|
+
</span>
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
function ToolStatusDot({ status }: { status: string }) {
|
|
1284
|
+
const colors: Record<string, string> = {
|
|
1285
|
+
pending: "bg-gray-400",
|
|
1286
|
+
executing: "bg-amber-400 animate-pulse",
|
|
1287
|
+
completed: "bg-green-500",
|
|
1288
|
+
failed: "bg-red-500",
|
|
1289
|
+
};
|
|
1290
|
+
return (
|
|
1291
|
+
<div
|
|
1292
|
+
className={`w-2 h-2 rounded-full shrink-0 ${colors[status] ?? "bg-gray-400"}`}
|
|
1293
|
+
/>
|
|
1294
|
+
);
|
|
1295
|
+
}
|