@agent-native/dispatch 0.21.0 → 0.22.1
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 +2 -0
- package/dist/actions/provider-api-register.d.ts +8 -8
- package/dist/actions/search-agent-threads.js +2 -2
- package/dist/actions/search-agent-threads.js.map +1 -1
- package/dist/components/layout/HeaderActions.d.ts +1 -17
- package/dist/components/layout/HeaderActions.d.ts.map +1 -1
- package/dist/components/layout/HeaderActions.js +1 -51
- package/dist/components/layout/HeaderActions.js.map +1 -1
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +22 -14
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/ui/dialog.d.ts +1 -19
- package/dist/components/ui/dialog.d.ts.map +1 -1
- package/dist/components/ui/dialog.js +1 -22
- package/dist/components/ui/dialog.js.map +1 -1
- package/dist/components/ui/dropdown-menu.d.ts +1 -27
- package/dist/components/ui/dropdown-menu.d.ts.map +1 -1
- package/dist/components/ui/dropdown-menu.js +1 -35
- package/dist/components/ui/dropdown-menu.js.map +1 -1
- package/dist/components/ui/popover.d.ts +2 -1
- package/dist/components/ui/popover.d.ts.map +1 -1
- package/dist/components/ui/popover.js +2 -1
- package/dist/components/ui/popover.js.map +1 -1
- package/dist/components/ui/select.d.ts +1 -13
- package/dist/components/ui/select.d.ts.map +1 -1
- package/dist/components/ui/select.js +1 -26
- package/dist/components/ui/select.js.map +1 -1
- package/dist/hooks/use-mobile.d.ts +1 -1
- package/dist/hooks/use-mobile.d.ts.map +1 -1
- package/dist/hooks/use-mobile.js +1 -15
- package/dist/hooks/use-mobile.js.map +1 -1
- package/dist/lib/automation-display.js +1 -1
- package/dist/lib/automation-display.js.map +1 -1
- package/dist/routes/pages/thread-debug.d.ts.map +1 -1
- package/dist/routes/pages/thread-debug.js +288 -118
- package/dist/routes/pages/thread-debug.js.map +1 -1
- package/dist/server/lib/thread-debug-store.d.ts +2 -0
- package/dist/server/lib/thread-debug-store.d.ts.map +1 -1
- package/dist/server/lib/thread-debug-store.js +6 -2
- package/dist/server/lib/thread-debug-store.js.map +1 -1
- package/package.json +3 -3
- package/src/actions/search-agent-threads.ts +2 -2
- package/src/components/layout/HeaderActions.tsx +1 -84
- package/src/components/layout/Layout.spec.tsx +94 -10
- package/src/components/layout/Layout.tsx +107 -101
- package/src/components/ui/dialog.tsx +1 -120
- package/src/components/ui/dropdown-menu.tsx +1 -198
- package/src/components/ui/popover.tsx +3 -1
- package/src/components/ui/select.tsx +1 -158
- package/src/hooks/use-mobile.tsx +1 -21
- package/src/lib/automation-display.spec.ts +1 -1
- package/src/lib/automation-display.ts +1 -1
- package/src/routes/pages/thread-debug.spec.tsx +103 -5
- package/src/routes/pages/thread-debug.tsx +1051 -451
- package/src/server/lib/thread-debug-store.spec.ts +17 -0
- package/src/server/lib/thread-debug-store.ts +9 -2
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { useActionQuery } from "@agent-native/core/client/hooks";
|
|
2
2
|
import { useT } from "@agent-native/core/client/i18n";
|
|
3
3
|
import {
|
|
4
|
+
IconActivity,
|
|
5
|
+
IconAdjustmentsHorizontal,
|
|
4
6
|
IconAlertTriangle,
|
|
5
|
-
|
|
7
|
+
IconCopy,
|
|
6
8
|
IconDatabase,
|
|
7
9
|
IconFileSearch,
|
|
10
|
+
IconInfoCircle,
|
|
8
11
|
IconRefresh,
|
|
9
12
|
IconSearch,
|
|
13
|
+
IconTool,
|
|
10
14
|
} from "@tabler/icons-react";
|
|
11
15
|
import { useMemo, useState } from "react";
|
|
12
16
|
import { useSearchParams } from "react-router";
|
|
@@ -16,6 +20,11 @@ import { DispatchShell } from "../../components/dispatch-shell";
|
|
|
16
20
|
import { Badge } from "../../components/ui/badge";
|
|
17
21
|
import { Button } from "../../components/ui/button";
|
|
18
22
|
import { Input } from "../../components/ui/input";
|
|
23
|
+
import {
|
|
24
|
+
Popover,
|
|
25
|
+
PopoverAnchor,
|
|
26
|
+
PopoverContent,
|
|
27
|
+
} from "../../components/ui/popover";
|
|
19
28
|
import {
|
|
20
29
|
Select,
|
|
21
30
|
SelectContent,
|
|
@@ -87,13 +96,38 @@ interface ThreadRun {
|
|
|
87
96
|
lastProgressAt?: number | null;
|
|
88
97
|
durationMs?: number | null;
|
|
89
98
|
peakRssMb?: number | null;
|
|
99
|
+
inFlightSince?: number | null;
|
|
100
|
+
hasDispatchPayload?: boolean;
|
|
90
101
|
events: Array<{ seq: number; event: any; rawEventData: string }>;
|
|
91
102
|
}
|
|
92
103
|
|
|
93
104
|
type ThreadDebugMode = "failures" | "threads";
|
|
94
105
|
type FailureStatus = "all" | "errored" | "aborted" | "truncated";
|
|
106
|
+
type FailureRegime = "all" | "interactive" | "scheduled";
|
|
95
107
|
type FailureRange = "24h" | "7d" | "30d";
|
|
96
108
|
|
|
109
|
+
interface FailureTaxonomy {
|
|
110
|
+
code: string;
|
|
111
|
+
label: string;
|
|
112
|
+
regime: "interactive" | "scheduled";
|
|
113
|
+
source: "error_code" | "error_detail" | "unknown";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
interface RunFailureLike {
|
|
117
|
+
id?: string;
|
|
118
|
+
status?: string | null;
|
|
119
|
+
errorCode?: string | null;
|
|
120
|
+
errorDetail?: string | null;
|
|
121
|
+
terminalReason?: string | null;
|
|
122
|
+
abortReason?: string | null;
|
|
123
|
+
dispatchMode?: string | null;
|
|
124
|
+
diagStage?: string | null;
|
|
125
|
+
workerStage?: string | null;
|
|
126
|
+
durationMs?: number | null;
|
|
127
|
+
heartbeatAt?: number | null;
|
|
128
|
+
lastProgressAt?: number | null;
|
|
129
|
+
}
|
|
130
|
+
|
|
97
131
|
interface AgentRunFailure {
|
|
98
132
|
id: string;
|
|
99
133
|
threadId: string;
|
|
@@ -106,6 +140,7 @@ interface AgentRunFailure {
|
|
|
106
140
|
databaseUrlEnv?: string | null;
|
|
107
141
|
};
|
|
108
142
|
ownerEmail: string;
|
|
143
|
+
turnId?: string | null;
|
|
109
144
|
threadTitle: string;
|
|
110
145
|
threadPreview: string;
|
|
111
146
|
status: string;
|
|
@@ -113,12 +148,16 @@ interface AgentRunFailure {
|
|
|
113
148
|
errorDetail: string | null;
|
|
114
149
|
terminalReason: string | null;
|
|
115
150
|
abortReason: string | null;
|
|
151
|
+
heartbeatAt?: number | null;
|
|
152
|
+
lastProgressAt?: number | null;
|
|
116
153
|
dispatchMode: string | null;
|
|
117
154
|
diagStage: string | null;
|
|
118
155
|
workerStage?: string | null;
|
|
119
156
|
startedAt: number;
|
|
120
157
|
completedAt: number | null;
|
|
121
158
|
durationMs: number | null;
|
|
159
|
+
regime?: "interactive" | "scheduled";
|
|
160
|
+
failureTaxonomy?: FailureTaxonomy;
|
|
122
161
|
}
|
|
123
162
|
|
|
124
163
|
interface AgentRunFailuresResponse {
|
|
@@ -175,6 +214,8 @@ const FAILURE_RANGE_HOURS: Record<FailureRange, number> = {
|
|
|
175
214
|
"30d": 30 * 24,
|
|
176
215
|
};
|
|
177
216
|
|
|
217
|
+
const EMPTY_FAILURES: AgentRunFailure[] = [];
|
|
218
|
+
|
|
178
219
|
function parseMode(value: string | null): ThreadDebugMode {
|
|
179
220
|
return value === "threads" ? "threads" : "failures";
|
|
180
221
|
}
|
|
@@ -185,6 +226,10 @@ function parseFailureStatus(value: string | null): FailureStatus {
|
|
|
185
226
|
: "all";
|
|
186
227
|
}
|
|
187
228
|
|
|
229
|
+
function parseFailureRegime(value: string | null): FailureRegime {
|
|
230
|
+
return value === "interactive" || value === "scheduled" ? value : "all";
|
|
231
|
+
}
|
|
232
|
+
|
|
188
233
|
function parseFailureRange(value: string | null): FailureRange {
|
|
189
234
|
return value === "7d" || value === "30d" ? value : "24h";
|
|
190
235
|
}
|
|
@@ -199,6 +244,39 @@ function failureSourceLabel(failure: AgentRunFailure): string {
|
|
|
199
244
|
);
|
|
200
245
|
}
|
|
201
246
|
|
|
247
|
+
function isTechnicalIdentifier(value: string): boolean {
|
|
248
|
+
return /^(?:job|run|thread|turn|request|req)-[a-z0-9][a-z0-9_-]*$/i.test(
|
|
249
|
+
value.trim(),
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function displayListTitle(
|
|
254
|
+
value: string | null | undefined,
|
|
255
|
+
fallbackValue: string | null | undefined,
|
|
256
|
+
fallbackLabel: string,
|
|
257
|
+
): string {
|
|
258
|
+
const candidate = [value, fallbackValue]
|
|
259
|
+
.map((entry) => entry?.trim() || "")
|
|
260
|
+
.find((entry) => entry && !isTechnicalIdentifier(entry));
|
|
261
|
+
if (!candidate) return fallbackLabel;
|
|
262
|
+
|
|
263
|
+
const withoutDate = candidate
|
|
264
|
+
.replace(/\s+[—-]\s+\d{1,2}\/\d{1,2}\/\d{4}\s*$/, "")
|
|
265
|
+
.trim();
|
|
266
|
+
if (!withoutDate) return fallbackLabel;
|
|
267
|
+
|
|
268
|
+
if (/^job:/i.test(withoutDate)) {
|
|
269
|
+
return withoutDate
|
|
270
|
+
.replace(/^job:\s*/i, "")
|
|
271
|
+
.replace(/[-_]+/g, " ")
|
|
272
|
+
.replace(/\s+/g, " ")
|
|
273
|
+
.replace(/\b\w/g, (character) => character.toUpperCase())
|
|
274
|
+
.trim();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return withoutDate;
|
|
278
|
+
}
|
|
279
|
+
|
|
202
280
|
function formatDate(value: number | string | null | undefined): string {
|
|
203
281
|
if (value == null || value === "") return "n/a";
|
|
204
282
|
const numeric = Number(value);
|
|
@@ -207,6 +285,21 @@ function formatDate(value: number | string | null | undefined): string {
|
|
|
207
285
|
return date.toLocaleString();
|
|
208
286
|
}
|
|
209
287
|
|
|
288
|
+
function formatRelativeDate(value: number | string | null | undefined): string {
|
|
289
|
+
if (value == null || value === "") return "n/a";
|
|
290
|
+
const numeric = Number(value);
|
|
291
|
+
const timestamp = Number.isFinite(numeric)
|
|
292
|
+
? numeric
|
|
293
|
+
: new Date(value).getTime();
|
|
294
|
+
if (!Number.isFinite(timestamp)) return "n/a";
|
|
295
|
+
const elapsed = Date.now() - timestamp;
|
|
296
|
+
if (elapsed < 0) return "in a moment";
|
|
297
|
+
if (elapsed < 60_000) return "just now";
|
|
298
|
+
if (elapsed < 3_600_000) return `${Math.floor(elapsed / 60_000)}m ago`;
|
|
299
|
+
if (elapsed < 86_400_000) return `${Math.floor(elapsed / 3_600_000)}h ago`;
|
|
300
|
+
return `${Math.floor(elapsed / 86_400_000)}d ago`;
|
|
301
|
+
}
|
|
302
|
+
|
|
210
303
|
function formatDuration(value: number | null | undefined): string {
|
|
211
304
|
if (value == null || !Number.isFinite(value)) return "n/a";
|
|
212
305
|
if (value < 1_000) return `${Math.round(value)}ms`;
|
|
@@ -214,6 +307,130 @@ function formatDuration(value: number | null | undefined): string {
|
|
|
214
307
|
return `${(value / 60_000).toFixed(1)}m`;
|
|
215
308
|
}
|
|
216
309
|
|
|
310
|
+
function humanizeIdentifier(value: string | null | undefined): string {
|
|
311
|
+
if (!value) return "Unknown failure";
|
|
312
|
+
return value
|
|
313
|
+
.replace(/^(error:|failure:)/, "")
|
|
314
|
+
.replace(/[_-]+/g, " ")
|
|
315
|
+
.replace(/\b\w/g, (character) => character.toUpperCase());
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function failureLabel(run: RunFailureLike): string {
|
|
319
|
+
const code = run.errorCode || run.terminalReason || run.abortReason;
|
|
320
|
+
const labels: Record<string, string> = {
|
|
321
|
+
stale_run: "Worker heartbeat stopped",
|
|
322
|
+
background_worker_never_started: "Background worker never started",
|
|
323
|
+
background_worker_failed: "Background worker setup failed",
|
|
324
|
+
builder_gateway_network_error: "Gateway stream ended early",
|
|
325
|
+
provider_timeout: "Provider timed out",
|
|
326
|
+
provider_network_error: "Provider connection dropped",
|
|
327
|
+
provider_config_error: "Provider configuration rejected",
|
|
328
|
+
authentication_error: "Provider authentication failed",
|
|
329
|
+
overloaded_error: "Provider was overloaded",
|
|
330
|
+
"aborted:user": "Stopped by user",
|
|
331
|
+
};
|
|
332
|
+
return (code && labels[code]) || humanizeIdentifier(code);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function runDiagnosis(run: RunFailureLike): {
|
|
336
|
+
title: string;
|
|
337
|
+
summary: string;
|
|
338
|
+
nextStep: string;
|
|
339
|
+
code: string;
|
|
340
|
+
} {
|
|
341
|
+
const code =
|
|
342
|
+
run.errorCode || run.terminalReason || run.abortReason || "unknown";
|
|
343
|
+
if (code === "stale_run") {
|
|
344
|
+
const workerStarted = run.dispatchMode === "background-processing";
|
|
345
|
+
return {
|
|
346
|
+
title: "Worker stopped reporting",
|
|
347
|
+
summary: workerStarted
|
|
348
|
+
? "The worker claimed this run, then stopped writing heartbeat or progress signals before it finished. This is a liveness failure, not proof that the provider failed."
|
|
349
|
+
: "The run stayed active until liveness recovery closed it. No completed result was recorded, so inspect the handoff and retained evidence before blaming a provider.",
|
|
350
|
+
nextStep: workerStarted
|
|
351
|
+
? "Check the last worker stage and database heartbeat writes."
|
|
352
|
+
: "Check the scheduled background handoff and whether a worker claimed it.",
|
|
353
|
+
code,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
if (code === "background_worker_never_started") {
|
|
357
|
+
return {
|
|
358
|
+
title: "Background worker never started",
|
|
359
|
+
summary:
|
|
360
|
+
"The handoff was acknowledged, but no background worker claimed the run.",
|
|
361
|
+
nextStep:
|
|
362
|
+
"Check the background route, authentication, and function logs.",
|
|
363
|
+
code,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
if (code === "background_worker_failed") {
|
|
367
|
+
return {
|
|
368
|
+
title: "Background worker failed during setup",
|
|
369
|
+
summary:
|
|
370
|
+
"The worker claimed the run but stopped before it could start the turn.",
|
|
371
|
+
nextStep:
|
|
372
|
+
"Use the recorded setup stage and worker logs to find the first failure.",
|
|
373
|
+
code,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
if (code === "builder_gateway_network_error") {
|
|
377
|
+
return {
|
|
378
|
+
title: "Gateway stream ended early",
|
|
379
|
+
summary:
|
|
380
|
+
run.errorDetail ||
|
|
381
|
+
"The model stream ended before the run emitted a terminal event.",
|
|
382
|
+
nextStep:
|
|
383
|
+
"Retry once; if it repeats, inspect gateway and provider transport health.",
|
|
384
|
+
code,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
if (code === "aborted:user") {
|
|
388
|
+
return {
|
|
389
|
+
title: "Stopped by user",
|
|
390
|
+
summary: "This run was explicitly aborted and is not a system failure.",
|
|
391
|
+
nextStep: "No recovery action is required.",
|
|
392
|
+
code,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
title: failureLabel(run),
|
|
397
|
+
summary:
|
|
398
|
+
run.errorDetail || "The run ended without a more specific explanation.",
|
|
399
|
+
nextStep: "Open the timeline and inspect the last recorded stage or event.",
|
|
400
|
+
code,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function eventType(event: any): string {
|
|
405
|
+
return typeof event?.type === "string" ? event.type : "event";
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function eventIsNoise(event: any): boolean {
|
|
409
|
+
return [
|
|
410
|
+
"thinking",
|
|
411
|
+
"text",
|
|
412
|
+
"tool_input_delta",
|
|
413
|
+
"stream_keepalive",
|
|
414
|
+
"activity",
|
|
415
|
+
].includes(eventType(event));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function summarizeEvents(events: ThreadRun["events"]) {
|
|
419
|
+
const toolNames = new Map<string, number>();
|
|
420
|
+
let toolStarts = 0;
|
|
421
|
+
for (const entry of events) {
|
|
422
|
+
if (eventType(entry.event) !== "tool_start") continue;
|
|
423
|
+
toolStarts += 1;
|
|
424
|
+
const name = String(entry.event?.tool ?? "tool");
|
|
425
|
+
toolNames.set(name, (toolNames.get(name) ?? 0) + 1);
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
toolStarts,
|
|
429
|
+
toolNames: [...toolNames.entries()],
|
|
430
|
+
meaningful: events.filter((entry) => !eventIsNoise(entry.event)).slice(-12),
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
217
434
|
function json(value: unknown): string {
|
|
218
435
|
try {
|
|
219
436
|
return JSON.stringify(value, null, 2);
|
|
@@ -278,14 +495,6 @@ function RawBlock({
|
|
|
278
495
|
);
|
|
279
496
|
}
|
|
280
497
|
|
|
281
|
-
function SourceBadge({ source }: { source: ThreadDebugSource }) {
|
|
282
|
-
return (
|
|
283
|
-
<Badge variant={source.current ? "default" : "secondary"}>
|
|
284
|
-
{source.current ? "current" : source.kind}
|
|
285
|
-
</Badge>
|
|
286
|
-
);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
498
|
function ResultCard({
|
|
290
499
|
result,
|
|
291
500
|
selected,
|
|
@@ -295,36 +504,36 @@ function ResultCard({
|
|
|
295
504
|
selected: boolean;
|
|
296
505
|
onSelect: () => void;
|
|
297
506
|
}) {
|
|
507
|
+
const title = displayListTitle(
|
|
508
|
+
result.title,
|
|
509
|
+
result.preview,
|
|
510
|
+
"Untitled thread",
|
|
511
|
+
);
|
|
512
|
+
|
|
298
513
|
return (
|
|
299
514
|
<button
|
|
300
515
|
type="button"
|
|
301
516
|
onClick={onSelect}
|
|
517
|
+
aria-current={selected ? "true" : undefined}
|
|
302
518
|
className={cn(
|
|
303
|
-
"w-full
|
|
304
|
-
selected
|
|
305
|
-
? "border-foreground bg-muted"
|
|
306
|
-
: "bg-card hover:border-foreground/30 hover:bg-muted/40",
|
|
519
|
+
"group w-full border-b px-4 py-3 text-left transition-colors last:border-b-0",
|
|
520
|
+
selected ? "bg-accent/70" : "hover:bg-muted/50",
|
|
307
521
|
)}
|
|
308
522
|
>
|
|
309
523
|
<div className="flex items-start justify-between gap-3">
|
|
310
524
|
<div className="min-w-0">
|
|
311
525
|
<div className="truncate text-sm font-medium text-foreground">
|
|
312
|
-
{
|
|
313
|
-
</div>
|
|
314
|
-
<div className="mt-1 truncate font-mono text-[11px] text-muted-foreground">
|
|
315
|
-
{result.id}
|
|
526
|
+
{title}
|
|
316
527
|
</div>
|
|
317
528
|
</div>
|
|
318
529
|
<Badge variant="outline" className="shrink-0">
|
|
319
|
-
{result.messageCount}
|
|
530
|
+
{result.messageCount}{" "}
|
|
531
|
+
{result.messageCount === 1 ? "message" : "messages"}
|
|
320
532
|
</Badge>
|
|
321
533
|
</div>
|
|
322
|
-
<div className="mt-2 line-clamp-3 text-xs leading-relaxed text-muted-foreground">
|
|
323
|
-
{result.snippet || result.preview || "No preview"}
|
|
324
|
-
</div>
|
|
325
534
|
<div className="mt-2 flex items-center justify-between gap-3 text-[11px] text-muted-foreground">
|
|
326
|
-
<span className="truncate">{result.ownerEmail}</span>
|
|
327
|
-
<span className="shrink-0">{
|
|
535
|
+
<span className="min-w-0 truncate">{result.ownerEmail}</span>
|
|
536
|
+
<span className="shrink-0">{formatRelativeDate(result.updatedAt)}</span>
|
|
328
537
|
</div>
|
|
329
538
|
</button>
|
|
330
539
|
);
|
|
@@ -340,11 +549,12 @@ function FailureCard({
|
|
|
340
549
|
onSelect: () => void;
|
|
341
550
|
}) {
|
|
342
551
|
const t = useT();
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
failure.
|
|
346
|
-
failure.
|
|
347
|
-
|
|
552
|
+
const diagnosis = runDiagnosis(failure);
|
|
553
|
+
const title = displayListTitle(
|
|
554
|
+
failure.threadTitle,
|
|
555
|
+
failure.threadPreview,
|
|
556
|
+
"Agent run",
|
|
557
|
+
);
|
|
348
558
|
const statusLabel =
|
|
349
559
|
failure.status === "errored"
|
|
350
560
|
? t("dispatch.pages.threadDebugErrored", {
|
|
@@ -364,46 +574,51 @@ function FailureCard({
|
|
|
364
574
|
<button
|
|
365
575
|
type="button"
|
|
366
576
|
onClick={onSelect}
|
|
577
|
+
aria-current={selected ? "true" : undefined}
|
|
367
578
|
className={cn(
|
|
368
|
-
"w-full
|
|
369
|
-
selected
|
|
370
|
-
? "border-foreground bg-muted"
|
|
371
|
-
: "bg-card hover:border-foreground/30 hover:bg-muted/40",
|
|
579
|
+
"group w-full border-b px-4 py-3 text-left transition-[background-color,border-color] last:border-b-0",
|
|
580
|
+
selected ? "bg-accent/70" : "hover:bg-muted/50",
|
|
372
581
|
)}
|
|
373
582
|
>
|
|
374
|
-
<div className="flex items-start
|
|
375
|
-
<
|
|
376
|
-
|
|
377
|
-
|
|
583
|
+
<div className="flex items-start gap-3">
|
|
584
|
+
<span
|
|
585
|
+
aria-hidden="true"
|
|
586
|
+
className={cn(
|
|
587
|
+
"mt-1.5 size-2 shrink-0 rounded-full",
|
|
588
|
+
failure.status === "aborted"
|
|
589
|
+
? "bg-muted-foreground/50"
|
|
590
|
+
: "bg-destructive",
|
|
591
|
+
)}
|
|
592
|
+
/>
|
|
593
|
+
<div className="min-w-0 flex-1">
|
|
594
|
+
<div className="flex items-start justify-between gap-3">
|
|
595
|
+
<div className="min-w-0">
|
|
596
|
+
<div className="truncate text-sm font-medium text-foreground">
|
|
597
|
+
{title}
|
|
598
|
+
</div>
|
|
599
|
+
<div className="mt-0.5 truncate text-xs text-muted-foreground">
|
|
600
|
+
{failureSourceLabel(failure)}
|
|
601
|
+
</div>
|
|
602
|
+
</div>
|
|
603
|
+
<span className="shrink-0 text-[11px] text-muted-foreground">
|
|
604
|
+
{formatRelativeDate(failure.completedAt ?? failure.startedAt)}
|
|
605
|
+
</span>
|
|
378
606
|
</div>
|
|
379
|
-
<div className="mt-
|
|
380
|
-
|
|
607
|
+
<div className="mt-2 flex min-w-0 items-center gap-2 text-xs">
|
|
608
|
+
<span className="truncate font-medium text-foreground">
|
|
609
|
+
{diagnosis.title}
|
|
610
|
+
</span>
|
|
611
|
+
<span className="shrink-0 text-muted-foreground">·</span>
|
|
612
|
+
<span className="shrink-0 text-muted-foreground">
|
|
613
|
+
{statusLabel}
|
|
614
|
+
</span>
|
|
381
615
|
</div>
|
|
616
|
+
{failure.durationMs == null ? null : (
|
|
617
|
+
<div className="mt-1 text-[11px] text-muted-foreground">
|
|
618
|
+
{formatDuration(failure.durationMs)}
|
|
619
|
+
</div>
|
|
620
|
+
)}
|
|
382
621
|
</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
622
|
</div>
|
|
408
623
|
<span className="sr-only">
|
|
409
624
|
{t("dispatch.pages.threadDebugInspectFailure", {
|
|
@@ -464,8 +679,209 @@ function MessageBlock({ message }: { message: ThreadMessage }) {
|
|
|
464
679
|
);
|
|
465
680
|
}
|
|
466
681
|
|
|
682
|
+
function EvidenceStat({ label, value }: { label: string; value: string }) {
|
|
683
|
+
return (
|
|
684
|
+
<div className="min-w-0">
|
|
685
|
+
<div className="text-[11px] text-muted-foreground">{label}</div>
|
|
686
|
+
<div className="mt-1 truncate text-sm font-medium text-foreground">
|
|
687
|
+
{value}
|
|
688
|
+
</div>
|
|
689
|
+
</div>
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function DiagnosisPanel({
|
|
694
|
+
run,
|
|
695
|
+
eventCount,
|
|
696
|
+
toolCount,
|
|
697
|
+
}: {
|
|
698
|
+
run: ThreadRun;
|
|
699
|
+
eventCount: number;
|
|
700
|
+
toolCount: number;
|
|
701
|
+
}) {
|
|
702
|
+
const diagnosis = runDiagnosis(run);
|
|
703
|
+
const isStoppedByUser = diagnosis.code === "aborted:user";
|
|
704
|
+
const isScheduled =
|
|
705
|
+
run.id?.startsWith("job-") || run.dispatchMode === "background-processing";
|
|
706
|
+
const workerClaimed = run.dispatchMode === "background-processing";
|
|
707
|
+
const staleThreshold = isScheduled
|
|
708
|
+
? workerClaimed
|
|
709
|
+
? "45s after claim"
|
|
710
|
+
: "90s before claim"
|
|
711
|
+
: "15s";
|
|
712
|
+
return (
|
|
713
|
+
<section
|
|
714
|
+
className={cn(
|
|
715
|
+
"border-b px-5 py-4",
|
|
716
|
+
isStoppedByUser ? "bg-muted/20" : "bg-destructive/[0.04]",
|
|
717
|
+
)}
|
|
718
|
+
aria-label="Run diagnosis"
|
|
719
|
+
>
|
|
720
|
+
<div className="flex items-start gap-3">
|
|
721
|
+
<span
|
|
722
|
+
aria-hidden="true"
|
|
723
|
+
className={cn(
|
|
724
|
+
"mt-1.5 size-2 shrink-0 rounded-full",
|
|
725
|
+
isStoppedByUser ? "bg-muted-foreground/50" : "bg-destructive",
|
|
726
|
+
)}
|
|
727
|
+
/>
|
|
728
|
+
<div className="min-w-0 flex-1">
|
|
729
|
+
<div className="flex flex-wrap items-start justify-between gap-2">
|
|
730
|
+
<div>
|
|
731
|
+
<h2 className="text-sm font-semibold text-foreground">
|
|
732
|
+
{diagnosis.title}
|
|
733
|
+
</h2>
|
|
734
|
+
<p className="mt-1 max-w-3xl text-sm leading-relaxed text-muted-foreground">
|
|
735
|
+
{diagnosis.summary}
|
|
736
|
+
</p>
|
|
737
|
+
</div>
|
|
738
|
+
<span className="font-mono text-[11px] text-muted-foreground">
|
|
739
|
+
{diagnosis.code}
|
|
740
|
+
</span>
|
|
741
|
+
</div>
|
|
742
|
+
<div className="mt-3 flex items-start gap-2 text-xs text-foreground">
|
|
743
|
+
<IconInfoCircle className="mt-0.5 size-3.5 shrink-0 text-muted-foreground" />
|
|
744
|
+
<span>
|
|
745
|
+
<span className="font-medium">Next check:</span>{" "}
|
|
746
|
+
{diagnosis.nextStep}
|
|
747
|
+
</span>
|
|
748
|
+
</div>
|
|
749
|
+
</div>
|
|
750
|
+
</div>
|
|
751
|
+
<div className="mt-4 grid gap-3 border-t pt-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-7">
|
|
752
|
+
<EvidenceStat
|
|
753
|
+
label="Run type"
|
|
754
|
+
value={isScheduled ? "Scheduled job" : "Interactive chat"}
|
|
755
|
+
/>
|
|
756
|
+
<EvidenceStat
|
|
757
|
+
label="Worker claim"
|
|
758
|
+
value={workerClaimed ? "Claimed" : "Not recorded"}
|
|
759
|
+
/>
|
|
760
|
+
<EvidenceStat
|
|
761
|
+
label="Last heartbeat"
|
|
762
|
+
value={formatDate(run.heartbeatAt)}
|
|
763
|
+
/>
|
|
764
|
+
<EvidenceStat
|
|
765
|
+
label="Last progress"
|
|
766
|
+
value={formatDate(run.lastProgressAt)}
|
|
767
|
+
/>
|
|
768
|
+
<EvidenceStat label="Stale threshold" value={staleThreshold} />
|
|
769
|
+
<EvidenceStat
|
|
770
|
+
label="Recovery payload"
|
|
771
|
+
value={run.hasDispatchPayload ? "Retained" : "Not retained"}
|
|
772
|
+
/>
|
|
773
|
+
<EvidenceStat
|
|
774
|
+
label="Worker stage"
|
|
775
|
+
value={
|
|
776
|
+
diagnosticStage(run.workerStage) ||
|
|
777
|
+
diagnosticStage(run.diagStage) ||
|
|
778
|
+
"Not recorded"
|
|
779
|
+
}
|
|
780
|
+
/>
|
|
781
|
+
</div>
|
|
782
|
+
<div className="mt-3 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground">
|
|
783
|
+
<span>
|
|
784
|
+
Evidence:{" "}
|
|
785
|
+
<span className="font-medium text-foreground">
|
|
786
|
+
{eventCount.toLocaleString()}
|
|
787
|
+
</span>{" "}
|
|
788
|
+
retained events
|
|
789
|
+
</span>
|
|
790
|
+
<span aria-hidden="true">·</span>
|
|
791
|
+
<span>
|
|
792
|
+
<span className="font-medium text-foreground">
|
|
793
|
+
{toolCount.toLocaleString()}
|
|
794
|
+
</span>{" "}
|
|
795
|
+
tool starts
|
|
796
|
+
</span>
|
|
797
|
+
<span aria-hidden="true">·</span>
|
|
798
|
+
<span>Liveness uses the newer heartbeat or progress timestamp.</span>
|
|
799
|
+
</div>
|
|
800
|
+
</section>
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function RunTimeline({ run }: { run: ThreadRun }) {
|
|
805
|
+
const summary = summarizeEvents(run.events);
|
|
806
|
+
return (
|
|
807
|
+
<div className="space-y-4">
|
|
808
|
+
<div className="flex flex-wrap items-center justify-between gap-3 border-b pb-3">
|
|
809
|
+
<div className="flex items-center gap-2">
|
|
810
|
+
<IconActivity className="size-4 text-muted-foreground" />
|
|
811
|
+
<span className="text-sm font-medium text-foreground">
|
|
812
|
+
{run.events.length.toLocaleString()} events retained
|
|
813
|
+
</span>
|
|
814
|
+
<span className="text-xs text-muted-foreground">
|
|
815
|
+
{summary.toolStarts} tool starts
|
|
816
|
+
</span>
|
|
817
|
+
</div>
|
|
818
|
+
<span className="text-xs text-muted-foreground">
|
|
819
|
+
Started {formatDate(run.startedAt)}
|
|
820
|
+
</span>
|
|
821
|
+
</div>
|
|
822
|
+
|
|
823
|
+
{summary.toolNames.length > 0 ? (
|
|
824
|
+
<div className="flex flex-wrap gap-2">
|
|
825
|
+
{summary.toolNames.map(([name, count]) => (
|
|
826
|
+
<span
|
|
827
|
+
key={name}
|
|
828
|
+
className="inline-flex items-center gap-1.5 rounded-md bg-muted/60 px-2 py-1 text-xs text-foreground"
|
|
829
|
+
>
|
|
830
|
+
<IconTool className="size-3.5 text-muted-foreground" />
|
|
831
|
+
{name} <span className="text-muted-foreground">×{count}</span>
|
|
832
|
+
</span>
|
|
833
|
+
))}
|
|
834
|
+
</div>
|
|
835
|
+
) : null}
|
|
836
|
+
|
|
837
|
+
<div className="divide-y rounded-lg border">
|
|
838
|
+
{summary.meaningful.length > 0 ? (
|
|
839
|
+
summary.meaningful.map((entry) => (
|
|
840
|
+
<div
|
|
841
|
+
key={`${run.id}-${entry.seq}`}
|
|
842
|
+
className="flex items-start gap-3 px-3 py-2.5 text-xs"
|
|
843
|
+
>
|
|
844
|
+
<span className="w-8 shrink-0 font-mono text-muted-foreground">
|
|
845
|
+
#{entry.seq}
|
|
846
|
+
</span>
|
|
847
|
+
<span className="min-w-0 flex-1 break-words text-foreground">
|
|
848
|
+
{eventLabel(entry.event)}
|
|
849
|
+
</span>
|
|
850
|
+
</div>
|
|
851
|
+
))
|
|
852
|
+
) : (
|
|
853
|
+
<div className="px-3 py-4 text-sm text-muted-foreground">
|
|
854
|
+
No summarized events were retained for this run.
|
|
855
|
+
</div>
|
|
856
|
+
)}
|
|
857
|
+
</div>
|
|
858
|
+
|
|
859
|
+
<details className="rounded-lg border">
|
|
860
|
+
<summary className="flex cursor-pointer list-none items-center justify-between gap-3 px-3 py-2.5 text-xs font-medium text-foreground">
|
|
861
|
+
<span>Show raw event stream</span>
|
|
862
|
+
<span className="font-normal text-muted-foreground">
|
|
863
|
+
{run.events.length.toLocaleString()} records
|
|
864
|
+
</span>
|
|
865
|
+
</summary>
|
|
866
|
+
<div className="space-y-2 border-t p-3">
|
|
867
|
+
{run.events.map((entry) => (
|
|
868
|
+
<details
|
|
869
|
+
key={`${run.id}-raw-${entry.seq}`}
|
|
870
|
+
className="rounded-md border bg-muted/20 px-3 py-2"
|
|
871
|
+
>
|
|
872
|
+
<summary className="cursor-pointer text-xs text-foreground">
|
|
873
|
+
#{entry.seq} {eventLabel(entry.event)}
|
|
874
|
+
</summary>
|
|
875
|
+
<RawBlock value={entry.event} className="mt-2 max-h-72" />
|
|
876
|
+
</details>
|
|
877
|
+
))}
|
|
878
|
+
</div>
|
|
879
|
+
</details>
|
|
880
|
+
</div>
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
|
|
467
884
|
function ThreadDetail({ detail }: { detail: ThreadDebugResponse }) {
|
|
468
|
-
const t = useT();
|
|
469
885
|
const rawBundle = useMemo(
|
|
470
886
|
() => ({
|
|
471
887
|
thread: detail.thread,
|
|
@@ -482,40 +898,166 @@ function ThreadDetail({ detail }: { detail: ThreadDebugResponse }) {
|
|
|
482
898
|
}),
|
|
483
899
|
[detail],
|
|
484
900
|
);
|
|
901
|
+
const primaryRun =
|
|
902
|
+
detail.runs.find((run) => run.id === detail.lookup?.runId) ??
|
|
903
|
+
detail.runs[0] ??
|
|
904
|
+
null;
|
|
905
|
+
const eventCount = detail.runs.reduce(
|
|
906
|
+
(total, run) => total + run.events.length,
|
|
907
|
+
0,
|
|
908
|
+
);
|
|
909
|
+
const toolCount = detail.runs.reduce(
|
|
910
|
+
(total, run) => total + summarizeEvents(run.events).toolStarts,
|
|
911
|
+
0,
|
|
912
|
+
);
|
|
485
913
|
|
|
486
914
|
return (
|
|
487
|
-
<div className="
|
|
488
|
-
<div className="border-b px-
|
|
915
|
+
<div className="min-w-0">
|
|
916
|
+
<div className="border-b px-5 py-4">
|
|
489
917
|
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
490
918
|
<div className="min-w-0">
|
|
491
919
|
<div className="truncate text-base font-semibold text-foreground">
|
|
492
920
|
{detail.thread.title || detail.thread.preview || detail.thread.id}
|
|
493
921
|
</div>
|
|
494
|
-
<div className="mt-1
|
|
495
|
-
|
|
922
|
+
<div className="mt-1 flex min-w-0 items-center gap-2 text-xs text-muted-foreground">
|
|
923
|
+
<span className="truncate font-mono">
|
|
924
|
+
{detail.lookup?.runId || detail.thread.id}
|
|
925
|
+
</span>
|
|
926
|
+
{detail.lookup?.runId ? (
|
|
927
|
+
<Button
|
|
928
|
+
type="button"
|
|
929
|
+
variant="ghost"
|
|
930
|
+
size="icon"
|
|
931
|
+
className="size-7 shrink-0"
|
|
932
|
+
title="Copy run ID"
|
|
933
|
+
aria-label="Copy run ID"
|
|
934
|
+
onClick={() =>
|
|
935
|
+
void navigator.clipboard?.writeText(
|
|
936
|
+
detail.lookup?.runId ?? "",
|
|
937
|
+
)
|
|
938
|
+
}
|
|
939
|
+
>
|
|
940
|
+
<IconCopy className="size-3.5" />
|
|
941
|
+
</Button>
|
|
942
|
+
) : null}
|
|
496
943
|
</div>
|
|
497
944
|
</div>
|
|
498
945
|
<div className="flex flex-wrap items-center gap-2">
|
|
499
|
-
|
|
500
|
-
|
|
946
|
+
{primaryRun ? (
|
|
947
|
+
<Badge
|
|
948
|
+
variant={
|
|
949
|
+
primaryRun.status === "errored" ? "destructive" : "secondary"
|
|
950
|
+
}
|
|
951
|
+
>
|
|
952
|
+
{primaryRun.status}
|
|
953
|
+
</Badge>
|
|
954
|
+
) : null}
|
|
501
955
|
<Badge variant="outline">{detail.source.label}</Badge>
|
|
502
956
|
</div>
|
|
503
957
|
</div>
|
|
504
|
-
<div className="mt-3
|
|
505
|
-
<
|
|
506
|
-
<
|
|
507
|
-
<
|
|
958
|
+
<div className="mt-3 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground">
|
|
959
|
+
<span className="truncate">{detail.thread.ownerEmail}</span>
|
|
960
|
+
<span aria-hidden="true">·</span>
|
|
961
|
+
<span>{detail.messages.length} messages</span>
|
|
962
|
+
<span aria-hidden="true">·</span>
|
|
963
|
+
<span>{detail.runs.length} runs</span>
|
|
964
|
+
<span aria-hidden="true">·</span>
|
|
965
|
+
<span>updated {formatDate(detail.thread.updatedAt)}</span>
|
|
508
966
|
</div>
|
|
509
967
|
</div>
|
|
510
968
|
|
|
511
|
-
|
|
512
|
-
<
|
|
969
|
+
{primaryRun ? (
|
|
970
|
+
<DiagnosisPanel
|
|
971
|
+
run={primaryRun}
|
|
972
|
+
eventCount={eventCount}
|
|
973
|
+
toolCount={toolCount}
|
|
974
|
+
/>
|
|
975
|
+
) : null}
|
|
976
|
+
|
|
977
|
+
<Tabs defaultValue="overview" className="p-5">
|
|
978
|
+
<TabsList className="h-9">
|
|
979
|
+
<TabsTrigger value="overview">Overview</TabsTrigger>
|
|
980
|
+
<TabsTrigger value="timeline">Timeline</TabsTrigger>
|
|
513
981
|
<TabsTrigger value="transcript">Transcript</TabsTrigger>
|
|
514
|
-
<TabsTrigger value="
|
|
515
|
-
<TabsTrigger value="internals">Internals</TabsTrigger>
|
|
516
|
-
<TabsTrigger value="raw">Raw</TabsTrigger>
|
|
982
|
+
<TabsTrigger value="technical">Technical</TabsTrigger>
|
|
517
983
|
</TabsList>
|
|
518
984
|
|
|
985
|
+
<TabsContent value="overview" className="mt-4 space-y-4">
|
|
986
|
+
<div className="flex items-start gap-2 border-b pb-4 text-sm text-muted-foreground">
|
|
987
|
+
<IconInfoCircle className="mt-0.5 size-4 shrink-0" />
|
|
988
|
+
<p className="max-w-2xl leading-relaxed">
|
|
989
|
+
This is the compact readout. Open Timeline for the last meaningful
|
|
990
|
+
signals, Transcript for persisted messages and tool calls, or
|
|
991
|
+
Technical for raw records.
|
|
992
|
+
</p>
|
|
993
|
+
</div>
|
|
994
|
+
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
|
995
|
+
<EvidenceStat
|
|
996
|
+
label="Created"
|
|
997
|
+
value={formatDate(detail.thread.createdAt)}
|
|
998
|
+
/>
|
|
999
|
+
<EvidenceStat
|
|
1000
|
+
label="Updated"
|
|
1001
|
+
value={formatDate(detail.thread.updatedAt)}
|
|
1002
|
+
/>
|
|
1003
|
+
<EvidenceStat label="Source" value={detail.source.label} />
|
|
1004
|
+
<EvidenceStat
|
|
1005
|
+
label="Messages"
|
|
1006
|
+
value={detail.messages.length.toLocaleString()}
|
|
1007
|
+
/>
|
|
1008
|
+
<EvidenceStat
|
|
1009
|
+
label="Retained events"
|
|
1010
|
+
value={eventCount.toLocaleString()}
|
|
1011
|
+
/>
|
|
1012
|
+
<EvidenceStat
|
|
1013
|
+
label="Tool starts"
|
|
1014
|
+
value={toolCount.toLocaleString()}
|
|
1015
|
+
/>
|
|
1016
|
+
</div>
|
|
1017
|
+
{detail.messages.length === 0 && eventCount > 0 ? (
|
|
1018
|
+
<div className="flex items-start gap-2 rounded-lg border bg-muted/20 px-3 py-3 text-sm text-muted-foreground">
|
|
1019
|
+
<IconActivity className="mt-0.5 size-4 shrink-0" />
|
|
1020
|
+
<span>
|
|
1021
|
+
No persisted messages are available, but this run retained{" "}
|
|
1022
|
+
{eventCount.toLocaleString()} execution events. The timeline is
|
|
1023
|
+
the authoritative audit trail for this run.
|
|
1024
|
+
</span>
|
|
1025
|
+
</div>
|
|
1026
|
+
) : null}
|
|
1027
|
+
</TabsContent>
|
|
1028
|
+
|
|
1029
|
+
<TabsContent value="timeline" className="mt-4 space-y-5">
|
|
1030
|
+
{detail.runs.length > 0 ? (
|
|
1031
|
+
detail.runs.map((run) => (
|
|
1032
|
+
<section
|
|
1033
|
+
key={run.id}
|
|
1034
|
+
className="border-b pb-5 last:border-b-0 last:pb-0"
|
|
1035
|
+
>
|
|
1036
|
+
<div className="mb-3 flex flex-wrap items-center gap-2">
|
|
1037
|
+
<Badge
|
|
1038
|
+
variant={
|
|
1039
|
+
run.status === "errored" ? "destructive" : "outline"
|
|
1040
|
+
}
|
|
1041
|
+
>
|
|
1042
|
+
{run.status}
|
|
1043
|
+
</Badge>
|
|
1044
|
+
<span className="font-mono text-xs text-muted-foreground">
|
|
1045
|
+
{run.id}
|
|
1046
|
+
</span>
|
|
1047
|
+
<span className="text-xs text-muted-foreground">
|
|
1048
|
+
{formatDuration(run.durationMs)}
|
|
1049
|
+
</span>
|
|
1050
|
+
</div>
|
|
1051
|
+
<RunTimeline run={run} />
|
|
1052
|
+
</section>
|
|
1053
|
+
))
|
|
1054
|
+
) : (
|
|
1055
|
+
<div className="rounded-lg border border-dashed px-4 py-8 text-center text-sm text-muted-foreground">
|
|
1056
|
+
No retained runs.
|
|
1057
|
+
</div>
|
|
1058
|
+
)}
|
|
1059
|
+
</TabsContent>
|
|
1060
|
+
|
|
519
1061
|
<TabsContent value="transcript" className="mt-4 space-y-3">
|
|
520
1062
|
{detail.messages.length > 0 ? (
|
|
521
1063
|
detail.messages.map((message) => (
|
|
@@ -526,177 +1068,162 @@ function ThreadDetail({ detail }: { detail: ThreadDebugResponse }) {
|
|
|
526
1068
|
))
|
|
527
1069
|
) : (
|
|
528
1070
|
<div className="rounded-lg border border-dashed px-4 py-8 text-center text-sm text-muted-foreground">
|
|
529
|
-
No persisted messages
|
|
1071
|
+
<div>No persisted messages.</div>
|
|
1072
|
+
{eventCount > 0 ? (
|
|
1073
|
+
<div className="mt-1">
|
|
1074
|
+
Open Timeline to audit the retained execution events.
|
|
1075
|
+
</div>
|
|
1076
|
+
) : null}
|
|
530
1077
|
</div>
|
|
531
1078
|
)}
|
|
532
1079
|
</TabsContent>
|
|
533
1080
|
|
|
534
|
-
<TabsContent value="
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
<
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
1081
|
+
<TabsContent value="technical" className="mt-4 space-y-3">
|
|
1082
|
+
<details className="rounded-lg border">
|
|
1083
|
+
<summary className="flex cursor-pointer list-none items-center justify-between gap-3 px-3 py-3 text-sm font-medium text-foreground">
|
|
1084
|
+
<span>Run records</span>
|
|
1085
|
+
<span className="text-xs font-normal text-muted-foreground">
|
|
1086
|
+
{detail.runs.length} {detail.runs.length === 1 ? "run" : "runs"}
|
|
1087
|
+
</span>
|
|
1088
|
+
</summary>
|
|
1089
|
+
<div className="space-y-4 border-t p-3">
|
|
1090
|
+
{detail.runs.length > 0 ? (
|
|
1091
|
+
detail.runs.map((run) => (
|
|
1092
|
+
<div key={run.id} className="space-y-3">
|
|
1093
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
1094
|
+
<Badge
|
|
1095
|
+
variant={
|
|
1096
|
+
run.status === "errored" ? "destructive" : "outline"
|
|
1097
|
+
}
|
|
1098
|
+
>
|
|
1099
|
+
{run.status}
|
|
1100
|
+
</Badge>
|
|
1101
|
+
<span className="break-all font-mono text-xs text-foreground">
|
|
1102
|
+
{run.id}
|
|
547
1103
|
</span>
|
|
548
|
-
) : null}
|
|
549
|
-
<span className="text-xs text-muted-foreground">
|
|
550
|
-
{formatDate(run.startedAt)}
|
|
551
|
-
</span>
|
|
552
|
-
</div>
|
|
553
|
-
</summary>
|
|
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
1104
|
</div>
|
|
586
|
-
<div>
|
|
587
|
-
<
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
{
|
|
1105
|
+
<div className="grid gap-3 text-xs sm:grid-cols-2 xl:grid-cols-3">
|
|
1106
|
+
<EvidenceStat
|
|
1107
|
+
label="Failure code"
|
|
1108
|
+
value={run.errorCode || "n/a"}
|
|
1109
|
+
/>
|
|
1110
|
+
<EvidenceStat
|
|
1111
|
+
label="Terminal reason"
|
|
1112
|
+
value={run.terminalReason || run.abortReason || "n/a"}
|
|
1113
|
+
/>
|
|
1114
|
+
<EvidenceStat
|
|
1115
|
+
label="Dispatch mode"
|
|
1116
|
+
value={run.dispatchMode || "foreground"}
|
|
1117
|
+
/>
|
|
1118
|
+
<EvidenceStat
|
|
1119
|
+
label="Last stage"
|
|
1120
|
+
value={
|
|
1121
|
+
diagnosticStage(run.workerStage) ||
|
|
594
1122
|
diagnosticStage(run.diagStage) ||
|
|
595
|
-
"n/a"
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
{
|
|
601
|
-
defaultValue: "Duration",
|
|
602
|
-
})}
|
|
603
|
-
</div>
|
|
604
|
-
<div className="mt-0.5 text-foreground">
|
|
605
|
-
{formatDuration(
|
|
1123
|
+
"n/a"
|
|
1124
|
+
}
|
|
1125
|
+
/>
|
|
1126
|
+
<EvidenceStat
|
|
1127
|
+
label="Duration"
|
|
1128
|
+
value={formatDuration(
|
|
606
1129
|
run.durationMs ??
|
|
607
1130
|
(run.completedAt == null
|
|
608
1131
|
? null
|
|
609
1132
|
: run.completedAt - run.startedAt),
|
|
610
1133
|
)}
|
|
611
|
-
|
|
1134
|
+
/>
|
|
1135
|
+
<EvidenceStat
|
|
1136
|
+
label="Last progress"
|
|
1137
|
+
value={formatDate(
|
|
1138
|
+
run.lastProgressAt ?? run.heartbeatAt,
|
|
1139
|
+
)}
|
|
1140
|
+
/>
|
|
1141
|
+
<EvidenceStat
|
|
1142
|
+
label="In-flight marker"
|
|
1143
|
+
value={formatDate(run.inFlightSince)}
|
|
1144
|
+
/>
|
|
1145
|
+
<EvidenceStat
|
|
1146
|
+
label="Recovery payload"
|
|
1147
|
+
value={
|
|
1148
|
+
run.hasDispatchPayload ? "retained" : "not retained"
|
|
1149
|
+
}
|
|
1150
|
+
/>
|
|
612
1151
|
</div>
|
|
613
|
-
|
|
614
|
-
<div className="text-
|
|
615
|
-
{
|
|
616
|
-
defaultValue: "Last progress",
|
|
617
|
-
})}
|
|
618
|
-
</div>
|
|
619
|
-
<div className="mt-0.5 text-foreground">
|
|
620
|
-
{formatDate(run.lastProgressAt ?? run.heartbeatAt)}
|
|
1152
|
+
{run.errorDetail ? (
|
|
1153
|
+
<div className="rounded-md bg-destructive/10 px-3 py-2 text-xs leading-relaxed text-destructive">
|
|
1154
|
+
{run.errorDetail}
|
|
621
1155
|
</div>
|
|
622
|
-
|
|
1156
|
+
) : null}
|
|
623
1157
|
</div>
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
) : null}
|
|
629
|
-
{run.events.map((event) => (
|
|
630
|
-
<details
|
|
631
|
-
key={`${run.id}-${event.seq}`}
|
|
632
|
-
className="rounded-md border bg-muted/30 px-3 py-2"
|
|
633
|
-
>
|
|
634
|
-
<summary className="cursor-pointer text-xs font-medium text-foreground">
|
|
635
|
-
#{event.seq} {eventLabel(event.event)}
|
|
636
|
-
</summary>
|
|
637
|
-
<RawBlock value={event.event} className="mt-2 max-h-72" />
|
|
638
|
-
</details>
|
|
639
|
-
))}
|
|
640
|
-
{run.events.length === 0 ? (
|
|
641
|
-
<div className="text-sm text-muted-foreground">
|
|
642
|
-
No retained run events.
|
|
643
|
-
</div>
|
|
644
|
-
) : null}
|
|
1158
|
+
))
|
|
1159
|
+
) : (
|
|
1160
|
+
<div className="text-sm text-muted-foreground">
|
|
1161
|
+
No retained run records.
|
|
645
1162
|
</div>
|
|
646
|
-
|
|
647
|
-
))
|
|
648
|
-
) : (
|
|
649
|
-
<div className="rounded-lg border border-dashed px-4 py-8 text-center text-sm text-muted-foreground">
|
|
650
|
-
No retained runs.
|
|
1163
|
+
)}
|
|
651
1164
|
</div>
|
|
652
|
-
|
|
653
|
-
</TabsContent>
|
|
1165
|
+
</details>
|
|
654
1166
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
<
|
|
659
|
-
|
|
1167
|
+
<details className="rounded-lg border">
|
|
1168
|
+
<summary className="flex cursor-pointer list-none items-center justify-between gap-3 px-3 py-3 text-sm font-medium text-foreground">
|
|
1169
|
+
<span>Traces, feedback, and evaluations</span>
|
|
1170
|
+
<span className="text-xs font-normal text-muted-foreground">
|
|
1171
|
+
diagnostic records
|
|
1172
|
+
</span>
|
|
1173
|
+
</summary>
|
|
1174
|
+
<div className="grid gap-4 border-t p-3 lg:grid-cols-2">
|
|
1175
|
+
<div>
|
|
1176
|
+
<div className="mb-2 text-xs font-medium text-foreground">
|
|
1177
|
+
Debug runs
|
|
1178
|
+
</div>
|
|
1179
|
+
<RawBlock
|
|
1180
|
+
value={
|
|
1181
|
+
detail.debugRuns.length > 0
|
|
1182
|
+
? detail.debugRuns
|
|
1183
|
+
: (detail.debug ?? {})
|
|
1184
|
+
}
|
|
1185
|
+
/>
|
|
660
1186
|
</div>
|
|
661
|
-
<
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
}
|
|
667
|
-
/>
|
|
668
|
-
</div>
|
|
669
|
-
<div>
|
|
670
|
-
<div className="mb-2 text-sm font-medium text-foreground">
|
|
671
|
-
Trace Summaries
|
|
1187
|
+
<div>
|
|
1188
|
+
<div className="mb-2 text-xs font-medium text-foreground">
|
|
1189
|
+
Trace summaries
|
|
1190
|
+
</div>
|
|
1191
|
+
<RawBlock value={detail.traces.summaries} />
|
|
672
1192
|
</div>
|
|
673
|
-
<
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
1193
|
+
<div>
|
|
1194
|
+
<div className="mb-2 text-xs font-medium text-foreground">
|
|
1195
|
+
Trace spans
|
|
1196
|
+
</div>
|
|
1197
|
+
<RawBlock value={detail.traces.spans} />
|
|
678
1198
|
</div>
|
|
679
|
-
<
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
1199
|
+
<div>
|
|
1200
|
+
<div className="mb-2 text-xs font-medium text-foreground">
|
|
1201
|
+
Feedback and evals
|
|
1202
|
+
</div>
|
|
1203
|
+
<RawBlock
|
|
1204
|
+
value={{
|
|
1205
|
+
feedback: detail.feedback,
|
|
1206
|
+
satisfaction: detail.satisfaction,
|
|
1207
|
+
evals: detail.evals,
|
|
1208
|
+
checkpoints: detail.checkpoints,
|
|
1209
|
+
}}
|
|
1210
|
+
/>
|
|
684
1211
|
</div>
|
|
685
|
-
<RawBlock
|
|
686
|
-
value={{
|
|
687
|
-
feedback: detail.feedback,
|
|
688
|
-
satisfaction: detail.satisfaction,
|
|
689
|
-
evals: detail.evals,
|
|
690
|
-
checkpoints: detail.checkpoints,
|
|
691
|
-
}}
|
|
692
|
-
/>
|
|
693
1212
|
</div>
|
|
694
|
-
</
|
|
695
|
-
</TabsContent>
|
|
1213
|
+
</details>
|
|
696
1214
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
1215
|
+
<details className="rounded-lg border">
|
|
1216
|
+
<summary className="flex cursor-pointer list-none items-center justify-between gap-3 px-3 py-3 text-sm font-medium text-foreground">
|
|
1217
|
+
<span>Raw thread bundle</span>
|
|
1218
|
+
<span className="text-xs font-normal text-muted-foreground">
|
|
1219
|
+
JSON
|
|
1220
|
+
</span>
|
|
1221
|
+
</summary>
|
|
1222
|
+
<div className="space-y-3 border-t p-3">
|
|
1223
|
+
<RawBlock value={rawBundle} />
|
|
1224
|
+
<RawBlock value={detail.rawThreadData} />
|
|
1225
|
+
</div>
|
|
1226
|
+
</details>
|
|
700
1227
|
</TabsContent>
|
|
701
1228
|
</Tabs>
|
|
702
1229
|
</div>
|
|
@@ -713,11 +1240,13 @@ export default function ThreadDebugRoute() {
|
|
|
713
1240
|
const ownerEmail = routeSearchParams.get("owner") || "";
|
|
714
1241
|
const query = routeSearchParams.get("query") || "";
|
|
715
1242
|
const status = parseFailureStatus(routeSearchParams.get("status"));
|
|
1243
|
+
const regime = parseFailureRegime(routeSearchParams.get("regime"));
|
|
716
1244
|
const range = parseFailureRange(routeSearchParams.get("range"));
|
|
717
1245
|
const runId = routeSearchParams.get("runId") || "";
|
|
718
1246
|
const threadId = routeSearchParams.get("threadId") || "";
|
|
719
1247
|
const inspectSourceId = routeSearchParams.get("inspectSource") || "";
|
|
720
1248
|
const [lookupId, setLookupId] = useState("");
|
|
1249
|
+
const [searchFocused, setSearchFocused] = useState(false);
|
|
721
1250
|
|
|
722
1251
|
function updateRouteState(
|
|
723
1252
|
updates: Record<string, string | null | undefined>,
|
|
@@ -741,7 +1270,7 @@ export default function ThreadDebugRoute() {
|
|
|
741
1270
|
};
|
|
742
1271
|
sources: ThreadDebugSource[];
|
|
743
1272
|
}>("list-agent-thread-sources", {});
|
|
744
|
-
const { data: sourcesData
|
|
1273
|
+
const { data: sourcesData } = sourcesQuery;
|
|
745
1274
|
|
|
746
1275
|
const sources: ThreadDebugSource[] = sourcesData?.sources ?? [];
|
|
747
1276
|
const failureParams = useMemo(
|
|
@@ -749,10 +1278,11 @@ export default function ThreadDebugRoute() {
|
|
|
749
1278
|
sourceId,
|
|
750
1279
|
ownerEmail: ownerEmail.trim() || undefined,
|
|
751
1280
|
status,
|
|
1281
|
+
regime,
|
|
752
1282
|
lookbackHours: FAILURE_RANGE_HOURS[range],
|
|
753
1283
|
limit: 25,
|
|
754
1284
|
}),
|
|
755
|
-
[ownerEmail, range, sourceId, status],
|
|
1285
|
+
[ownerEmail, range, regime, sourceId, status],
|
|
756
1286
|
);
|
|
757
1287
|
const {
|
|
758
1288
|
data: failuresData,
|
|
@@ -764,7 +1294,19 @@ export default function ThreadDebugRoute() {
|
|
|
764
1294
|
failureParams,
|
|
765
1295
|
{ enabled: mode === "failures" },
|
|
766
1296
|
);
|
|
767
|
-
const failures = failuresData?.failures ??
|
|
1297
|
+
const failures = failuresData?.failures ?? EMPTY_FAILURES;
|
|
1298
|
+
const failurePatterns = useMemo(() => {
|
|
1299
|
+
const patterns = new Map<string, { label: string; count: number }>();
|
|
1300
|
+
for (const failure of failures) {
|
|
1301
|
+
const diagnosis = runDiagnosis(failure);
|
|
1302
|
+
const current = patterns.get(diagnosis.code);
|
|
1303
|
+
patterns.set(diagnosis.code, {
|
|
1304
|
+
label: diagnosis.title,
|
|
1305
|
+
count: (current?.count ?? 0) + 1,
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
return [...patterns.values()].sort((a, b) => b.count - a.count);
|
|
1309
|
+
}, [failures]);
|
|
768
1310
|
const unavailableFailureSources = (failuresData?.sources ?? []).filter(
|
|
769
1311
|
(source) => source.status !== "ok",
|
|
770
1312
|
);
|
|
@@ -788,10 +1330,9 @@ export default function ThreadDebugRoute() {
|
|
|
788
1330
|
() => ({
|
|
789
1331
|
sourceId: threadSourceId,
|
|
790
1332
|
query: query.trim() || undefined,
|
|
791
|
-
ownerEmail: ownerEmail.trim() || undefined,
|
|
792
1333
|
limit: 25,
|
|
793
1334
|
}),
|
|
794
|
-
[
|
|
1335
|
+
[query, threadSourceId],
|
|
795
1336
|
);
|
|
796
1337
|
const {
|
|
797
1338
|
data: searchData,
|
|
@@ -805,6 +1346,16 @@ export default function ThreadDebugRoute() {
|
|
|
805
1346
|
source: { id: string; label: string };
|
|
806
1347
|
}>("search-agent-threads", searchParams, { enabled: mode === "threads" });
|
|
807
1348
|
const searchThreads: ThreadSearchResult[] = searchData?.threads ?? [];
|
|
1349
|
+
const ownerEmailSuggestions = useMemo(() => {
|
|
1350
|
+
const emailQuery = query.trim().includes("@")
|
|
1351
|
+
? query.trim().toLowerCase()
|
|
1352
|
+
: "";
|
|
1353
|
+
return [...new Set(searchThreads.map((thread) => thread.ownerEmail))]
|
|
1354
|
+
.filter(
|
|
1355
|
+
(email) => !emailQuery || email.toLowerCase().includes(emailQuery),
|
|
1356
|
+
)
|
|
1357
|
+
.slice(0, 8);
|
|
1358
|
+
}, [query, searchThreads]);
|
|
808
1359
|
|
|
809
1360
|
const detailParams = useMemo(
|
|
810
1361
|
() => ({
|
|
@@ -830,7 +1381,6 @@ export default function ThreadDebugRoute() {
|
|
|
830
1381
|
},
|
|
831
1382
|
);
|
|
832
1383
|
|
|
833
|
-
const selectedSource = sources.find((source) => source.id === threadSourceId);
|
|
834
1384
|
const detailPane = (
|
|
835
1385
|
<section className="min-w-0">
|
|
836
1386
|
{detailError ? (
|
|
@@ -840,7 +1390,7 @@ export default function ThreadDebugRoute() {
|
|
|
840
1390
|
/>
|
|
841
1391
|
) : null}
|
|
842
1392
|
{detailLoading ? (
|
|
843
|
-
<div className="
|
|
1393
|
+
<div className="p-5">
|
|
844
1394
|
<Skeleton className="h-6 w-72" />
|
|
845
1395
|
<Skeleton className="mt-3 h-4 w-96" />
|
|
846
1396
|
<Skeleton className="mt-6 h-[520px] w-full" />
|
|
@@ -848,11 +1398,15 @@ export default function ThreadDebugRoute() {
|
|
|
848
1398
|
) : detail ? (
|
|
849
1399
|
<ThreadDetail detail={detail} />
|
|
850
1400
|
) : (
|
|
851
|
-
<div className="flex min-h-[520px] flex-col items-center justify-center
|
|
1401
|
+
<div className="flex min-h-[520px] flex-col items-center justify-center px-5 text-center text-sm text-muted-foreground">
|
|
852
1402
|
<IconFileSearch className="mb-2 size-5" />
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
1403
|
+
<div className="font-medium text-foreground">
|
|
1404
|
+
Choose a run to inspect
|
|
1405
|
+
</div>
|
|
1406
|
+
<div className="mt-1 max-w-xs leading-relaxed">
|
|
1407
|
+
See the diagnosis first, then open the timeline or technical
|
|
1408
|
+
evidence.
|
|
1409
|
+
</div>
|
|
856
1410
|
</div>
|
|
857
1411
|
)}
|
|
858
1412
|
</section>
|
|
@@ -864,8 +1418,7 @@ export default function ThreadDebugRoute() {
|
|
|
864
1418
|
defaultValue: "Thread Debug",
|
|
865
1419
|
})}
|
|
866
1420
|
description={t("dispatch.pages.threadDebugDescription", {
|
|
867
|
-
defaultValue:
|
|
868
|
-
"Inspect failed agent runs, persisted threads, run events, and AI internals.",
|
|
1421
|
+
defaultValue: "Find the failure pattern, then inspect the evidence.",
|
|
869
1422
|
})}
|
|
870
1423
|
>
|
|
871
1424
|
<div className="space-y-4">
|
|
@@ -905,8 +1458,29 @@ export default function ThreadDebugRoute() {
|
|
|
905
1458
|
</TabsList>
|
|
906
1459
|
|
|
907
1460
|
<TabsContent value="failures" className="mt-4 space-y-4">
|
|
908
|
-
<section className="
|
|
909
|
-
<div className="
|
|
1461
|
+
<section className="border-b pb-4">
|
|
1462
|
+
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
1463
|
+
<div>
|
|
1464
|
+
<h2 className="text-sm font-semibold text-foreground">
|
|
1465
|
+
Run health
|
|
1466
|
+
</h2>
|
|
1467
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
1468
|
+
Start with the dominant failure pattern, then open one run.
|
|
1469
|
+
</p>
|
|
1470
|
+
</div>
|
|
1471
|
+
<Button
|
|
1472
|
+
type="button"
|
|
1473
|
+
variant="ghost"
|
|
1474
|
+
size="icon"
|
|
1475
|
+
onClick={() => void refetchFailures()}
|
|
1476
|
+
aria-label={t("dispatch.pages.threadDebugRefreshFailures", {
|
|
1477
|
+
defaultValue: "Refresh failed runs",
|
|
1478
|
+
})}
|
|
1479
|
+
>
|
|
1480
|
+
<IconRefresh className="size-4" />
|
|
1481
|
+
</Button>
|
|
1482
|
+
</div>
|
|
1483
|
+
<div className="mt-3 grid gap-2 sm:grid-cols-2 xl:grid-cols-[200px_minmax(180px,1fr)_180px_160px_150px]">
|
|
910
1484
|
<Select
|
|
911
1485
|
value={sourceId}
|
|
912
1486
|
onValueChange={(value) =>
|
|
@@ -943,6 +1517,9 @@ export default function ThreadDebugRoute() {
|
|
|
943
1517
|
onChange={(event) =>
|
|
944
1518
|
updateRouteState({ owner: event.target.value })
|
|
945
1519
|
}
|
|
1520
|
+
aria-label={t("dispatch.pages.threadDebugOwner", {
|
|
1521
|
+
defaultValue: "Owner email",
|
|
1522
|
+
})}
|
|
946
1523
|
placeholder={t("dispatch.pages.threadDebugOwner", {
|
|
947
1524
|
defaultValue: "Owner email",
|
|
948
1525
|
})}
|
|
@@ -987,6 +1564,27 @@ export default function ThreadDebugRoute() {
|
|
|
987
1564
|
</SelectItem>
|
|
988
1565
|
</SelectContent>
|
|
989
1566
|
</Select>
|
|
1567
|
+
<Select
|
|
1568
|
+
value={regime}
|
|
1569
|
+
onValueChange={(value) =>
|
|
1570
|
+
updateRouteState({
|
|
1571
|
+
regime: parseFailureRegime(value),
|
|
1572
|
+
runId: null,
|
|
1573
|
+
inspectSource: null,
|
|
1574
|
+
})
|
|
1575
|
+
}
|
|
1576
|
+
>
|
|
1577
|
+
<SelectTrigger aria-label="Run type">
|
|
1578
|
+
<SelectValue placeholder="Run type" />
|
|
1579
|
+
</SelectTrigger>
|
|
1580
|
+
<SelectContent>
|
|
1581
|
+
<SelectItem value="all">All run types</SelectItem>
|
|
1582
|
+
<SelectItem value="interactive">
|
|
1583
|
+
Interactive chats
|
|
1584
|
+
</SelectItem>
|
|
1585
|
+
<SelectItem value="scheduled">Scheduled jobs</SelectItem>
|
|
1586
|
+
</SelectContent>
|
|
1587
|
+
</Select>
|
|
990
1588
|
<Select
|
|
991
1589
|
value={range}
|
|
992
1590
|
onValueChange={(value) =>
|
|
@@ -1024,11 +1622,9 @@ export default function ThreadDebugRoute() {
|
|
|
1024
1622
|
</Select>
|
|
1025
1623
|
</div>
|
|
1026
1624
|
<div className="mt-3 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
|
1027
|
-
<span>
|
|
1028
|
-
{
|
|
1029
|
-
{
|
|
1030
|
-
defaultValue: "failed runs",
|
|
1031
|
-
})}
|
|
1625
|
+
<span className="font-medium text-foreground">
|
|
1626
|
+
{failures.length} loaded{" "}
|
|
1627
|
+
{failures.length === 1 ? "run" : "runs"}
|
|
1032
1628
|
</span>
|
|
1033
1629
|
<span>·</span>
|
|
1034
1630
|
<span>
|
|
@@ -1037,6 +1633,11 @@ export default function ThreadDebugRoute() {
|
|
|
1037
1633
|
defaultValue: "current scope",
|
|
1038
1634
|
})}
|
|
1039
1635
|
</span>
|
|
1636
|
+
{failurePatterns.slice(0, 3).map((pattern) => (
|
|
1637
|
+
<Badge key={pattern.label} variant="outline">
|
|
1638
|
+
{pattern.count} {pattern.label}
|
|
1639
|
+
</Badge>
|
|
1640
|
+
))}
|
|
1040
1641
|
{failuresData?.partial ? (
|
|
1041
1642
|
<Badge variant="outline">
|
|
1042
1643
|
{t("dispatch.pages.threadDebugPartialResults", {
|
|
@@ -1070,68 +1671,67 @@ export default function ThreadDebugRoute() {
|
|
|
1070
1671
|
/>
|
|
1071
1672
|
) : null}
|
|
1072
1673
|
|
|
1073
|
-
<div className="
|
|
1074
|
-
<
|
|
1075
|
-
<
|
|
1076
|
-
<div className="
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
aria-label={t("dispatch.pages.threadDebugRefreshFailures", {
|
|
1087
|
-
defaultValue: "Refresh failed runs",
|
|
1088
|
-
})}
|
|
1089
|
-
>
|
|
1090
|
-
<IconRefresh className="size-4" />
|
|
1091
|
-
</Button>
|
|
1092
|
-
</div>
|
|
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
|
-
})}
|
|
1674
|
+
<div className="overflow-hidden rounded-xl border bg-card">
|
|
1675
|
+
<div className="grid xl:grid-cols-[360px_minmax(0,1fr)]">
|
|
1676
|
+
<section className="min-h-[560px] border-b xl:border-b-0 xl:border-r">
|
|
1677
|
+
<div className="flex items-center justify-between border-b px-4 py-3">
|
|
1678
|
+
<div>
|
|
1679
|
+
<div className="text-sm font-semibold text-foreground">
|
|
1680
|
+
Needs attention
|
|
1681
|
+
</div>
|
|
1682
|
+
<div className="mt-0.5 text-xs text-muted-foreground">
|
|
1683
|
+
{failurePatterns[0]
|
|
1684
|
+
? `${failurePatterns[0].count} ${failurePatterns[0].label.toLowerCase()}`
|
|
1685
|
+
: "No active failure pattern"}
|
|
1686
|
+
</div>
|
|
1107
1687
|
</div>
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1688
|
+
<span className="text-xs text-muted-foreground">
|
|
1689
|
+
{failures.length}
|
|
1690
|
+
</span>
|
|
1691
|
+
</div>
|
|
1692
|
+
<div className="max-h-[760px] overflow-auto">
|
|
1693
|
+
{failuresLoading ? (
|
|
1694
|
+
<>
|
|
1695
|
+
<Skeleton className="mx-4 mt-4 h-16 w-[calc(100%-2rem)]" />
|
|
1696
|
+
<Skeleton className="mx-4 mt-2 h-16 w-[calc(100%-2rem)]" />
|
|
1697
|
+
<Skeleton className="mx-4 mt-2 h-16 w-[calc(100%-2rem)]" />
|
|
1698
|
+
</>
|
|
1699
|
+
) : null}
|
|
1700
|
+
{!failuresLoading && failures.length === 0 ? (
|
|
1701
|
+
<div className="flex min-h-64 flex-col items-center justify-center px-4 text-center text-sm text-muted-foreground">
|
|
1702
|
+
<IconDatabase className="mb-2 size-5" />
|
|
1703
|
+
{t("dispatch.pages.threadDebugNoFailures", {
|
|
1704
|
+
defaultValue: "No failed runs found.",
|
|
1705
|
+
})}
|
|
1706
|
+
</div>
|
|
1707
|
+
) : null}
|
|
1708
|
+
{failures.map((failure) => (
|
|
1709
|
+
<FailureCard
|
|
1710
|
+
key={`${failureSourceId(failure)}:${failure.id}`}
|
|
1711
|
+
failure={failure}
|
|
1712
|
+
selected={
|
|
1713
|
+
runId === failure.id &&
|
|
1714
|
+
detailSourceId === failureSourceId(failure)
|
|
1715
|
+
}
|
|
1716
|
+
onSelect={() =>
|
|
1717
|
+
updateRouteState({
|
|
1718
|
+
inspectSource: failureSourceId(failure),
|
|
1719
|
+
runId: failure.id,
|
|
1720
|
+
threadId: null,
|
|
1721
|
+
})
|
|
1722
|
+
}
|
|
1723
|
+
/>
|
|
1724
|
+
))}
|
|
1725
|
+
</div>
|
|
1726
|
+
</section>
|
|
1727
|
+
<div className="min-w-0">{detailPane}</div>
|
|
1728
|
+
</div>
|
|
1129
1729
|
</div>
|
|
1130
1730
|
</TabsContent>
|
|
1131
1731
|
|
|
1132
1732
|
<TabsContent value="threads" className="mt-4 space-y-4">
|
|
1133
|
-
<section className="
|
|
1134
|
-
<div className="grid gap-3 lg:grid-cols-[
|
|
1733
|
+
<section className="border-b pb-4">
|
|
1734
|
+
<div className="grid gap-2 pt-3 lg:grid-cols-2 xl:grid-cols-[auto_180px_minmax(220px,1fr)_auto_auto]">
|
|
1135
1735
|
<Select
|
|
1136
1736
|
value={threadSourceId}
|
|
1137
1737
|
onValueChange={(value) =>
|
|
@@ -1165,97 +1765,126 @@ export default function ThreadDebugRoute() {
|
|
|
1165
1765
|
) : null}
|
|
1166
1766
|
</SelectContent>
|
|
1167
1767
|
</Select>
|
|
1168
|
-
<
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1768
|
+
<Popover
|
|
1769
|
+
open={searchFocused && ownerEmailSuggestions.length > 0}
|
|
1770
|
+
onOpenChange={setSearchFocused}
|
|
1771
|
+
>
|
|
1772
|
+
<PopoverAnchor asChild>
|
|
1773
|
+
<span className="block">
|
|
1774
|
+
<Input
|
|
1775
|
+
value={query}
|
|
1776
|
+
onChange={(event) =>
|
|
1777
|
+
updateRouteState({ query: event.target.value })
|
|
1778
|
+
}
|
|
1779
|
+
onFocus={() => setSearchFocused(true)}
|
|
1780
|
+
placeholder={t(
|
|
1781
|
+
"dispatch.pages.threadDebugSearchPlaceholder",
|
|
1782
|
+
{
|
|
1783
|
+
defaultValue: "Search threads or email",
|
|
1784
|
+
},
|
|
1785
|
+
)}
|
|
1786
|
+
aria-label="Search threads or email"
|
|
1787
|
+
/>
|
|
1788
|
+
</span>
|
|
1789
|
+
</PopoverAnchor>
|
|
1790
|
+
<PopoverContent
|
|
1791
|
+
align="start"
|
|
1792
|
+
sideOffset={6}
|
|
1793
|
+
className="w-[var(--radix-popover-trigger-width)] p-1"
|
|
1794
|
+
onOpenAutoFocus={(event) => event.preventDefault()}
|
|
1795
|
+
>
|
|
1796
|
+
<div role="listbox" aria-label="Owner email suggestions">
|
|
1797
|
+
{ownerEmailSuggestions.map((email) => (
|
|
1798
|
+
<button
|
|
1799
|
+
key={email}
|
|
1800
|
+
type="button"
|
|
1801
|
+
role="option"
|
|
1802
|
+
aria-selected="false"
|
|
1803
|
+
className="flex w-full items-center rounded-sm px-2 py-1.5 text-left text-sm text-foreground outline-none hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground"
|
|
1804
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
1805
|
+
onClick={() => {
|
|
1806
|
+
updateRouteState({ query: email });
|
|
1807
|
+
setSearchFocused(false);
|
|
1808
|
+
}}
|
|
1809
|
+
>
|
|
1810
|
+
{email}
|
|
1811
|
+
</button>
|
|
1812
|
+
))}
|
|
1813
|
+
</div>
|
|
1814
|
+
</PopoverContent>
|
|
1815
|
+
</Popover>
|
|
1816
|
+
<Button
|
|
1817
|
+
type="button"
|
|
1818
|
+
size="icon"
|
|
1819
|
+
onClick={() => void refetchSearch()}
|
|
1820
|
+
aria-label="Search threads or email"
|
|
1821
|
+
title="Search threads or email"
|
|
1822
|
+
>
|
|
1190
1823
|
<IconSearch className="size-4" />
|
|
1191
|
-
{t("dispatch.pages.threadDebugSearch", {
|
|
1192
|
-
defaultValue: "Search",
|
|
1193
|
-
})}
|
|
1194
1824
|
</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
1825
|
<Button
|
|
1210
1826
|
type="button"
|
|
1211
|
-
variant="
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
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",
|
|
1827
|
+
variant="ghost"
|
|
1828
|
+
size="icon"
|
|
1829
|
+
onClick={() => void refetchSearch()}
|
|
1830
|
+
aria-label={t("dispatch.pages.threadDebugRefreshThreads", {
|
|
1831
|
+
defaultValue: "Refresh threads",
|
|
1233
1832
|
})}
|
|
1833
|
+
>
|
|
1834
|
+
<IconRefresh className="size-4" />
|
|
1234
1835
|
</Button>
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1836
|
+
<details className="order-first justify-self-start rounded-lg border">
|
|
1837
|
+
<summary
|
|
1838
|
+
aria-label="Advanced lookup"
|
|
1839
|
+
title="Advanced lookup"
|
|
1840
|
+
className="flex size-10 cursor-pointer list-none items-center justify-center text-foreground"
|
|
1841
|
+
>
|
|
1842
|
+
<IconAdjustmentsHorizontal
|
|
1843
|
+
aria-hidden="true"
|
|
1844
|
+
className="size-4"
|
|
1845
|
+
/>
|
|
1846
|
+
<span className="sr-only">Advanced lookup</span>
|
|
1847
|
+
</summary>
|
|
1848
|
+
<div className="grid gap-3 border-t p-3 lg:w-80">
|
|
1849
|
+
<Input
|
|
1850
|
+
value={lookupId}
|
|
1851
|
+
onChange={(event) => setLookupId(event.target.value)}
|
|
1852
|
+
placeholder={t(
|
|
1853
|
+
"dispatch.pages.threadDebugLookupPlaceholder",
|
|
1854
|
+
{
|
|
1855
|
+
defaultValue: "Paste thread or request/run ID",
|
|
1856
|
+
},
|
|
1857
|
+
)}
|
|
1858
|
+
className="font-mono"
|
|
1859
|
+
/>
|
|
1860
|
+
<Button
|
|
1861
|
+
type="button"
|
|
1862
|
+
variant="outline"
|
|
1863
|
+
onClick={() => {
|
|
1864
|
+
const trimmed = lookupId.trim();
|
|
1865
|
+
if (!trimmed) return;
|
|
1866
|
+
updateRouteState(
|
|
1867
|
+
trimmed.startsWith("run-")
|
|
1868
|
+
? {
|
|
1869
|
+
runId: trimmed,
|
|
1870
|
+
threadId: null,
|
|
1871
|
+
inspectSource: null,
|
|
1872
|
+
}
|
|
1873
|
+
: {
|
|
1874
|
+
threadId: trimmed,
|
|
1875
|
+
runId: null,
|
|
1876
|
+
inspectSource: null,
|
|
1877
|
+
},
|
|
1878
|
+
);
|
|
1879
|
+
}}
|
|
1880
|
+
>
|
|
1881
|
+
<IconFileSearch className="size-4" />
|
|
1882
|
+
{t("dispatch.pages.threadDebugInspect", {
|
|
1883
|
+
defaultValue: "Inspect",
|
|
1884
|
+
})}
|
|
1885
|
+
</Button>
|
|
1886
|
+
</div>
|
|
1887
|
+
</details>
|
|
1259
1888
|
</div>
|
|
1260
1889
|
</section>
|
|
1261
1890
|
|
|
@@ -1266,72 +1895,43 @@ export default function ThreadDebugRoute() {
|
|
|
1266
1895
|
/>
|
|
1267
1896
|
) : null}
|
|
1268
1897
|
|
|
1269
|
-
<div className="
|
|
1270
|
-
<
|
|
1271
|
-
<
|
|
1272
|
-
<div>
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
t("dispatch.pages.threadDebugCurrentScope", {
|
|
1286
|
-
defaultValue: "current scope",
|
|
1898
|
+
<div className="overflow-hidden rounded-xl border bg-card">
|
|
1899
|
+
<div className="grid xl:grid-cols-[360px_minmax(0,1fr)]">
|
|
1900
|
+
<section className="min-h-[560px] border-b xl:border-b-0 xl:border-r">
|
|
1901
|
+
<div className="max-h-[760px] overflow-auto">
|
|
1902
|
+
{searchLoading ? (
|
|
1903
|
+
<>
|
|
1904
|
+
<Skeleton className="mx-4 mt-4 h-16 w-[calc(100%-2rem)]" />
|
|
1905
|
+
<Skeleton className="mx-4 mt-2 h-16 w-[calc(100%-2rem)]" />
|
|
1906
|
+
<Skeleton className="mx-4 mt-2 h-16 w-[calc(100%-2rem)]" />
|
|
1907
|
+
</>
|
|
1908
|
+
) : null}
|
|
1909
|
+
{!searchLoading && searchThreads.length === 0 ? (
|
|
1910
|
+
<div className="flex min-h-64 flex-col items-center justify-center px-4 text-center text-sm text-muted-foreground">
|
|
1911
|
+
<IconDatabase className="mb-2 size-5" />
|
|
1912
|
+
{t("dispatch.pages.threadDebugNoThreads", {
|
|
1913
|
+
defaultValue: "No threads found.",
|
|
1287
1914
|
})}
|
|
1288
|
-
|
|
1915
|
+
</div>
|
|
1916
|
+
) : null}
|
|
1917
|
+
{searchThreads.map((result) => (
|
|
1918
|
+
<ResultCard
|
|
1919
|
+
key={result.id}
|
|
1920
|
+
result={result}
|
|
1921
|
+
selected={threadId === result.id}
|
|
1922
|
+
onSelect={() =>
|
|
1923
|
+
updateRouteState({
|
|
1924
|
+
threadId: result.id,
|
|
1925
|
+
runId: null,
|
|
1926
|
+
inspectSource: null,
|
|
1927
|
+
})
|
|
1928
|
+
}
|
|
1929
|
+
/>
|
|
1930
|
+
))}
|
|
1289
1931
|
</div>
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
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}
|
|
1932
|
+
</section>
|
|
1933
|
+
<div className="min-w-0">{detailPane}</div>
|
|
1934
|
+
</div>
|
|
1335
1935
|
</div>
|
|
1336
1936
|
</TabsContent>
|
|
1337
1937
|
</Tabs>
|