@agent-native/dispatch 0.16.2 → 0.16.3
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 +9 -0
- package/dist/actions/index.d.ts.map +1 -1
- package/dist/actions/index.js +2 -0
- package/dist/actions/index.js.map +1 -1
- package/dist/actions/list-agent-run-failures.d.ts +61 -0
- package/dist/actions/list-agent-run-failures.d.ts.map +1 -0
- package/dist/actions/list-agent-run-failures.js +38 -0
- package/dist/actions/list-agent-run-failures.js.map +1 -0
- package/dist/actions/provider-api-register.d.ts +12 -12
- package/dist/actions/upsert-destination.d.ts +12 -12
- package/dist/actions/view-screen.js +32 -5
- package/dist/actions/view-screen.js.map +1 -1
- package/dist/hooks/use-navigation-state.d.ts +6 -0
- package/dist/hooks/use-navigation-state.d.ts.map +1 -1
- package/dist/hooks/use-navigation-state.js +30 -0
- package/dist/hooks/use-navigation-state.js.map +1 -1
- package/dist/routes/pages/thread-debug.d.ts.map +1 -1
- package/dist/routes/pages/thread-debug.js +291 -77
- package/dist/routes/pages/thread-debug.js.map +1 -1
- package/dist/server/lib/thread-debug-store.d.ts +72 -0
- package/dist/server/lib/thread-debug-store.d.ts.map +1 -1
- package/dist/server/lib/thread-debug-store.js +215 -19
- package/dist/server/lib/thread-debug-store.js.map +1 -1
- package/dist/server/plugins/agent-chat.js +1 -0
- package/dist/server/plugins/agent-chat.js.map +1 -1
- package/package.json +3 -3
- package/src/actions/index.ts +2 -0
- package/src/actions/list-agent-run-failures.ts +44 -0
- package/src/actions/view-screen.spec.ts +91 -0
- package/src/actions/view-screen.ts +34 -4
- package/src/hooks/use-navigation-state.spec.ts +45 -0
- package/src/hooks/use-navigation-state.ts +28 -0
- package/src/routes/pages/thread-debug.spec.tsx +272 -0
- package/src/routes/pages/thread-debug.tsx +843 -224
- package/src/server/lib/thread-debug-store.spec.ts +292 -16
- package/src/server/lib/thread-debug-store.ts +318 -22
- package/src/server/plugins/agent-chat.spec.ts +30 -0
- package/src/server/plugins/agent-chat.ts +1 -0
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import { agentNativePath } from "@agent-native/core/client/api-path";
|
|
2
1
|
import { useActionQuery } from "@agent-native/core/client/hooks";
|
|
2
|
+
import { useT } from "@agent-native/core/client/i18n";
|
|
3
3
|
import {
|
|
4
|
+
IconAlertTriangle,
|
|
5
|
+
IconClock,
|
|
4
6
|
IconDatabase,
|
|
5
7
|
IconFileSearch,
|
|
6
8
|
IconRefresh,
|
|
7
9
|
IconSearch,
|
|
8
10
|
} from "@tabler/icons-react";
|
|
9
|
-
import {
|
|
11
|
+
import { useMemo, useState } from "react";
|
|
10
12
|
import { useSearchParams } from "react-router";
|
|
11
13
|
|
|
12
14
|
import { ActionQueryError } from "../../components/action-query-error";
|
|
@@ -71,13 +73,78 @@ interface ThreadMessage {
|
|
|
71
73
|
interface ThreadRun {
|
|
72
74
|
id: string;
|
|
73
75
|
status: string;
|
|
76
|
+
turnId?: string | null;
|
|
74
77
|
abortReason: string | null;
|
|
78
|
+
errorCode?: string | null;
|
|
79
|
+
errorDetail?: string | null;
|
|
80
|
+
terminalReason?: string | null;
|
|
81
|
+
dispatchMode?: string | null;
|
|
82
|
+
diagStage?: string | null;
|
|
83
|
+
workerStage?: string | null;
|
|
75
84
|
startedAt: number;
|
|
76
85
|
completedAt: number | null;
|
|
77
86
|
heartbeatAt: number | null;
|
|
87
|
+
lastProgressAt?: number | null;
|
|
88
|
+
durationMs?: number | null;
|
|
89
|
+
peakRssMb?: number | null;
|
|
78
90
|
events: Array<{ seq: number; event: any; rawEventData: string }>;
|
|
79
91
|
}
|
|
80
92
|
|
|
93
|
+
type ThreadDebugMode = "failures" | "threads";
|
|
94
|
+
type FailureStatus = "all" | "errored" | "aborted" | "truncated";
|
|
95
|
+
type FailureRange = "24h" | "7d" | "30d";
|
|
96
|
+
|
|
97
|
+
interface AgentRunFailure {
|
|
98
|
+
id: string;
|
|
99
|
+
threadId: string;
|
|
100
|
+
sourceId?: string;
|
|
101
|
+
sourceLabel?: string;
|
|
102
|
+
source?: {
|
|
103
|
+
id: string;
|
|
104
|
+
label: string;
|
|
105
|
+
kind?: string;
|
|
106
|
+
databaseUrlEnv?: string | null;
|
|
107
|
+
};
|
|
108
|
+
ownerEmail: string;
|
|
109
|
+
threadTitle: string;
|
|
110
|
+
threadPreview: string;
|
|
111
|
+
status: string;
|
|
112
|
+
errorCode: string | null;
|
|
113
|
+
errorDetail: string | null;
|
|
114
|
+
terminalReason: string | null;
|
|
115
|
+
abortReason: string | null;
|
|
116
|
+
dispatchMode: string | null;
|
|
117
|
+
diagStage: string | null;
|
|
118
|
+
workerStage?: string | null;
|
|
119
|
+
startedAt: number;
|
|
120
|
+
completedAt: number | null;
|
|
121
|
+
durationMs: number | null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
interface AgentRunFailuresResponse {
|
|
125
|
+
failures: AgentRunFailure[];
|
|
126
|
+
sources: Array<{
|
|
127
|
+
source: {
|
|
128
|
+
id: string;
|
|
129
|
+
label: string;
|
|
130
|
+
kind?: string;
|
|
131
|
+
databaseUrlEnv?: string | null;
|
|
132
|
+
};
|
|
133
|
+
status: "ok" | "unsupported" | "unavailable" | "disconnected";
|
|
134
|
+
failureCount: number;
|
|
135
|
+
errorCode?: string | null;
|
|
136
|
+
}>;
|
|
137
|
+
partial: boolean;
|
|
138
|
+
count?: number;
|
|
139
|
+
access: { viewerEmail: string; scope: string; canInspectAll: boolean };
|
|
140
|
+
filters?: {
|
|
141
|
+
sourceId?: string;
|
|
142
|
+
status?: FailureStatus;
|
|
143
|
+
lookbackHours?: number;
|
|
144
|
+
limit?: number;
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
81
148
|
interface ThreadDebugResponse {
|
|
82
149
|
source: {
|
|
83
150
|
id: string;
|
|
@@ -102,6 +169,36 @@ interface ThreadDebugResponse {
|
|
|
102
169
|
checkpoints: any[];
|
|
103
170
|
}
|
|
104
171
|
|
|
172
|
+
const FAILURE_RANGE_HOURS: Record<FailureRange, number> = {
|
|
173
|
+
"24h": 24,
|
|
174
|
+
"7d": 7 * 24,
|
|
175
|
+
"30d": 30 * 24,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
function parseMode(value: string | null): ThreadDebugMode {
|
|
179
|
+
return value === "threads" ? "threads" : "failures";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function parseFailureStatus(value: string | null): FailureStatus {
|
|
183
|
+
return value === "errored" || value === "aborted" || value === "truncated"
|
|
184
|
+
? value
|
|
185
|
+
: "all";
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function parseFailureRange(value: string | null): FailureRange {
|
|
189
|
+
return value === "7d" || value === "30d" ? value : "24h";
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function failureSourceId(failure: AgentRunFailure): string {
|
|
193
|
+
return failure.sourceId || failure.source?.id || "current";
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function failureSourceLabel(failure: AgentRunFailure): string {
|
|
197
|
+
return (
|
|
198
|
+
failure.sourceLabel || failure.source?.label || failureSourceId(failure)
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
105
202
|
function formatDate(value: number | string | null | undefined): string {
|
|
106
203
|
if (value == null || value === "") return "n/a";
|
|
107
204
|
const numeric = Number(value);
|
|
@@ -110,6 +207,13 @@ function formatDate(value: number | string | null | undefined): string {
|
|
|
110
207
|
return date.toLocaleString();
|
|
111
208
|
}
|
|
112
209
|
|
|
210
|
+
function formatDuration(value: number | null | undefined): string {
|
|
211
|
+
if (value == null || !Number.isFinite(value)) return "n/a";
|
|
212
|
+
if (value < 1_000) return `${Math.round(value)}ms`;
|
|
213
|
+
if (value < 60_000) return `${(value / 1_000).toFixed(1)}s`;
|
|
214
|
+
return `${(value / 60_000).toFixed(1)}m`;
|
|
215
|
+
}
|
|
216
|
+
|
|
113
217
|
function json(value: unknown): string {
|
|
114
218
|
try {
|
|
115
219
|
return JSON.stringify(value, null, 2);
|
|
@@ -136,6 +240,24 @@ function toolParts(message: ThreadMessage): any[] {
|
|
|
136
240
|
return message.contentParts.filter((part) => part?.type === "tool-call");
|
|
137
241
|
}
|
|
138
242
|
|
|
243
|
+
function diagnosticStage(value: string | null | undefined): string | null {
|
|
244
|
+
if (!value) return null;
|
|
245
|
+
try {
|
|
246
|
+
const parsed = JSON.parse(value) as { stage?: unknown; detail?: unknown };
|
|
247
|
+
const stage =
|
|
248
|
+
typeof parsed.stage === "string" && parsed.stage.trim()
|
|
249
|
+
? parsed.stage.trim()
|
|
250
|
+
: value;
|
|
251
|
+
const detail =
|
|
252
|
+
typeof parsed.detail === "string" && parsed.detail.trim()
|
|
253
|
+
? parsed.detail.trim()
|
|
254
|
+
: "";
|
|
255
|
+
return detail ? `${stage}: ${detail}` : stage;
|
|
256
|
+
} catch {
|
|
257
|
+
return value;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
139
261
|
function RawBlock({
|
|
140
262
|
value,
|
|
141
263
|
className,
|
|
@@ -208,6 +330,90 @@ function ResultCard({
|
|
|
208
330
|
);
|
|
209
331
|
}
|
|
210
332
|
|
|
333
|
+
function FailureCard({
|
|
334
|
+
failure,
|
|
335
|
+
selected,
|
|
336
|
+
onSelect,
|
|
337
|
+
}: {
|
|
338
|
+
failure: AgentRunFailure;
|
|
339
|
+
selected: boolean;
|
|
340
|
+
onSelect: () => void;
|
|
341
|
+
}) {
|
|
342
|
+
const t = useT();
|
|
343
|
+
const summary =
|
|
344
|
+
failure.errorCode ||
|
|
345
|
+
failure.terminalReason ||
|
|
346
|
+
failure.abortReason ||
|
|
347
|
+
failure.status;
|
|
348
|
+
const statusLabel =
|
|
349
|
+
failure.status === "errored"
|
|
350
|
+
? t("dispatch.pages.threadDebugErrored", {
|
|
351
|
+
defaultValue: "Errored",
|
|
352
|
+
})
|
|
353
|
+
: failure.status === "aborted"
|
|
354
|
+
? t("dispatch.pages.threadDebugAborted", {
|
|
355
|
+
defaultValue: "Aborted",
|
|
356
|
+
})
|
|
357
|
+
: failure.status === "truncated"
|
|
358
|
+
? t("dispatch.pages.threadDebugTruncated", {
|
|
359
|
+
defaultValue: "Truncated",
|
|
360
|
+
})
|
|
361
|
+
: failure.status;
|
|
362
|
+
|
|
363
|
+
return (
|
|
364
|
+
<button
|
|
365
|
+
type="button"
|
|
366
|
+
onClick={onSelect}
|
|
367
|
+
className={cn(
|
|
368
|
+
"w-full rounded-lg border px-3 py-3 text-left transition-colors",
|
|
369
|
+
selected
|
|
370
|
+
? "border-foreground bg-muted"
|
|
371
|
+
: "bg-card hover:border-foreground/30 hover:bg-muted/40",
|
|
372
|
+
)}
|
|
373
|
+
>
|
|
374
|
+
<div className="flex items-start justify-between gap-3">
|
|
375
|
+
<div className="min-w-0">
|
|
376
|
+
<div className="truncate text-sm font-medium text-foreground">
|
|
377
|
+
{failure.threadTitle || failure.threadPreview || failure.threadId}
|
|
378
|
+
</div>
|
|
379
|
+
<div className="mt-1 truncate font-mono text-[11px] text-muted-foreground">
|
|
380
|
+
{failure.id}
|
|
381
|
+
</div>
|
|
382
|
+
</div>
|
|
383
|
+
<Badge variant="outline" className="shrink-0">
|
|
384
|
+
{statusLabel}
|
|
385
|
+
</Badge>
|
|
386
|
+
</div>
|
|
387
|
+
<div className="mt-2 flex items-center gap-1.5 text-xs text-foreground">
|
|
388
|
+
<IconAlertTriangle className="size-3.5 shrink-0 text-muted-foreground" />
|
|
389
|
+
<span className="truncate">{summary}</span>
|
|
390
|
+
</div>
|
|
391
|
+
{failure.errorDetail ? (
|
|
392
|
+
<div className="mt-1 line-clamp-2 text-xs leading-relaxed text-muted-foreground">
|
|
393
|
+
{failure.errorDetail}
|
|
394
|
+
</div>
|
|
395
|
+
) : null}
|
|
396
|
+
<div className="mt-2 flex items-center justify-between gap-3 text-[11px] text-muted-foreground">
|
|
397
|
+
<span className="truncate">
|
|
398
|
+
{failureSourceLabel(failure)} · {failure.ownerEmail}
|
|
399
|
+
</span>
|
|
400
|
+
<span className="inline-flex shrink-0 items-center gap-1">
|
|
401
|
+
<IconClock className="size-3" />
|
|
402
|
+
{formatDate(failure.completedAt ?? failure.startedAt)}
|
|
403
|
+
{failure.durationMs == null
|
|
404
|
+
? null
|
|
405
|
+
: ` · ${formatDuration(failure.durationMs)}`}
|
|
406
|
+
</span>
|
|
407
|
+
</div>
|
|
408
|
+
<span className="sr-only">
|
|
409
|
+
{t("dispatch.pages.threadDebugInspectFailure", {
|
|
410
|
+
defaultValue: "Inspect failed run",
|
|
411
|
+
})}
|
|
412
|
+
</span>
|
|
413
|
+
</button>
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
211
417
|
function MessageBlock({ message }: { message: ThreadMessage }) {
|
|
212
418
|
const tools = toolParts(message);
|
|
213
419
|
return (
|
|
@@ -259,6 +465,7 @@ function MessageBlock({ message }: { message: ThreadMessage }) {
|
|
|
259
465
|
}
|
|
260
466
|
|
|
261
467
|
function ThreadDetail({ detail }: { detail: ThreadDebugResponse }) {
|
|
468
|
+
const t = useT();
|
|
262
469
|
const rawBundle = useMemo(
|
|
263
470
|
() => ({
|
|
264
471
|
thread: detail.thread,
|
|
@@ -334,12 +541,91 @@ function ThreadDetail({ detail }: { detail: ThreadDebugResponse }) {
|
|
|
334
541
|
<span className="font-mono text-xs text-foreground">
|
|
335
542
|
{run.id}
|
|
336
543
|
</span>
|
|
544
|
+
{run.errorCode ? (
|
|
545
|
+
<span className="font-mono text-xs text-destructive">
|
|
546
|
+
{run.errorCode}
|
|
547
|
+
</span>
|
|
548
|
+
) : null}
|
|
337
549
|
<span className="text-xs text-muted-foreground">
|
|
338
550
|
{formatDate(run.startedAt)}
|
|
339
551
|
</span>
|
|
340
552
|
</div>
|
|
341
553
|
</summary>
|
|
342
|
-
<div className="space-y-
|
|
554
|
+
<div className="space-y-3 border-t px-4 py-3">
|
|
555
|
+
<div className="grid gap-2 text-xs sm:grid-cols-2 xl:grid-cols-3">
|
|
556
|
+
<div>
|
|
557
|
+
<div className="text-muted-foreground">
|
|
558
|
+
{t("dispatch.pages.threadDebugFailureCode", {
|
|
559
|
+
defaultValue: "Failure code",
|
|
560
|
+
})}
|
|
561
|
+
</div>
|
|
562
|
+
<div className="mt-0.5 break-words font-mono text-foreground">
|
|
563
|
+
{run.errorCode || "n/a"}
|
|
564
|
+
</div>
|
|
565
|
+
</div>
|
|
566
|
+
<div>
|
|
567
|
+
<div className="text-muted-foreground">
|
|
568
|
+
{t("dispatch.pages.threadDebugTerminalReason", {
|
|
569
|
+
defaultValue: "Terminal reason",
|
|
570
|
+
})}
|
|
571
|
+
</div>
|
|
572
|
+
<div className="mt-0.5 break-words font-mono text-foreground">
|
|
573
|
+
{run.terminalReason || run.abortReason || "n/a"}
|
|
574
|
+
</div>
|
|
575
|
+
</div>
|
|
576
|
+
<div>
|
|
577
|
+
<div className="text-muted-foreground">
|
|
578
|
+
{t("dispatch.pages.threadDebugDispatchMode", {
|
|
579
|
+
defaultValue: "Dispatch mode",
|
|
580
|
+
})}
|
|
581
|
+
</div>
|
|
582
|
+
<div className="mt-0.5 break-words font-mono text-foreground">
|
|
583
|
+
{run.dispatchMode || "foreground"}
|
|
584
|
+
</div>
|
|
585
|
+
</div>
|
|
586
|
+
<div>
|
|
587
|
+
<div className="text-muted-foreground">
|
|
588
|
+
{t("dispatch.pages.threadDebugLastStage", {
|
|
589
|
+
defaultValue: "Last stage",
|
|
590
|
+
})}
|
|
591
|
+
</div>
|
|
592
|
+
<div className="mt-0.5 break-words font-mono text-foreground">
|
|
593
|
+
{diagnosticStage(run.workerStage) ||
|
|
594
|
+
diagnosticStage(run.diagStage) ||
|
|
595
|
+
"n/a"}
|
|
596
|
+
</div>
|
|
597
|
+
</div>
|
|
598
|
+
<div>
|
|
599
|
+
<div className="text-muted-foreground">
|
|
600
|
+
{t("dispatch.pages.threadDebugDuration", {
|
|
601
|
+
defaultValue: "Duration",
|
|
602
|
+
})}
|
|
603
|
+
</div>
|
|
604
|
+
<div className="mt-0.5 text-foreground">
|
|
605
|
+
{formatDuration(
|
|
606
|
+
run.durationMs ??
|
|
607
|
+
(run.completedAt == null
|
|
608
|
+
? null
|
|
609
|
+
: run.completedAt - run.startedAt),
|
|
610
|
+
)}
|
|
611
|
+
</div>
|
|
612
|
+
</div>
|
|
613
|
+
<div>
|
|
614
|
+
<div className="text-muted-foreground">
|
|
615
|
+
{t("dispatch.pages.threadDebugLastProgress", {
|
|
616
|
+
defaultValue: "Last progress",
|
|
617
|
+
})}
|
|
618
|
+
</div>
|
|
619
|
+
<div className="mt-0.5 text-foreground">
|
|
620
|
+
{formatDate(run.lastProgressAt ?? run.heartbeatAt)}
|
|
621
|
+
</div>
|
|
622
|
+
</div>
|
|
623
|
+
</div>
|
|
624
|
+
{run.errorDetail ? (
|
|
625
|
+
<div className="rounded-md bg-destructive/10 px-3 py-2 text-xs leading-relaxed text-destructive">
|
|
626
|
+
{run.errorDetail}
|
|
627
|
+
</div>
|
|
628
|
+
) : null}
|
|
343
629
|
{run.events.map((event) => (
|
|
344
630
|
<details
|
|
345
631
|
key={`${run.id}-${event.seq}`}
|
|
@@ -418,25 +704,31 @@ function ThreadDetail({ detail }: { detail: ThreadDebugResponse }) {
|
|
|
418
704
|
}
|
|
419
705
|
|
|
420
706
|
export default function ThreadDebugRoute() {
|
|
421
|
-
const
|
|
422
|
-
const
|
|
423
|
-
const
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
const
|
|
707
|
+
const t = useT();
|
|
708
|
+
const [routeSearchParams, setRouteSearchParams] = useSearchParams();
|
|
709
|
+
const mode = parseMode(routeSearchParams.get("mode"));
|
|
710
|
+
const sourceId =
|
|
711
|
+
routeSearchParams.get("source") ||
|
|
712
|
+
(mode === "failures" ? "all" : "current");
|
|
713
|
+
const ownerEmail = routeSearchParams.get("owner") || "";
|
|
714
|
+
const query = routeSearchParams.get("query") || "";
|
|
715
|
+
const status = parseFailureStatus(routeSearchParams.get("status"));
|
|
716
|
+
const range = parseFailureRange(routeSearchParams.get("range"));
|
|
717
|
+
const runId = routeSearchParams.get("runId") || "";
|
|
718
|
+
const threadId = routeSearchParams.get("threadId") || "";
|
|
719
|
+
const inspectSourceId = routeSearchParams.get("inspectSource") || "";
|
|
428
720
|
const [lookupId, setLookupId] = useState("");
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
}
|
|
721
|
+
|
|
722
|
+
function updateRouteState(
|
|
723
|
+
updates: Record<string, string | null | undefined>,
|
|
724
|
+
) {
|
|
725
|
+
const next = new URLSearchParams(routeSearchParams);
|
|
726
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
727
|
+
if (value == null || value === "") next.delete(key);
|
|
728
|
+
else next.set(key, value);
|
|
729
|
+
}
|
|
730
|
+
setRouteSearchParams(next, { replace: true });
|
|
731
|
+
}
|
|
440
732
|
|
|
441
733
|
const sourcesQuery = useActionQuery<{
|
|
442
734
|
access: {
|
|
@@ -452,14 +744,54 @@ export default function ThreadDebugRoute() {
|
|
|
452
744
|
const { data: sourcesData, isLoading: sourcesLoading } = sourcesQuery;
|
|
453
745
|
|
|
454
746
|
const sources: ThreadDebugSource[] = sourcesData?.sources ?? [];
|
|
747
|
+
const failureParams = useMemo(
|
|
748
|
+
() => ({
|
|
749
|
+
sourceId,
|
|
750
|
+
ownerEmail: ownerEmail.trim() || undefined,
|
|
751
|
+
status,
|
|
752
|
+
lookbackHours: FAILURE_RANGE_HOURS[range],
|
|
753
|
+
limit: 25,
|
|
754
|
+
}),
|
|
755
|
+
[ownerEmail, range, sourceId, status],
|
|
756
|
+
);
|
|
757
|
+
const {
|
|
758
|
+
data: failuresData,
|
|
759
|
+
isLoading: failuresLoading,
|
|
760
|
+
error: failuresError,
|
|
761
|
+
refetch: refetchFailures,
|
|
762
|
+
} = useActionQuery<AgentRunFailuresResponse>(
|
|
763
|
+
"list-agent-run-failures",
|
|
764
|
+
failureParams,
|
|
765
|
+
{ enabled: mode === "failures" },
|
|
766
|
+
);
|
|
767
|
+
const failures = failuresData?.failures ?? [];
|
|
768
|
+
const unavailableFailureSources = (failuresData?.sources ?? []).filter(
|
|
769
|
+
(source) => source.status !== "ok",
|
|
770
|
+
);
|
|
771
|
+
const failureSourceStatusLabels = {
|
|
772
|
+
ok: "ok",
|
|
773
|
+
disconnected: t("dispatch.pages.threadDebugDisconnected", {
|
|
774
|
+
defaultValue: "disconnected",
|
|
775
|
+
}),
|
|
776
|
+
unsupported: t("dispatch.pages.threadDebugUnsupported", {
|
|
777
|
+
defaultValue: "unsupported",
|
|
778
|
+
}),
|
|
779
|
+
unavailable: t("dispatch.pages.threadDebugUnavailable", {
|
|
780
|
+
defaultValue: "unavailable",
|
|
781
|
+
}),
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
const threadSourceId = sourceId === "all" ? "current" : sourceId;
|
|
785
|
+
const detailSourceId =
|
|
786
|
+
runId && inspectSourceId ? inspectSourceId : threadSourceId;
|
|
455
787
|
const searchParams = useMemo(
|
|
456
788
|
() => ({
|
|
457
|
-
sourceId:
|
|
458
|
-
query:
|
|
459
|
-
ownerEmail:
|
|
789
|
+
sourceId: threadSourceId,
|
|
790
|
+
query: query.trim() || undefined,
|
|
791
|
+
ownerEmail: ownerEmail.trim() || undefined,
|
|
460
792
|
limit: 25,
|
|
461
793
|
}),
|
|
462
|
-
[
|
|
794
|
+
[ownerEmail, query, threadSourceId],
|
|
463
795
|
);
|
|
464
796
|
const {
|
|
465
797
|
data: searchData,
|
|
@@ -471,21 +803,19 @@ export default function ThreadDebugRoute() {
|
|
|
471
803
|
threads: ThreadSearchResult[];
|
|
472
804
|
access: { scope: string; canInspectAll: boolean };
|
|
473
805
|
source: { id: string; label: string };
|
|
474
|
-
}>("search-agent-threads", searchParams);
|
|
806
|
+
}>("search-agent-threads", searchParams, { enabled: mode === "threads" });
|
|
475
807
|
const searchThreads: ThreadSearchResult[] = searchData?.threads ?? [];
|
|
476
808
|
|
|
477
809
|
const detailParams = useMemo(
|
|
478
810
|
() => ({
|
|
479
|
-
sourceId:
|
|
480
|
-
...(
|
|
481
|
-
|
|
482
|
-
: { threadId: selected?.lookupId ?? "" }),
|
|
483
|
-
ownerEmail: selected?.ownerEmail,
|
|
811
|
+
sourceId: detailSourceId,
|
|
812
|
+
...(runId ? { runId } : { threadId }),
|
|
813
|
+
ownerEmail: ownerEmail.trim() || undefined,
|
|
484
814
|
maxRuns: 20,
|
|
485
815
|
maxEvents: 800,
|
|
486
816
|
maxTraceSpans: 600,
|
|
487
817
|
}),
|
|
488
|
-
[
|
|
818
|
+
[detailSourceId, ownerEmail, runId, threadId],
|
|
489
819
|
);
|
|
490
820
|
const {
|
|
491
821
|
data: detail,
|
|
@@ -496,46 +826,47 @@ export default function ThreadDebugRoute() {
|
|
|
496
826
|
"get-agent-thread-debug",
|
|
497
827
|
detailParams,
|
|
498
828
|
{
|
|
499
|
-
enabled: Boolean(
|
|
829
|
+
enabled: Boolean(runId || threadId),
|
|
500
830
|
},
|
|
501
831
|
);
|
|
502
832
|
|
|
503
|
-
const selectedSource = sources.find((source) => source.id ===
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
: undefined,
|
|
531
|
-
}),
|
|
532
|
-
}).catch(() => {});
|
|
533
|
-
}, [ownerEmail, query, selected, sourceId, lookupId]);
|
|
833
|
+
const selectedSource = sources.find((source) => source.id === threadSourceId);
|
|
834
|
+
const detailPane = (
|
|
835
|
+
<section className="min-w-0">
|
|
836
|
+
{detailError ? (
|
|
837
|
+
<ActionQueryError
|
|
838
|
+
error={detailError}
|
|
839
|
+
onRetry={() => void refetchDetail()}
|
|
840
|
+
/>
|
|
841
|
+
) : null}
|
|
842
|
+
{detailLoading ? (
|
|
843
|
+
<div className="rounded-lg bg-card p-4">
|
|
844
|
+
<Skeleton className="h-6 w-72" />
|
|
845
|
+
<Skeleton className="mt-3 h-4 w-96" />
|
|
846
|
+
<Skeleton className="mt-6 h-[520px] w-full" />
|
|
847
|
+
</div>
|
|
848
|
+
) : detail ? (
|
|
849
|
+
<ThreadDetail detail={detail} />
|
|
850
|
+
) : (
|
|
851
|
+
<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">
|
|
852
|
+
<IconFileSearch className="mb-2 size-5" />
|
|
853
|
+
{t("dispatch.pages.threadDebugSelectPrompt", {
|
|
854
|
+
defaultValue: "Select a failed run or thread to inspect.",
|
|
855
|
+
})}
|
|
856
|
+
</div>
|
|
857
|
+
)}
|
|
858
|
+
</section>
|
|
859
|
+
);
|
|
534
860
|
|
|
535
861
|
return (
|
|
536
862
|
<DispatchShell
|
|
537
|
-
title="
|
|
538
|
-
|
|
863
|
+
title={t("dispatch.pages.threadDebugTitle", {
|
|
864
|
+
defaultValue: "Thread Debug",
|
|
865
|
+
})}
|
|
866
|
+
description={t("dispatch.pages.threadDebugDescription", {
|
|
867
|
+
defaultValue:
|
|
868
|
+
"Inspect failed agent runs, persisted threads, run events, and AI internals.",
|
|
869
|
+
})}
|
|
539
870
|
>
|
|
540
871
|
<div className="space-y-4">
|
|
541
872
|
{sourcesQuery.isError ? (
|
|
@@ -544,178 +875,466 @@ export default function ThreadDebugRoute() {
|
|
|
544
875
|
onRetry={() => void sourcesQuery.refetch()}
|
|
545
876
|
/>
|
|
546
877
|
) : null}
|
|
547
|
-
<
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
<
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
<
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
type="button"
|
|
576
|
-
onClick={() =>
|
|
577
|
-
setSubmittedSearch({
|
|
578
|
-
sourceId,
|
|
579
|
-
query: query.trim(),
|
|
580
|
-
ownerEmail: ownerEmail.trim(),
|
|
581
|
-
})
|
|
582
|
-
}
|
|
583
|
-
>
|
|
584
|
-
<IconSearch size={16} />
|
|
585
|
-
Search
|
|
586
|
-
</Button>
|
|
587
|
-
</div>
|
|
878
|
+
<Tabs
|
|
879
|
+
value={mode}
|
|
880
|
+
onValueChange={(value) => {
|
|
881
|
+
const nextMode = parseMode(value);
|
|
882
|
+
updateRouteState({
|
|
883
|
+
mode: nextMode,
|
|
884
|
+
source:
|
|
885
|
+
nextMode === "threads" && sourceId === "all"
|
|
886
|
+
? "current"
|
|
887
|
+
: sourceId,
|
|
888
|
+
runId: null,
|
|
889
|
+
threadId: null,
|
|
890
|
+
inspectSource: null,
|
|
891
|
+
});
|
|
892
|
+
}}
|
|
893
|
+
>
|
|
894
|
+
<TabsList>
|
|
895
|
+
<TabsTrigger value="failures">
|
|
896
|
+
{t("dispatch.pages.threadDebugFailedRuns", {
|
|
897
|
+
defaultValue: "Failed runs",
|
|
898
|
+
})}
|
|
899
|
+
</TabsTrigger>
|
|
900
|
+
<TabsTrigger value="threads">
|
|
901
|
+
{t("dispatch.pages.threadDebugThreads", {
|
|
902
|
+
defaultValue: "Threads",
|
|
903
|
+
})}
|
|
904
|
+
</TabsTrigger>
|
|
905
|
+
</TabsList>
|
|
588
906
|
|
|
589
|
-
<
|
|
590
|
-
<
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
907
|
+
<TabsContent value="failures" className="mt-4 space-y-4">
|
|
908
|
+
<section className="rounded-lg bg-card p-4">
|
|
909
|
+
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-[220px_1fr_180px_150px]">
|
|
910
|
+
<Select
|
|
911
|
+
value={sourceId}
|
|
912
|
+
onValueChange={(value) =>
|
|
913
|
+
updateRouteState({
|
|
914
|
+
source: value,
|
|
915
|
+
runId: null,
|
|
916
|
+
threadId: null,
|
|
917
|
+
inspectSource: null,
|
|
918
|
+
})
|
|
919
|
+
}
|
|
920
|
+
>
|
|
921
|
+
<SelectTrigger>
|
|
922
|
+
<SelectValue
|
|
923
|
+
placeholder={t("dispatch.pages.threadDebugSource", {
|
|
924
|
+
defaultValue: "Source",
|
|
925
|
+
})}
|
|
926
|
+
/>
|
|
927
|
+
</SelectTrigger>
|
|
928
|
+
<SelectContent>
|
|
929
|
+
<SelectItem value="all">
|
|
930
|
+
{t("dispatch.pages.threadDebugAllSources", {
|
|
931
|
+
defaultValue: "All sources",
|
|
932
|
+
})}
|
|
933
|
+
</SelectItem>
|
|
934
|
+
{sources.map((source) => (
|
|
935
|
+
<SelectItem key={source.id} value={source.id}>
|
|
936
|
+
{source.label}
|
|
937
|
+
</SelectItem>
|
|
938
|
+
))}
|
|
939
|
+
</SelectContent>
|
|
940
|
+
</Select>
|
|
941
|
+
<Input
|
|
942
|
+
value={ownerEmail}
|
|
943
|
+
onChange={(event) =>
|
|
944
|
+
updateRouteState({ owner: event.target.value })
|
|
945
|
+
}
|
|
946
|
+
placeholder={t("dispatch.pages.threadDebugOwner", {
|
|
947
|
+
defaultValue: "Owner email",
|
|
948
|
+
})}
|
|
949
|
+
/>
|
|
950
|
+
<Select
|
|
951
|
+
value={status}
|
|
952
|
+
onValueChange={(value) =>
|
|
953
|
+
updateRouteState({
|
|
954
|
+
status: parseFailureStatus(value),
|
|
955
|
+
runId: null,
|
|
956
|
+
inspectSource: null,
|
|
957
|
+
})
|
|
958
|
+
}
|
|
959
|
+
>
|
|
960
|
+
<SelectTrigger>
|
|
961
|
+
<SelectValue
|
|
962
|
+
placeholder={t("dispatch.pages.threadDebugStatus", {
|
|
963
|
+
defaultValue: "Status",
|
|
964
|
+
})}
|
|
965
|
+
/>
|
|
966
|
+
</SelectTrigger>
|
|
967
|
+
<SelectContent>
|
|
968
|
+
<SelectItem value="all">
|
|
969
|
+
{t("dispatch.pages.threadDebugAllStatuses", {
|
|
970
|
+
defaultValue: "All statuses",
|
|
971
|
+
})}
|
|
972
|
+
</SelectItem>
|
|
973
|
+
<SelectItem value="errored">
|
|
974
|
+
{t("dispatch.pages.threadDebugErrored", {
|
|
975
|
+
defaultValue: "Errored",
|
|
976
|
+
})}
|
|
977
|
+
</SelectItem>
|
|
978
|
+
<SelectItem value="aborted">
|
|
979
|
+
{t("dispatch.pages.threadDebugAborted", {
|
|
980
|
+
defaultValue: "Aborted",
|
|
981
|
+
})}
|
|
982
|
+
</SelectItem>
|
|
983
|
+
<SelectItem value="truncated">
|
|
984
|
+
{t("dispatch.pages.threadDebugTruncated", {
|
|
985
|
+
defaultValue: "Truncated",
|
|
986
|
+
})}
|
|
987
|
+
</SelectItem>
|
|
988
|
+
</SelectContent>
|
|
989
|
+
</Select>
|
|
990
|
+
<Select
|
|
991
|
+
value={range}
|
|
992
|
+
onValueChange={(value) =>
|
|
993
|
+
updateRouteState({
|
|
994
|
+
range: parseFailureRange(value),
|
|
995
|
+
runId: null,
|
|
996
|
+
inspectSource: null,
|
|
997
|
+
})
|
|
998
|
+
}
|
|
999
|
+
>
|
|
1000
|
+
<SelectTrigger>
|
|
1001
|
+
<SelectValue
|
|
1002
|
+
placeholder={t("dispatch.pages.threadDebugRange", {
|
|
1003
|
+
defaultValue: "Time range",
|
|
1004
|
+
})}
|
|
1005
|
+
/>
|
|
1006
|
+
</SelectTrigger>
|
|
1007
|
+
<SelectContent>
|
|
1008
|
+
<SelectItem value="24h">
|
|
1009
|
+
{t("dispatch.pages.threadDebugRange24h", {
|
|
1010
|
+
defaultValue: "Last 24 hours",
|
|
1011
|
+
})}
|
|
1012
|
+
</SelectItem>
|
|
1013
|
+
<SelectItem value="7d">
|
|
1014
|
+
{t("dispatch.pages.threadDebugRange7d", {
|
|
1015
|
+
defaultValue: "Last 7 days",
|
|
1016
|
+
})}
|
|
1017
|
+
</SelectItem>
|
|
1018
|
+
<SelectItem value="30d">
|
|
1019
|
+
{t("dispatch.pages.threadDebugRange30d", {
|
|
1020
|
+
defaultValue: "Last 30 days",
|
|
1021
|
+
})}
|
|
1022
|
+
</SelectItem>
|
|
1023
|
+
</SelectContent>
|
|
1024
|
+
</Select>
|
|
1025
|
+
</div>
|
|
1026
|
+
<div className="mt-3 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
|
1027
|
+
<span>
|
|
1028
|
+
{failuresData?.count ?? failures.length}{" "}
|
|
1029
|
+
{t("dispatch.pages.threadDebugFailureResults", {
|
|
1030
|
+
defaultValue: "failed runs",
|
|
1031
|
+
})}
|
|
1032
|
+
</span>
|
|
1033
|
+
<span>·</span>
|
|
1034
|
+
<span>
|
|
1035
|
+
{failuresData?.access?.scope ??
|
|
1036
|
+
t("dispatch.pages.threadDebugCurrentScope", {
|
|
1037
|
+
defaultValue: "current scope",
|
|
1038
|
+
})}
|
|
1039
|
+
</span>
|
|
1040
|
+
{failuresData?.partial ? (
|
|
1041
|
+
<Badge variant="outline">
|
|
1042
|
+
{t("dispatch.pages.threadDebugPartialResults", {
|
|
1043
|
+
defaultValue: "Partial results",
|
|
1044
|
+
})}
|
|
1045
|
+
</Badge>
|
|
1046
|
+
) : null}
|
|
1047
|
+
</div>
|
|
1048
|
+
{unavailableFailureSources.length > 0 ? (
|
|
1049
|
+
<div className="mt-2 flex items-start gap-1.5 text-xs text-muted-foreground">
|
|
1050
|
+
<IconAlertTriangle className="mt-0.5 size-3.5 shrink-0" />
|
|
1051
|
+
<span>
|
|
1052
|
+
{t("dispatch.pages.threadDebugUnavailableSources", {
|
|
1053
|
+
defaultValue: "Unavailable sources:",
|
|
1054
|
+
})}{" "}
|
|
1055
|
+
{unavailableFailureSources
|
|
1056
|
+
.map(
|
|
1057
|
+
({ source, status: sourceStatus }) =>
|
|
1058
|
+
`${source.label} (${failureSourceStatusLabels[sourceStatus]})`,
|
|
1059
|
+
)
|
|
1060
|
+
.join(", ")}
|
|
1061
|
+
</span>
|
|
1062
|
+
</div>
|
|
1063
|
+
) : null}
|
|
1064
|
+
</section>
|
|
614
1065
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
{selectedSource.databaseUrlEnv}
|
|
621
|
-
</Badge>
|
|
622
|
-
) : null}
|
|
623
|
-
{sourcesData?.access ? (
|
|
624
|
-
<span>
|
|
625
|
-
{sourcesData.access.viewerEmail} ·{" "}
|
|
626
|
-
{sourcesData.access.canInspectAll ? "admin scope" : "own scope"}
|
|
627
|
-
</span>
|
|
1066
|
+
{failuresError ? (
|
|
1067
|
+
<ActionQueryError
|
|
1068
|
+
error={failuresError}
|
|
1069
|
+
onRetry={() => void refetchFailures()}
|
|
1070
|
+
/>
|
|
628
1071
|
) : null}
|
|
629
|
-
</div>
|
|
630
|
-
</section>
|
|
631
1072
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
1073
|
+
<div className="grid gap-4 xl:grid-cols-[380px_1fr]">
|
|
1074
|
+
<section className="min-h-[520px] rounded-lg bg-card">
|
|
1075
|
+
<div className="flex items-center justify-between border-b px-4 py-3">
|
|
1076
|
+
<div className="text-sm font-semibold text-foreground">
|
|
1077
|
+
{t("dispatch.pages.threadDebugFailedRuns", {
|
|
1078
|
+
defaultValue: "Failed runs",
|
|
1079
|
+
})}
|
|
1080
|
+
</div>
|
|
1081
|
+
<Button
|
|
1082
|
+
type="button"
|
|
1083
|
+
variant="ghost"
|
|
1084
|
+
size="icon"
|
|
1085
|
+
onClick={() => void refetchFailures()}
|
|
1086
|
+
aria-label={t("dispatch.pages.threadDebugRefreshFailures", {
|
|
1087
|
+
defaultValue: "Refresh failed runs",
|
|
1088
|
+
})}
|
|
1089
|
+
>
|
|
1090
|
+
<IconRefresh className="size-4" />
|
|
1091
|
+
</Button>
|
|
645
1092
|
</div>
|
|
646
|
-
<div className="
|
|
647
|
-
{
|
|
648
|
-
|
|
1093
|
+
<div className="max-h-[760px] space-y-2 overflow-auto p-3">
|
|
1094
|
+
{failuresLoading ? (
|
|
1095
|
+
<>
|
|
1096
|
+
<Skeleton className="h-32 w-full rounded-lg" />
|
|
1097
|
+
<Skeleton className="h-32 w-full rounded-lg" />
|
|
1098
|
+
<Skeleton className="h-32 w-full rounded-lg" />
|
|
1099
|
+
</>
|
|
1100
|
+
) : null}
|
|
1101
|
+
{!failuresLoading && failures.length === 0 ? (
|
|
1102
|
+
<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">
|
|
1103
|
+
<IconDatabase className="mb-2 size-5" />
|
|
1104
|
+
{t("dispatch.pages.threadDebugNoFailures", {
|
|
1105
|
+
defaultValue: "No failed runs found.",
|
|
1106
|
+
})}
|
|
1107
|
+
</div>
|
|
1108
|
+
) : null}
|
|
1109
|
+
{failures.map((failure) => (
|
|
1110
|
+
<FailureCard
|
|
1111
|
+
key={`${failureSourceId(failure)}:${failure.id}`}
|
|
1112
|
+
failure={failure}
|
|
1113
|
+
selected={
|
|
1114
|
+
runId === failure.id &&
|
|
1115
|
+
detailSourceId === failureSourceId(failure)
|
|
1116
|
+
}
|
|
1117
|
+
onSelect={() =>
|
|
1118
|
+
updateRouteState({
|
|
1119
|
+
inspectSource: failureSourceId(failure),
|
|
1120
|
+
runId: failure.id,
|
|
1121
|
+
threadId: null,
|
|
1122
|
+
})
|
|
1123
|
+
}
|
|
1124
|
+
/>
|
|
1125
|
+
))}
|
|
649
1126
|
</div>
|
|
650
|
-
</
|
|
651
|
-
|
|
652
|
-
type="button"
|
|
653
|
-
variant="ghost"
|
|
654
|
-
size="icon"
|
|
655
|
-
onClick={() => refetchSearch()}
|
|
656
|
-
aria-label="Refresh threads"
|
|
657
|
-
>
|
|
658
|
-
<IconRefresh size={16} />
|
|
659
|
-
</Button>
|
|
1127
|
+
</section>
|
|
1128
|
+
{detailPane}
|
|
660
1129
|
</div>
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
) : null}
|
|
675
|
-
{searchThreads.map((result) => (
|
|
676
|
-
<ResultCard
|
|
677
|
-
key={result.id}
|
|
678
|
-
result={result}
|
|
679
|
-
selected={
|
|
680
|
-
selected?.lookupKind === "thread" &&
|
|
681
|
-
selected.lookupId === result.id
|
|
682
|
-
}
|
|
683
|
-
onSelect={() =>
|
|
684
|
-
setSelected({
|
|
685
|
-
sourceId: submittedSearch.sourceId,
|
|
686
|
-
lookupId: result.id,
|
|
687
|
-
lookupKind: "thread",
|
|
688
|
-
ownerEmail: submittedSearch.ownerEmail || undefined,
|
|
1130
|
+
</TabsContent>
|
|
1131
|
+
|
|
1132
|
+
<TabsContent value="threads" className="mt-4 space-y-4">
|
|
1133
|
+
<section className="rounded-lg bg-card p-4">
|
|
1134
|
+
<div className="grid gap-3 lg:grid-cols-[220px_1fr_260px_auto]">
|
|
1135
|
+
<Select
|
|
1136
|
+
value={threadSourceId}
|
|
1137
|
+
onValueChange={(value) =>
|
|
1138
|
+
updateRouteState({
|
|
1139
|
+
source: value,
|
|
1140
|
+
runId: null,
|
|
1141
|
+
threadId: null,
|
|
1142
|
+
inspectSource: null,
|
|
689
1143
|
})
|
|
690
1144
|
}
|
|
1145
|
+
>
|
|
1146
|
+
<SelectTrigger>
|
|
1147
|
+
<SelectValue
|
|
1148
|
+
placeholder={t("dispatch.pages.threadDebugSource", {
|
|
1149
|
+
defaultValue: "Source",
|
|
1150
|
+
})}
|
|
1151
|
+
/>
|
|
1152
|
+
</SelectTrigger>
|
|
1153
|
+
<SelectContent>
|
|
1154
|
+
{sources.map((source) => (
|
|
1155
|
+
<SelectItem key={source.id} value={source.id}>
|
|
1156
|
+
{source.label}
|
|
1157
|
+
</SelectItem>
|
|
1158
|
+
))}
|
|
1159
|
+
{sources.length === 0 ? (
|
|
1160
|
+
<SelectItem value="current">
|
|
1161
|
+
{t("dispatch.pages.threadDebugCurrentDatabase", {
|
|
1162
|
+
defaultValue: "Current Dispatch DB",
|
|
1163
|
+
})}
|
|
1164
|
+
</SelectItem>
|
|
1165
|
+
) : null}
|
|
1166
|
+
</SelectContent>
|
|
1167
|
+
</Select>
|
|
1168
|
+
<Input
|
|
1169
|
+
value={query}
|
|
1170
|
+
onChange={(event) =>
|
|
1171
|
+
updateRouteState({ query: event.target.value })
|
|
1172
|
+
}
|
|
1173
|
+
placeholder={t(
|
|
1174
|
+
"dispatch.pages.threadDebugSearchPlaceholder",
|
|
1175
|
+
{
|
|
1176
|
+
defaultValue: "Search title, preview, messages, tools",
|
|
1177
|
+
},
|
|
1178
|
+
)}
|
|
691
1179
|
/>
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
1180
|
+
<Input
|
|
1181
|
+
value={ownerEmail}
|
|
1182
|
+
onChange={(event) =>
|
|
1183
|
+
updateRouteState({ owner: event.target.value })
|
|
1184
|
+
}
|
|
1185
|
+
placeholder={t("dispatch.pages.threadDebugOwner", {
|
|
1186
|
+
defaultValue: "Owner email",
|
|
1187
|
+
})}
|
|
1188
|
+
/>
|
|
1189
|
+
<Button type="button" onClick={() => void refetchSearch()}>
|
|
1190
|
+
<IconSearch className="size-4" />
|
|
1191
|
+
{t("dispatch.pages.threadDebugSearch", {
|
|
1192
|
+
defaultValue: "Search",
|
|
1193
|
+
})}
|
|
1194
|
+
</Button>
|
|
1195
|
+
</div>
|
|
1196
|
+
|
|
1197
|
+
<div className="mt-3 grid gap-3 lg:grid-cols-[1fr_auto]">
|
|
1198
|
+
<Input
|
|
1199
|
+
value={lookupId}
|
|
1200
|
+
onChange={(event) => setLookupId(event.target.value)}
|
|
1201
|
+
placeholder={t(
|
|
1202
|
+
"dispatch.pages.threadDebugLookupPlaceholder",
|
|
1203
|
+
{
|
|
1204
|
+
defaultValue: "Paste thread or request/run ID",
|
|
1205
|
+
},
|
|
1206
|
+
)}
|
|
1207
|
+
className="font-mono"
|
|
1208
|
+
/>
|
|
1209
|
+
<Button
|
|
1210
|
+
type="button"
|
|
1211
|
+
variant="outline"
|
|
1212
|
+
onClick={() => {
|
|
1213
|
+
const trimmed = lookupId.trim();
|
|
1214
|
+
if (!trimmed) return;
|
|
1215
|
+
updateRouteState(
|
|
1216
|
+
trimmed.startsWith("run-")
|
|
1217
|
+
? {
|
|
1218
|
+
runId: trimmed,
|
|
1219
|
+
threadId: null,
|
|
1220
|
+
inspectSource: null,
|
|
1221
|
+
}
|
|
1222
|
+
: {
|
|
1223
|
+
threadId: trimmed,
|
|
1224
|
+
runId: null,
|
|
1225
|
+
inspectSource: null,
|
|
1226
|
+
},
|
|
1227
|
+
);
|
|
1228
|
+
}}
|
|
1229
|
+
>
|
|
1230
|
+
<IconFileSearch className="size-4" />
|
|
1231
|
+
{t("dispatch.pages.threadDebugInspect", {
|
|
1232
|
+
defaultValue: "Inspect",
|
|
1233
|
+
})}
|
|
1234
|
+
</Button>
|
|
1235
|
+
</div>
|
|
1236
|
+
|
|
1237
|
+
<div className="mt-3 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
|
1238
|
+
{sourcesLoading ? <Skeleton className="h-5 w-32" /> : null}
|
|
1239
|
+
{selectedSource ? (
|
|
1240
|
+
<SourceBadge source={selectedSource} />
|
|
1241
|
+
) : null}
|
|
1242
|
+
{selectedSource?.databaseUrlEnv ? (
|
|
1243
|
+
<Badge variant="outline" className="font-mono">
|
|
1244
|
+
{selectedSource.databaseUrlEnv}
|
|
1245
|
+
</Badge>
|
|
1246
|
+
) : null}
|
|
1247
|
+
{sourcesData?.access ? (
|
|
1248
|
+
<span>
|
|
1249
|
+
{sourcesData.access.viewerEmail} ·{" "}
|
|
1250
|
+
{sourcesData.access.canInspectAll
|
|
1251
|
+
? t("dispatch.pages.threadDebugAdminScope", {
|
|
1252
|
+
defaultValue: "admin scope",
|
|
1253
|
+
})
|
|
1254
|
+
: t("dispatch.pages.threadDebugOwnScope", {
|
|
1255
|
+
defaultValue: "own scope",
|
|
1256
|
+
})}
|
|
1257
|
+
</span>
|
|
1258
|
+
) : null}
|
|
1259
|
+
</div>
|
|
1260
|
+
</section>
|
|
695
1261
|
|
|
696
|
-
|
|
697
|
-
{detailError ? (
|
|
1262
|
+
{searchError ? (
|
|
698
1263
|
<ActionQueryError
|
|
699
|
-
error={
|
|
700
|
-
onRetry={() => void
|
|
1264
|
+
error={searchError}
|
|
1265
|
+
onRetry={() => void refetchSearch()}
|
|
701
1266
|
/>
|
|
702
1267
|
) : null}
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
<
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
1268
|
+
|
|
1269
|
+
<div className="grid gap-4 xl:grid-cols-[380px_1fr]">
|
|
1270
|
+
<section className="min-h-[520px] rounded-lg bg-card">
|
|
1271
|
+
<div className="flex items-center justify-between border-b px-4 py-3">
|
|
1272
|
+
<div>
|
|
1273
|
+
<div className="text-sm font-semibold text-foreground">
|
|
1274
|
+
{t("dispatch.pages.threadDebugThreads", {
|
|
1275
|
+
defaultValue: "Threads",
|
|
1276
|
+
})}
|
|
1277
|
+
</div>
|
|
1278
|
+
<div className="text-xs text-muted-foreground">
|
|
1279
|
+
{searchData?.count ?? 0}{" "}
|
|
1280
|
+
{t("dispatch.pages.threadDebugResults", {
|
|
1281
|
+
defaultValue: "results",
|
|
1282
|
+
})}{" "}
|
|
1283
|
+
·{" "}
|
|
1284
|
+
{searchData?.access?.scope ??
|
|
1285
|
+
t("dispatch.pages.threadDebugCurrentScope", {
|
|
1286
|
+
defaultValue: "current scope",
|
|
1287
|
+
})}
|
|
1288
|
+
</div>
|
|
1289
|
+
</div>
|
|
1290
|
+
<Button
|
|
1291
|
+
type="button"
|
|
1292
|
+
variant="ghost"
|
|
1293
|
+
size="icon"
|
|
1294
|
+
onClick={() => void refetchSearch()}
|
|
1295
|
+
aria-label={t("dispatch.pages.threadDebugRefreshThreads", {
|
|
1296
|
+
defaultValue: "Refresh threads",
|
|
1297
|
+
})}
|
|
1298
|
+
>
|
|
1299
|
+
<IconRefresh className="size-4" />
|
|
1300
|
+
</Button>
|
|
1301
|
+
</div>
|
|
1302
|
+
<div className="max-h-[760px] space-y-2 overflow-auto p-3">
|
|
1303
|
+
{searchLoading ? (
|
|
1304
|
+
<>
|
|
1305
|
+
<Skeleton className="h-28 w-full rounded-lg" />
|
|
1306
|
+
<Skeleton className="h-28 w-full rounded-lg" />
|
|
1307
|
+
<Skeleton className="h-28 w-full rounded-lg" />
|
|
1308
|
+
</>
|
|
1309
|
+
) : null}
|
|
1310
|
+
{!searchLoading && searchThreads.length === 0 ? (
|
|
1311
|
+
<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">
|
|
1312
|
+
<IconDatabase className="mb-2 size-5" />
|
|
1313
|
+
{t("dispatch.pages.threadDebugNoThreads", {
|
|
1314
|
+
defaultValue: "No threads found.",
|
|
1315
|
+
})}
|
|
1316
|
+
</div>
|
|
1317
|
+
) : null}
|
|
1318
|
+
{searchThreads.map((result) => (
|
|
1319
|
+
<ResultCard
|
|
1320
|
+
key={result.id}
|
|
1321
|
+
result={result}
|
|
1322
|
+
selected={threadId === result.id}
|
|
1323
|
+
onSelect={() =>
|
|
1324
|
+
updateRouteState({
|
|
1325
|
+
threadId: result.id,
|
|
1326
|
+
runId: null,
|
|
1327
|
+
inspectSource: null,
|
|
1328
|
+
})
|
|
1329
|
+
}
|
|
1330
|
+
/>
|
|
1331
|
+
))}
|
|
1332
|
+
</div>
|
|
1333
|
+
</section>
|
|
1334
|
+
{detailPane}
|
|
1335
|
+
</div>
|
|
1336
|
+
</TabsContent>
|
|
1337
|
+
</Tabs>
|
|
719
1338
|
</div>
|
|
720
1339
|
</DispatchShell>
|
|
721
1340
|
);
|