@agent-native/dispatch 0.2.20 → 0.3.0
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/actions/get-agent-thread-debug.d.ts +3 -0
- package/dist/actions/get-agent-thread-debug.d.ts.map +1 -0
- package/dist/actions/get-agent-thread-debug.js +24 -0
- package/dist/actions/get-agent-thread-debug.js.map +1 -0
- package/dist/actions/index.d.ts.map +1 -1
- package/dist/actions/index.js +6 -0
- package/dist/actions/index.js.map +1 -1
- package/dist/actions/list-agent-thread-sources.d.ts +3 -0
- package/dist/actions/list-agent-thread-sources.d.ts.map +1 -0
- package/dist/actions/list-agent-thread-sources.js +11 -0
- package/dist/actions/list-agent-thread-sources.js.map +1 -0
- package/dist/actions/navigate.js +1 -1
- package/dist/actions/navigate.js.map +1 -1
- package/dist/actions/search-agent-threads.d.ts +3 -0
- package/dist/actions/search-agent-threads.d.ts.map +1 -0
- package/dist/actions/search-agent-threads.js +25 -0
- package/dist/actions/search-agent-threads.js.map +1 -0
- package/dist/actions/view-screen.d.ts.map +1 -1
- package/dist/actions/view-screen.js +38 -0
- package/dist/actions/view-screen.js.map +1 -1
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +8 -1
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/ui/command.d.ts +7 -7
- package/dist/hooks/use-navigation-state.js +5 -0
- package/dist/hooks/use-navigation-state.js.map +1 -1
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/index.js +1 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/pages/thread-debug.d.ts +5 -0
- package/dist/routes/pages/thread-debug.d.ts.map +1 -0
- package/dist/routes/pages/thread-debug.js +160 -0
- package/dist/routes/pages/thread-debug.js.map +1 -0
- package/dist/server/lib/thread-debug-store.d.ts +101 -0
- package/dist/server/lib/thread-debug-store.d.ts.map +1 -0
- package/dist/server/lib/thread-debug-store.js +587 -0
- package/dist/server/lib/thread-debug-store.js.map +1 -0
- package/package.json +2 -2
- package/src/actions/get-agent-thread-debug.ts +25 -0
- package/src/actions/index.ts +6 -0
- package/src/actions/list-agent-thread-sources.ts +12 -0
- package/src/actions/navigate.ts +1 -1
- package/src/actions/search-agent-threads.ts +30 -0
- package/src/actions/view-screen.ts +41 -0
- package/src/components/layout/Layout.tsx +8 -0
- package/src/hooks/use-navigation-state.ts +4 -0
- package/src/routes/index.ts +1 -0
- package/src/routes/pages/thread-debug.tsx +683 -0
- package/src/server/lib/thread-debug-store.ts +779 -0
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { agentNativePath, useActionQuery } from "@agent-native/core/client";
|
|
3
|
+
import {
|
|
4
|
+
IconDatabase,
|
|
5
|
+
IconFileSearch,
|
|
6
|
+
IconRefresh,
|
|
7
|
+
IconSearch,
|
|
8
|
+
} from "@tabler/icons-react";
|
|
9
|
+
import { DispatchShell } from "@/components/dispatch-shell";
|
|
10
|
+
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
11
|
+
import { Badge } from "@/components/ui/badge";
|
|
12
|
+
import { Button } from "@/components/ui/button";
|
|
13
|
+
import { Input } from "@/components/ui/input";
|
|
14
|
+
import {
|
|
15
|
+
Select,
|
|
16
|
+
SelectContent,
|
|
17
|
+
SelectItem,
|
|
18
|
+
SelectTrigger,
|
|
19
|
+
SelectValue,
|
|
20
|
+
} from "@/components/ui/select";
|
|
21
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
22
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
23
|
+
import { cn } from "@/lib/utils";
|
|
24
|
+
|
|
25
|
+
export function meta() {
|
|
26
|
+
return [{ title: "Thread Debug — Dispatch" }];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface ThreadDebugSource {
|
|
30
|
+
id: string;
|
|
31
|
+
label: string;
|
|
32
|
+
kind: "current" | "env" | "configured";
|
|
33
|
+
current: boolean;
|
|
34
|
+
connected: boolean;
|
|
35
|
+
databaseUrlEnv: string | null;
|
|
36
|
+
databaseAuthTokenEnv: string | null;
|
|
37
|
+
canInspectAll: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface ThreadSearchResult {
|
|
41
|
+
id: string;
|
|
42
|
+
ownerEmail: string;
|
|
43
|
+
title: string;
|
|
44
|
+
preview: string;
|
|
45
|
+
messageCount: number;
|
|
46
|
+
createdAt: number;
|
|
47
|
+
updatedAt: number;
|
|
48
|
+
snippet: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface ThreadMessage {
|
|
52
|
+
index: number;
|
|
53
|
+
id: string | null;
|
|
54
|
+
role: string;
|
|
55
|
+
createdAt: string | number | null;
|
|
56
|
+
status: unknown;
|
|
57
|
+
text: string;
|
|
58
|
+
contentParts: any[];
|
|
59
|
+
attachments: any[];
|
|
60
|
+
metadata: unknown;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface ThreadRun {
|
|
64
|
+
id: string;
|
|
65
|
+
status: string;
|
|
66
|
+
abortReason: string | null;
|
|
67
|
+
startedAt: number;
|
|
68
|
+
completedAt: number | null;
|
|
69
|
+
heartbeatAt: number | null;
|
|
70
|
+
events: Array<{ seq: number; event: any; rawEventData: string }>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface ThreadDebugResponse {
|
|
74
|
+
source: {
|
|
75
|
+
id: string;
|
|
76
|
+
label: string;
|
|
77
|
+
kind: string;
|
|
78
|
+
databaseUrlEnv: string | null;
|
|
79
|
+
};
|
|
80
|
+
access: { viewerEmail: string; scope: string; canInspectAll: boolean };
|
|
81
|
+
thread: ThreadSearchResult;
|
|
82
|
+
messages: ThreadMessage[];
|
|
83
|
+
debug: any;
|
|
84
|
+
debugRuns: any[];
|
|
85
|
+
queuedMessages: any[];
|
|
86
|
+
threadData: any;
|
|
87
|
+
rawThreadData: string;
|
|
88
|
+
runs: ThreadRun[];
|
|
89
|
+
traces: { summaries: any[]; spans: any[] };
|
|
90
|
+
feedback: any[];
|
|
91
|
+
satisfaction: any[];
|
|
92
|
+
evals: any[];
|
|
93
|
+
checkpoints: any[];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function formatDate(value: number | string | null | undefined): string {
|
|
97
|
+
if (value == null || value === "") return "n/a";
|
|
98
|
+
const numeric = Number(value);
|
|
99
|
+
const date = Number.isFinite(numeric) ? new Date(numeric) : new Date(value);
|
|
100
|
+
if (Number.isNaN(date.getTime())) return "n/a";
|
|
101
|
+
return date.toLocaleString();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function json(value: unknown): string {
|
|
105
|
+
try {
|
|
106
|
+
return JSON.stringify(value, null, 2);
|
|
107
|
+
} catch {
|
|
108
|
+
return String(value);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function eventLabel(event: any): string {
|
|
113
|
+
if (!event || typeof event !== "object") return "event";
|
|
114
|
+
if (event.type === "tool_start") return `tool_start · ${event.tool}`;
|
|
115
|
+
if (event.type === "tool_done") return `tool_done · ${event.tool}`;
|
|
116
|
+
if (event.type === "text") return "text";
|
|
117
|
+
if (event.type === "error") return `error · ${event.errorCode ?? "agent"}`;
|
|
118
|
+
return String(event.type ?? "event");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function messageTitle(message: ThreadMessage): string {
|
|
122
|
+
const role = message.role || "unknown";
|
|
123
|
+
return `${role.charAt(0).toUpperCase()}${role.slice(1)} ${message.index + 1}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function toolParts(message: ThreadMessage): any[] {
|
|
127
|
+
return message.contentParts.filter((part) => part?.type === "tool-call");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function RawBlock({
|
|
131
|
+
value,
|
|
132
|
+
className,
|
|
133
|
+
}: {
|
|
134
|
+
value: unknown;
|
|
135
|
+
className?: string;
|
|
136
|
+
}) {
|
|
137
|
+
return (
|
|
138
|
+
<pre
|
|
139
|
+
className={cn(
|
|
140
|
+
"max-h-[520px] overflow-auto rounded-lg border bg-muted/30 p-3 text-xs leading-relaxed text-foreground",
|
|
141
|
+
"whitespace-pre-wrap break-words",
|
|
142
|
+
className,
|
|
143
|
+
)}
|
|
144
|
+
>
|
|
145
|
+
{typeof value === "string" ? value : json(value)}
|
|
146
|
+
</pre>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function SourceBadge({ source }: { source: ThreadDebugSource }) {
|
|
151
|
+
return (
|
|
152
|
+
<Badge variant={source.current ? "default" : "secondary"}>
|
|
153
|
+
{source.current ? "current" : source.kind}
|
|
154
|
+
</Badge>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function ResultCard({
|
|
159
|
+
result,
|
|
160
|
+
selected,
|
|
161
|
+
onSelect,
|
|
162
|
+
}: {
|
|
163
|
+
result: ThreadSearchResult;
|
|
164
|
+
selected: boolean;
|
|
165
|
+
onSelect: () => void;
|
|
166
|
+
}) {
|
|
167
|
+
return (
|
|
168
|
+
<button
|
|
169
|
+
type="button"
|
|
170
|
+
onClick={onSelect}
|
|
171
|
+
className={cn(
|
|
172
|
+
"w-full rounded-lg border px-3 py-3 text-left transition-colors",
|
|
173
|
+
selected
|
|
174
|
+
? "border-foreground bg-muted"
|
|
175
|
+
: "bg-card hover:border-foreground/30 hover:bg-muted/40",
|
|
176
|
+
)}
|
|
177
|
+
>
|
|
178
|
+
<div className="flex items-start justify-between gap-3">
|
|
179
|
+
<div className="min-w-0">
|
|
180
|
+
<div className="truncate text-sm font-medium text-foreground">
|
|
181
|
+
{result.title || result.preview || result.id}
|
|
182
|
+
</div>
|
|
183
|
+
<div className="mt-1 truncate font-mono text-[11px] text-muted-foreground">
|
|
184
|
+
{result.id}
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
<Badge variant="outline" className="shrink-0">
|
|
188
|
+
{result.messageCount}
|
|
189
|
+
</Badge>
|
|
190
|
+
</div>
|
|
191
|
+
<div className="mt-2 line-clamp-3 text-xs leading-relaxed text-muted-foreground">
|
|
192
|
+
{result.snippet || result.preview || "No preview"}
|
|
193
|
+
</div>
|
|
194
|
+
<div className="mt-2 flex items-center justify-between gap-3 text-[11px] text-muted-foreground">
|
|
195
|
+
<span className="truncate">{result.ownerEmail}</span>
|
|
196
|
+
<span className="shrink-0">{formatDate(result.updatedAt)}</span>
|
|
197
|
+
</div>
|
|
198
|
+
</button>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function MessageBlock({ message }: { message: ThreadMessage }) {
|
|
203
|
+
const tools = toolParts(message);
|
|
204
|
+
return (
|
|
205
|
+
<div className="rounded-lg border bg-card">
|
|
206
|
+
<div className="flex flex-wrap items-center justify-between gap-2 border-b px-3 py-2">
|
|
207
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
208
|
+
<Badge
|
|
209
|
+
variant={message.role === "assistant" ? "default" : "secondary"}
|
|
210
|
+
>
|
|
211
|
+
{message.role}
|
|
212
|
+
</Badge>
|
|
213
|
+
<span className="truncate text-sm font-medium text-foreground">
|
|
214
|
+
{messageTitle(message)}
|
|
215
|
+
</span>
|
|
216
|
+
</div>
|
|
217
|
+
<div className="flex items-center gap-2 text-[11px] text-muted-foreground">
|
|
218
|
+
{message.attachments.length > 0 ? (
|
|
219
|
+
<Badge variant="outline">{message.attachments.length} files</Badge>
|
|
220
|
+
) : null}
|
|
221
|
+
<span>{formatDate(message.createdAt)}</span>
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
<div className="space-y-3 px-3 py-3">
|
|
225
|
+
{message.text ? (
|
|
226
|
+
<div className="whitespace-pre-wrap break-words text-sm leading-relaxed text-foreground">
|
|
227
|
+
{message.text}
|
|
228
|
+
</div>
|
|
229
|
+
) : (
|
|
230
|
+
<div className="text-sm text-muted-foreground">No text content</div>
|
|
231
|
+
)}
|
|
232
|
+
{tools.length > 0 ? (
|
|
233
|
+
<div className="space-y-2">
|
|
234
|
+
{tools.map((tool, index) => (
|
|
235
|
+
<details
|
|
236
|
+
key={`${message.id ?? message.index}-tool-${index}`}
|
|
237
|
+
className="rounded-md border bg-muted/30 px-3 py-2"
|
|
238
|
+
>
|
|
239
|
+
<summary className="cursor-pointer text-xs font-medium text-foreground">
|
|
240
|
+
{tool.toolName ?? tool.name ?? "tool-call"}
|
|
241
|
+
</summary>
|
|
242
|
+
<RawBlock value={tool} className="mt-2 max-h-72" />
|
|
243
|
+
</details>
|
|
244
|
+
))}
|
|
245
|
+
</div>
|
|
246
|
+
) : null}
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function ThreadDetail({ detail }: { detail: ThreadDebugResponse }) {
|
|
253
|
+
const rawBundle = useMemo(
|
|
254
|
+
() => ({
|
|
255
|
+
thread: detail.thread,
|
|
256
|
+
debug: detail.debug,
|
|
257
|
+
debugRuns: detail.debugRuns,
|
|
258
|
+
queuedMessages: detail.queuedMessages,
|
|
259
|
+
threadData: detail.threadData,
|
|
260
|
+
runs: detail.runs,
|
|
261
|
+
traces: detail.traces,
|
|
262
|
+
feedback: detail.feedback,
|
|
263
|
+
satisfaction: detail.satisfaction,
|
|
264
|
+
evals: detail.evals,
|
|
265
|
+
checkpoints: detail.checkpoints,
|
|
266
|
+
}),
|
|
267
|
+
[detail],
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
return (
|
|
271
|
+
<div className="rounded-lg border bg-card">
|
|
272
|
+
<div className="border-b px-4 py-3">
|
|
273
|
+
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
274
|
+
<div className="min-w-0">
|
|
275
|
+
<div className="truncate text-base font-semibold text-foreground">
|
|
276
|
+
{detail.thread.title || detail.thread.preview || detail.thread.id}
|
|
277
|
+
</div>
|
|
278
|
+
<div className="mt-1 truncate font-mono text-xs text-muted-foreground">
|
|
279
|
+
{detail.thread.id}
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
283
|
+
<Badge variant="secondary">{detail.messages.length} messages</Badge>
|
|
284
|
+
<Badge variant="secondary">{detail.runs.length} runs</Badge>
|
|
285
|
+
<Badge variant="outline">{detail.source.label}</Badge>
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
<div className="mt-3 grid gap-2 text-xs text-muted-foreground sm:grid-cols-3">
|
|
289
|
+
<div className="truncate">Owner: {detail.thread.ownerEmail}</div>
|
|
290
|
+
<div>Created: {formatDate(detail.thread.createdAt)}</div>
|
|
291
|
+
<div>Updated: {formatDate(detail.thread.updatedAt)}</div>
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
|
|
295
|
+
<Tabs defaultValue="transcript" className="p-4">
|
|
296
|
+
<TabsList>
|
|
297
|
+
<TabsTrigger value="transcript">Transcript</TabsTrigger>
|
|
298
|
+
<TabsTrigger value="runs">Runs</TabsTrigger>
|
|
299
|
+
<TabsTrigger value="internals">Internals</TabsTrigger>
|
|
300
|
+
<TabsTrigger value="raw">Raw</TabsTrigger>
|
|
301
|
+
</TabsList>
|
|
302
|
+
|
|
303
|
+
<TabsContent value="transcript" className="mt-4 space-y-3">
|
|
304
|
+
{detail.messages.length > 0 ? (
|
|
305
|
+
detail.messages.map((message) => (
|
|
306
|
+
<MessageBlock
|
|
307
|
+
key={message.id ?? `message-${message.index}`}
|
|
308
|
+
message={message}
|
|
309
|
+
/>
|
|
310
|
+
))
|
|
311
|
+
) : (
|
|
312
|
+
<div className="rounded-lg border border-dashed px-4 py-8 text-center text-sm text-muted-foreground">
|
|
313
|
+
No persisted messages.
|
|
314
|
+
</div>
|
|
315
|
+
)}
|
|
316
|
+
</TabsContent>
|
|
317
|
+
|
|
318
|
+
<TabsContent value="runs" className="mt-4 space-y-3">
|
|
319
|
+
{detail.runs.length > 0 ? (
|
|
320
|
+
detail.runs.map((run) => (
|
|
321
|
+
<details key={run.id} className="rounded-lg border bg-card">
|
|
322
|
+
<summary className="cursor-pointer px-4 py-3">
|
|
323
|
+
<div className="inline-flex flex-wrap items-center gap-2">
|
|
324
|
+
<Badge variant="outline">{run.status}</Badge>
|
|
325
|
+
<span className="font-mono text-xs text-foreground">
|
|
326
|
+
{run.id}
|
|
327
|
+
</span>
|
|
328
|
+
<span className="text-xs text-muted-foreground">
|
|
329
|
+
{formatDate(run.startedAt)}
|
|
330
|
+
</span>
|
|
331
|
+
</div>
|
|
332
|
+
</summary>
|
|
333
|
+
<div className="space-y-2 border-t px-4 py-3">
|
|
334
|
+
{run.events.map((event) => (
|
|
335
|
+
<details
|
|
336
|
+
key={`${run.id}-${event.seq}`}
|
|
337
|
+
className="rounded-md border bg-muted/30 px-3 py-2"
|
|
338
|
+
>
|
|
339
|
+
<summary className="cursor-pointer text-xs font-medium text-foreground">
|
|
340
|
+
#{event.seq} {eventLabel(event.event)}
|
|
341
|
+
</summary>
|
|
342
|
+
<RawBlock value={event.event} className="mt-2 max-h-72" />
|
|
343
|
+
</details>
|
|
344
|
+
))}
|
|
345
|
+
{run.events.length === 0 ? (
|
|
346
|
+
<div className="text-sm text-muted-foreground">
|
|
347
|
+
No retained run events.
|
|
348
|
+
</div>
|
|
349
|
+
) : null}
|
|
350
|
+
</div>
|
|
351
|
+
</details>
|
|
352
|
+
))
|
|
353
|
+
) : (
|
|
354
|
+
<div className="rounded-lg border border-dashed px-4 py-8 text-center text-sm text-muted-foreground">
|
|
355
|
+
No retained runs.
|
|
356
|
+
</div>
|
|
357
|
+
)}
|
|
358
|
+
</TabsContent>
|
|
359
|
+
|
|
360
|
+
<TabsContent value="internals" className="mt-4 space-y-4">
|
|
361
|
+
<div className="grid gap-4 lg:grid-cols-2">
|
|
362
|
+
<div>
|
|
363
|
+
<div className="mb-2 text-sm font-medium text-foreground">
|
|
364
|
+
Debug Runs
|
|
365
|
+
</div>
|
|
366
|
+
<RawBlock
|
|
367
|
+
value={
|
|
368
|
+
detail.debugRuns.length > 0
|
|
369
|
+
? detail.debugRuns
|
|
370
|
+
: (detail.debug ?? {})
|
|
371
|
+
}
|
|
372
|
+
/>
|
|
373
|
+
</div>
|
|
374
|
+
<div>
|
|
375
|
+
<div className="mb-2 text-sm font-medium text-foreground">
|
|
376
|
+
Trace Summaries
|
|
377
|
+
</div>
|
|
378
|
+
<RawBlock value={detail.traces.summaries} />
|
|
379
|
+
</div>
|
|
380
|
+
<div>
|
|
381
|
+
<div className="mb-2 text-sm font-medium text-foreground">
|
|
382
|
+
Trace Spans
|
|
383
|
+
</div>
|
|
384
|
+
<RawBlock value={detail.traces.spans} />
|
|
385
|
+
</div>
|
|
386
|
+
<div>
|
|
387
|
+
<div className="mb-2 text-sm font-medium text-foreground">
|
|
388
|
+
Feedback And Evals
|
|
389
|
+
</div>
|
|
390
|
+
<RawBlock
|
|
391
|
+
value={{
|
|
392
|
+
feedback: detail.feedback,
|
|
393
|
+
satisfaction: detail.satisfaction,
|
|
394
|
+
evals: detail.evals,
|
|
395
|
+
checkpoints: detail.checkpoints,
|
|
396
|
+
}}
|
|
397
|
+
/>
|
|
398
|
+
</div>
|
|
399
|
+
</div>
|
|
400
|
+
</TabsContent>
|
|
401
|
+
|
|
402
|
+
<TabsContent value="raw" className="mt-4 space-y-4">
|
|
403
|
+
<RawBlock value={rawBundle} />
|
|
404
|
+
<RawBlock value={detail.rawThreadData} />
|
|
405
|
+
</TabsContent>
|
|
406
|
+
</Tabs>
|
|
407
|
+
</div>
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export default function ThreadDebugRoute() {
|
|
412
|
+
const [sourceId, setSourceId] = useState("current");
|
|
413
|
+
const [query, setQuery] = useState("");
|
|
414
|
+
const [ownerEmail, setOwnerEmail] = useState("");
|
|
415
|
+
const [threadId, setThreadId] = useState("");
|
|
416
|
+
const [submittedSearch, setSubmittedSearch] = useState({
|
|
417
|
+
sourceId: "current",
|
|
418
|
+
query: "",
|
|
419
|
+
ownerEmail: "",
|
|
420
|
+
});
|
|
421
|
+
const [selected, setSelected] = useState<{
|
|
422
|
+
sourceId: string;
|
|
423
|
+
threadId: string;
|
|
424
|
+
ownerEmail?: string;
|
|
425
|
+
} | null>(null);
|
|
426
|
+
|
|
427
|
+
const { data: sourcesData, isLoading: sourcesLoading } = useActionQuery<{
|
|
428
|
+
access: {
|
|
429
|
+
viewerEmail: string;
|
|
430
|
+
orgId: string | null;
|
|
431
|
+
role: string | null;
|
|
432
|
+
envAdmin: boolean;
|
|
433
|
+
canInspectAll: boolean;
|
|
434
|
+
memberCount: number;
|
|
435
|
+
};
|
|
436
|
+
sources: ThreadDebugSource[];
|
|
437
|
+
}>("list-agent-thread-sources", {});
|
|
438
|
+
|
|
439
|
+
const sources = sourcesData?.sources ?? [];
|
|
440
|
+
const searchParams = useMemo(
|
|
441
|
+
() => ({
|
|
442
|
+
sourceId: submittedSearch.sourceId,
|
|
443
|
+
query: submittedSearch.query || undefined,
|
|
444
|
+
ownerEmail: submittedSearch.ownerEmail || undefined,
|
|
445
|
+
limit: 25,
|
|
446
|
+
}),
|
|
447
|
+
[submittedSearch],
|
|
448
|
+
);
|
|
449
|
+
const {
|
|
450
|
+
data: searchData,
|
|
451
|
+
isLoading: searchLoading,
|
|
452
|
+
error: searchError,
|
|
453
|
+
refetch: refetchSearch,
|
|
454
|
+
} = useActionQuery<{
|
|
455
|
+
count: number;
|
|
456
|
+
threads: ThreadSearchResult[];
|
|
457
|
+
access: { scope: string; canInspectAll: boolean };
|
|
458
|
+
source: { id: string; label: string };
|
|
459
|
+
}>("search-agent-threads", searchParams);
|
|
460
|
+
|
|
461
|
+
const detailParams = useMemo(
|
|
462
|
+
() => ({
|
|
463
|
+
sourceId: selected?.sourceId ?? "current",
|
|
464
|
+
threadId: selected?.threadId ?? "",
|
|
465
|
+
ownerEmail: selected?.ownerEmail,
|
|
466
|
+
maxRuns: 20,
|
|
467
|
+
maxEvents: 800,
|
|
468
|
+
maxTraceSpans: 600,
|
|
469
|
+
}),
|
|
470
|
+
[selected],
|
|
471
|
+
);
|
|
472
|
+
const {
|
|
473
|
+
data: detail,
|
|
474
|
+
isLoading: detailLoading,
|
|
475
|
+
error: detailError,
|
|
476
|
+
} = useActionQuery<ThreadDebugResponse>(
|
|
477
|
+
"get-agent-thread-debug",
|
|
478
|
+
detailParams,
|
|
479
|
+
{
|
|
480
|
+
enabled: Boolean(selected?.threadId),
|
|
481
|
+
},
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
const selectedSource = sources.find((source) => source.id === sourceId);
|
|
485
|
+
|
|
486
|
+
useEffect(() => {
|
|
487
|
+
fetch(agentNativePath("/_agent-native/application-state/navigation"), {
|
|
488
|
+
method: "PUT",
|
|
489
|
+
keepalive: true,
|
|
490
|
+
headers: { "Content-Type": "application/json" },
|
|
491
|
+
body: JSON.stringify({
|
|
492
|
+
view: "thread-debug",
|
|
493
|
+
path:
|
|
494
|
+
typeof window === "undefined"
|
|
495
|
+
? "/thread-debug"
|
|
496
|
+
: window.location.pathname,
|
|
497
|
+
sourceId,
|
|
498
|
+
query,
|
|
499
|
+
ownerEmail: ownerEmail.trim() || undefined,
|
|
500
|
+
threadId: selected?.threadId ?? (threadId.trim() || undefined),
|
|
501
|
+
}),
|
|
502
|
+
}).catch(() => {});
|
|
503
|
+
}, [ownerEmail, query, selected?.threadId, sourceId, threadId]);
|
|
504
|
+
|
|
505
|
+
return (
|
|
506
|
+
<DispatchShell
|
|
507
|
+
title="Thread Debug"
|
|
508
|
+
description="Inspect persisted agent chat threads, run events, and AI internals."
|
|
509
|
+
>
|
|
510
|
+
<div className="space-y-4">
|
|
511
|
+
<section className="rounded-lg border bg-card p-4">
|
|
512
|
+
<div className="grid gap-3 lg:grid-cols-[220px_1fr_260px_auto]">
|
|
513
|
+
<Select value={sourceId} onValueChange={setSourceId}>
|
|
514
|
+
<SelectTrigger>
|
|
515
|
+
<SelectValue placeholder="Source" />
|
|
516
|
+
</SelectTrigger>
|
|
517
|
+
<SelectContent>
|
|
518
|
+
{sources.map((source) => (
|
|
519
|
+
<SelectItem key={source.id} value={source.id}>
|
|
520
|
+
{source.label}
|
|
521
|
+
</SelectItem>
|
|
522
|
+
))}
|
|
523
|
+
{sources.length === 0 ? (
|
|
524
|
+
<SelectItem value="current">Current Dispatch DB</SelectItem>
|
|
525
|
+
) : null}
|
|
526
|
+
</SelectContent>
|
|
527
|
+
</Select>
|
|
528
|
+
<Input
|
|
529
|
+
value={query}
|
|
530
|
+
onChange={(event) => setQuery(event.target.value)}
|
|
531
|
+
placeholder="Search title, preview, messages, tools"
|
|
532
|
+
/>
|
|
533
|
+
<Input
|
|
534
|
+
value={ownerEmail}
|
|
535
|
+
onChange={(event) => setOwnerEmail(event.target.value)}
|
|
536
|
+
placeholder="Owner email"
|
|
537
|
+
/>
|
|
538
|
+
<Button
|
|
539
|
+
type="button"
|
|
540
|
+
onClick={() =>
|
|
541
|
+
setSubmittedSearch({
|
|
542
|
+
sourceId,
|
|
543
|
+
query: query.trim(),
|
|
544
|
+
ownerEmail: ownerEmail.trim(),
|
|
545
|
+
})
|
|
546
|
+
}
|
|
547
|
+
>
|
|
548
|
+
<IconSearch size={16} />
|
|
549
|
+
Search
|
|
550
|
+
</Button>
|
|
551
|
+
</div>
|
|
552
|
+
|
|
553
|
+
<div className="mt-3 grid gap-3 lg:grid-cols-[1fr_auto]">
|
|
554
|
+
<Input
|
|
555
|
+
value={threadId}
|
|
556
|
+
onChange={(event) => setThreadId(event.target.value)}
|
|
557
|
+
placeholder="Paste thread ID"
|
|
558
|
+
className="font-mono"
|
|
559
|
+
/>
|
|
560
|
+
<Button
|
|
561
|
+
type="button"
|
|
562
|
+
variant="outline"
|
|
563
|
+
onClick={() => {
|
|
564
|
+
const trimmed = threadId.trim();
|
|
565
|
+
if (!trimmed) return;
|
|
566
|
+
setSelected({
|
|
567
|
+
sourceId,
|
|
568
|
+
threadId: trimmed,
|
|
569
|
+
ownerEmail: ownerEmail.trim() || undefined,
|
|
570
|
+
});
|
|
571
|
+
}}
|
|
572
|
+
>
|
|
573
|
+
<IconFileSearch size={16} />
|
|
574
|
+
Inspect
|
|
575
|
+
</Button>
|
|
576
|
+
</div>
|
|
577
|
+
|
|
578
|
+
<div className="mt-3 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
|
579
|
+
{sourcesLoading ? <Skeleton className="h-5 w-32" /> : null}
|
|
580
|
+
{selectedSource ? <SourceBadge source={selectedSource} /> : null}
|
|
581
|
+
{selectedSource?.databaseUrlEnv ? (
|
|
582
|
+
<Badge variant="outline" className="font-mono">
|
|
583
|
+
{selectedSource.databaseUrlEnv}
|
|
584
|
+
</Badge>
|
|
585
|
+
) : null}
|
|
586
|
+
{sourcesData?.access ? (
|
|
587
|
+
<span>
|
|
588
|
+
{sourcesData.access.viewerEmail} ·{" "}
|
|
589
|
+
{sourcesData.access.canInspectAll ? "admin scope" : "own scope"}
|
|
590
|
+
</span>
|
|
591
|
+
) : null}
|
|
592
|
+
</div>
|
|
593
|
+
</section>
|
|
594
|
+
|
|
595
|
+
{searchError ? (
|
|
596
|
+
<Alert variant="destructive">
|
|
597
|
+
<AlertTitle>Search failed</AlertTitle>
|
|
598
|
+
<AlertDescription>{String(searchError.message)}</AlertDescription>
|
|
599
|
+
</Alert>
|
|
600
|
+
) : null}
|
|
601
|
+
|
|
602
|
+
<div className="grid gap-4 xl:grid-cols-[380px_1fr]">
|
|
603
|
+
<section className="min-h-[520px] rounded-lg border bg-card">
|
|
604
|
+
<div className="flex items-center justify-between border-b px-4 py-3">
|
|
605
|
+
<div>
|
|
606
|
+
<div className="text-sm font-semibold text-foreground">
|
|
607
|
+
Threads
|
|
608
|
+
</div>
|
|
609
|
+
<div className="text-xs text-muted-foreground">
|
|
610
|
+
{searchData?.count ?? 0} results ·{" "}
|
|
611
|
+
{searchData?.access?.scope ?? "current scope"}
|
|
612
|
+
</div>
|
|
613
|
+
</div>
|
|
614
|
+
<Button
|
|
615
|
+
type="button"
|
|
616
|
+
variant="ghost"
|
|
617
|
+
size="icon"
|
|
618
|
+
onClick={() => refetchSearch()}
|
|
619
|
+
aria-label="Refresh threads"
|
|
620
|
+
>
|
|
621
|
+
<IconRefresh size={16} />
|
|
622
|
+
</Button>
|
|
623
|
+
</div>
|
|
624
|
+
<div className="max-h-[760px] space-y-2 overflow-auto p-3">
|
|
625
|
+
{searchLoading ? (
|
|
626
|
+
<>
|
|
627
|
+
<Skeleton className="h-28 w-full rounded-lg" />
|
|
628
|
+
<Skeleton className="h-28 w-full rounded-lg" />
|
|
629
|
+
<Skeleton className="h-28 w-full rounded-lg" />
|
|
630
|
+
</>
|
|
631
|
+
) : null}
|
|
632
|
+
{!searchLoading && (searchData?.threads?.length ?? 0) === 0 ? (
|
|
633
|
+
<div className="flex min-h-64 flex-col items-center justify-center rounded-lg border border-dashed px-4 text-center text-sm text-muted-foreground">
|
|
634
|
+
<IconDatabase className="mb-2 h-5 w-5" />
|
|
635
|
+
No threads found.
|
|
636
|
+
</div>
|
|
637
|
+
) : null}
|
|
638
|
+
{searchData?.threads?.map((result) => (
|
|
639
|
+
<ResultCard
|
|
640
|
+
key={result.id}
|
|
641
|
+
result={result}
|
|
642
|
+
selected={selected?.threadId === result.id}
|
|
643
|
+
onSelect={() =>
|
|
644
|
+
setSelected({
|
|
645
|
+
sourceId: submittedSearch.sourceId,
|
|
646
|
+
threadId: result.id,
|
|
647
|
+
ownerEmail: submittedSearch.ownerEmail || undefined,
|
|
648
|
+
})
|
|
649
|
+
}
|
|
650
|
+
/>
|
|
651
|
+
))}
|
|
652
|
+
</div>
|
|
653
|
+
</section>
|
|
654
|
+
|
|
655
|
+
<section className="min-w-0">
|
|
656
|
+
{detailError ? (
|
|
657
|
+
<Alert variant="destructive">
|
|
658
|
+
<AlertTitle>Thread lookup failed</AlertTitle>
|
|
659
|
+
<AlertDescription>
|
|
660
|
+
{String(detailError.message)}
|
|
661
|
+
</AlertDescription>
|
|
662
|
+
</Alert>
|
|
663
|
+
) : null}
|
|
664
|
+
{detailLoading ? (
|
|
665
|
+
<div className="rounded-lg border bg-card p-4">
|
|
666
|
+
<Skeleton className="h-6 w-72" />
|
|
667
|
+
<Skeleton className="mt-3 h-4 w-96" />
|
|
668
|
+
<Skeleton className="mt-6 h-[520px] w-full" />
|
|
669
|
+
</div>
|
|
670
|
+
) : detail ? (
|
|
671
|
+
<ThreadDetail detail={detail} />
|
|
672
|
+
) : (
|
|
673
|
+
<div className="flex min-h-[520px] flex-col items-center justify-center rounded-lg border border-dashed bg-card px-4 text-center text-sm text-muted-foreground">
|
|
674
|
+
<IconFileSearch className="mb-2 h-5 w-5" />
|
|
675
|
+
Select or inspect a thread.
|
|
676
|
+
</div>
|
|
677
|
+
)}
|
|
678
|
+
</section>
|
|
679
|
+
</div>
|
|
680
|
+
</div>
|
|
681
|
+
</DispatchShell>
|
|
682
|
+
);
|
|
683
|
+
}
|