@tonyclaw/agent-inspector 2.0.43 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-CxM1gCfL.js → CompareDrawer-VDpcSNGM.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-fsXSjqtm.js +117 -0
- package/.output/public/assets/{ReplayDialog-UseUkucS.js → ReplayDialog-CoDgTzHR.js} +1 -1
- package/.output/public/assets/RequestAnatomy-t3plgkPE.js +1 -0
- package/.output/public/assets/ResponseView-BYuKiNeG.js +1 -0
- package/.output/public/assets/{StreamingChunkSequence-JSQEPeNS.js → StreamingChunkSequence-DOdU38Is.js} +1 -1
- package/.output/public/assets/_sessionId-ew5QlyZ2.js +1 -0
- package/.output/public/assets/index-DFICWD6o.js +1 -0
- package/.output/public/assets/index-zLLCkAnF.css +1 -0
- package/.output/public/assets/{main-BpGVJcpB.js → main-DmzLt6ti.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +219 -178
- package/.output/server/{_sessionId-DGn-TENM.mjs → _sessionId-CCN1UoEf.mjs} +3 -3
- package/.output/server/_ssr/{CompareDrawer-CFElCSYY.mjs → CompareDrawer-DkSY3vxP.mjs} +4 -4
- package/.output/server/_ssr/{ProxyViewerContainer-B7SR9mrD.mjs → ProxyViewerContainer-OTh1V_Kh.mjs} +798 -394
- package/.output/server/_ssr/{ReplayDialog-DfKapj0k.mjs → ReplayDialog-DhI6yxrR.mjs} +5 -5
- package/.output/server/_ssr/{RequestAnatomy-CUxTCg31.mjs → RequestAnatomy-DN1SJVVL.mjs} +4 -4
- package/.output/server/_ssr/{ResponseView-BXY0w197.mjs → ResponseView-DW-kpJL5.mjs} +4 -4
- package/.output/server/_ssr/{StreamingChunkSequence-CzMnES9H.mjs → StreamingChunkSequence-BAE57Fae.mjs} +4 -4
- package/.output/server/_ssr/{index-CVlT-REW.mjs → index-C9ryJEna.mjs} +3 -3
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-B2d1LUx6.mjs → router-DIIJGyML.mjs} +458 -10
- package/.output/server/{_tanstack-start-manifest_v-Drpi28BM.mjs → _tanstack-start-manifest_v-C_v9-E5d.mjs} +1 -1
- package/.output/server/index.mjs +63 -63
- package/package.json +1 -1
- package/src/components/OnboardingBanner.tsx +74 -68
- package/src/components/ProxyViewer.tsx +869 -363
- package/src/components/ProxyViewerContainer.tsx +135 -3
- package/src/components/proxy-viewer/LogEntryHeader.tsx +12 -6
- package/src/components/ui/crab-logo.tsx +0 -50
- package/src/proxy/logIndex.ts +188 -1
- package/src/proxy/store.ts +405 -3
- package/src/routes/api/logs.ts +36 -0
- package/.output/public/assets/ProxyViewerContainer-TtRG-0E7.js +0 -117
- package/.output/public/assets/RequestAnatomy-aPxgEJ2L.js +0 -1
- package/.output/public/assets/ResponseView-DA-F4F97.js +0 -1
- package/.output/public/assets/_sessionId-BEXuCWq5.js +0 -1
- package/.output/public/assets/index-Bhsa_2xX.js +0 -1
- package/.output/public/assets/index-DOWlRJ0W.css +0 -1
|
@@ -3,11 +3,20 @@ import {
|
|
|
3
3
|
ArrowDownRight,
|
|
4
4
|
ArrowLeft,
|
|
5
5
|
ArrowUpRight,
|
|
6
|
+
Clapperboard,
|
|
6
7
|
Check,
|
|
8
|
+
ChevronDown,
|
|
9
|
+
ChevronLeft,
|
|
10
|
+
ChevronRight,
|
|
11
|
+
ChevronUp,
|
|
12
|
+
ChevronsLeft,
|
|
13
|
+
ChevronsRight,
|
|
7
14
|
Copy,
|
|
8
15
|
Download,
|
|
9
16
|
FileJson,
|
|
17
|
+
Gauge,
|
|
10
18
|
Plus,
|
|
19
|
+
Siren,
|
|
11
20
|
Trash2,
|
|
12
21
|
} from "lucide-react";
|
|
13
22
|
|
|
@@ -98,6 +107,20 @@ export type SessionMembershipEvidence = {
|
|
|
98
107
|
member: InspectorGroupMember;
|
|
99
108
|
};
|
|
100
109
|
|
|
110
|
+
export type LogPaginationControls = {
|
|
111
|
+
isLoading: boolean;
|
|
112
|
+
total: number | null;
|
|
113
|
+
pageSize: number;
|
|
114
|
+
hasOlder: boolean;
|
|
115
|
+
hasNewer: boolean;
|
|
116
|
+
oldestLogId: number | null;
|
|
117
|
+
newestLogId: number | null;
|
|
118
|
+
onOldest: () => void;
|
|
119
|
+
onOlder: () => void;
|
|
120
|
+
onNewer: () => void;
|
|
121
|
+
onNewest: () => void;
|
|
122
|
+
};
|
|
123
|
+
|
|
101
124
|
function formatTimeRange(logs: CapturedLog[], timeDisplayFormat: TimeDisplayFormat): string | null {
|
|
102
125
|
const first = logs[0];
|
|
103
126
|
const last = logs[logs.length - 1];
|
|
@@ -114,6 +137,167 @@ function getFirstUserAgent(logs: CapturedLog[]): string | null {
|
|
|
114
137
|
return null;
|
|
115
138
|
}
|
|
116
139
|
|
|
140
|
+
type SessionSlateStatus = "rolling" | "clear" | "watch" | "failed";
|
|
141
|
+
|
|
142
|
+
type SessionSlateStats = {
|
|
143
|
+
status: SessionSlateStatus;
|
|
144
|
+
statusLabel: string;
|
|
145
|
+
modelLabel: string;
|
|
146
|
+
requests: number;
|
|
147
|
+
failures: number;
|
|
148
|
+
slow: number;
|
|
149
|
+
timeRange: string | null;
|
|
150
|
+
tokenLabel: string;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
function hasLogFailure(log: CapturedLog): boolean {
|
|
154
|
+
if (log.error !== null && log.error !== undefined && log.error.length > 0) return true;
|
|
155
|
+
return log.responseStatus !== null && log.responseStatus >= 400;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function getModelLabel(logs: readonly CapturedLog[]): string {
|
|
159
|
+
const models = new Set<string>();
|
|
160
|
+
for (const log of logs) {
|
|
161
|
+
if (log.model !== null && log.model.length > 0) {
|
|
162
|
+
models.add(log.model);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const sorted = [...models].sort();
|
|
166
|
+
if (sorted.length === 0) return "Model unknown";
|
|
167
|
+
if (sorted.length === 1) return sorted[0] ?? "Model unknown";
|
|
168
|
+
return `${sorted.length} models`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function buildSessionSlateStats({
|
|
172
|
+
logs,
|
|
173
|
+
isLoading,
|
|
174
|
+
timeDisplayFormat,
|
|
175
|
+
slowResponseThresholdSeconds,
|
|
176
|
+
}: {
|
|
177
|
+
logs: CapturedLog[];
|
|
178
|
+
isLoading: boolean;
|
|
179
|
+
timeDisplayFormat: TimeDisplayFormat;
|
|
180
|
+
slowResponseThresholdSeconds: number;
|
|
181
|
+
}): SessionSlateStats {
|
|
182
|
+
let failures = 0;
|
|
183
|
+
let slow = 0;
|
|
184
|
+
let totalIn = 0;
|
|
185
|
+
let totalOut = 0;
|
|
186
|
+
const slowThresholdMs = slowResponseThresholdSeconds * 1000;
|
|
187
|
+
|
|
188
|
+
for (const log of logs) {
|
|
189
|
+
if (hasLogFailure(log)) failures += 1;
|
|
190
|
+
if (
|
|
191
|
+
slowResponseThresholdSeconds > 0 &&
|
|
192
|
+
log.elapsedMs !== null &&
|
|
193
|
+
log.elapsedMs >= slowThresholdMs
|
|
194
|
+
) {
|
|
195
|
+
slow += 1;
|
|
196
|
+
}
|
|
197
|
+
if (log.inputTokens !== null) totalIn += log.inputTokens;
|
|
198
|
+
if (log.outputTokens !== null) totalOut += log.outputTokens;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const status: SessionSlateStatus = isLoading
|
|
202
|
+
? "rolling"
|
|
203
|
+
: failures > 0
|
|
204
|
+
? "failed"
|
|
205
|
+
: slow > 0
|
|
206
|
+
? "watch"
|
|
207
|
+
: "clear";
|
|
208
|
+
const statusLabel =
|
|
209
|
+
status === "rolling"
|
|
210
|
+
? "Rolling"
|
|
211
|
+
: status === "failed"
|
|
212
|
+
? "Failure"
|
|
213
|
+
: status === "watch"
|
|
214
|
+
? "Watch"
|
|
215
|
+
: "Clear";
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
status,
|
|
219
|
+
statusLabel,
|
|
220
|
+
modelLabel: getModelLabel(logs),
|
|
221
|
+
requests: logs.length,
|
|
222
|
+
failures,
|
|
223
|
+
slow,
|
|
224
|
+
timeRange: formatTimeRange(logs, timeDisplayFormat),
|
|
225
|
+
tokenLabel: `${formatTokens(totalIn)} in / ${formatTokens(totalOut)} out`,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function slateStatusClass(status: SessionSlateStatus): string {
|
|
230
|
+
switch (status) {
|
|
231
|
+
case "rolling":
|
|
232
|
+
return "border-cyan-400/35 bg-cyan-500/10 text-cyan-200";
|
|
233
|
+
case "failed":
|
|
234
|
+
return "border-red-400/35 bg-red-500/10 text-red-100";
|
|
235
|
+
case "watch":
|
|
236
|
+
return "border-amber-300/35 bg-amber-400/10 text-amber-100";
|
|
237
|
+
case "clear":
|
|
238
|
+
return "border-emerald-400/30 bg-emerald-500/10 text-emerald-100";
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function SessionOpeningSlate({ stats }: { stats: SessionSlateStats }): JSX.Element | null {
|
|
243
|
+
if (stats.requests === 0) return null;
|
|
244
|
+
|
|
245
|
+
return (
|
|
246
|
+
<div className="mb-4 rounded-md border border-border bg-[#0c0d0f] shadow-[0_14px_50px_rgba(0,0,0,0.25)]">
|
|
247
|
+
<div className="grid grid-cols-2 gap-0 md:grid-cols-[minmax(220px,1.2fr)_repeat(4,minmax(120px,1fr))]">
|
|
248
|
+
<div className="col-span-2 border-b border-border/70 px-4 py-3 md:col-span-1 md:border-b-0 md:border-r">
|
|
249
|
+
<div className="flex items-center gap-2 text-[10px] font-semibold uppercase text-muted-foreground">
|
|
250
|
+
<Clapperboard className="size-3.5 text-cyan-300" />
|
|
251
|
+
Opening Slate
|
|
252
|
+
</div>
|
|
253
|
+
<div className="mt-2 min-w-0 truncate font-mono text-sm font-semibold text-foreground">
|
|
254
|
+
{stats.modelLabel}
|
|
255
|
+
</div>
|
|
256
|
+
<div className="mt-1 truncate font-mono text-[11px] text-muted-foreground">
|
|
257
|
+
{stats.timeRange ?? "Timeline pending"}
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
<div className="border-r border-b border-border/70 px-4 py-3 md:border-b-0">
|
|
261
|
+
<div className="text-[10px] uppercase text-muted-foreground">Status</div>
|
|
262
|
+
<div
|
|
263
|
+
className={cn(
|
|
264
|
+
"mt-2 inline-flex h-7 items-center rounded-md border px-2.5 font-mono text-xs font-semibold",
|
|
265
|
+
slateStatusClass(stats.status),
|
|
266
|
+
)}
|
|
267
|
+
>
|
|
268
|
+
{stats.statusLabel}
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
<div className="border-b border-border/70 px-4 py-3 md:border-b-0 md:border-r">
|
|
272
|
+
<div className="text-[10px] uppercase text-muted-foreground">Requests</div>
|
|
273
|
+
<div className="mt-2 font-mono text-xl font-semibold text-foreground">
|
|
274
|
+
{stats.requests}
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
<div className="border-r border-border/70 px-4 py-3 md:border-r">
|
|
278
|
+
<div className="flex items-center gap-1 text-[10px] uppercase text-muted-foreground">
|
|
279
|
+
<Siren className="size-3 text-red-300" />
|
|
280
|
+
Failure
|
|
281
|
+
</div>
|
|
282
|
+
<div className="mt-2 font-mono text-xl font-semibold text-red-100">{stats.failures}</div>
|
|
283
|
+
</div>
|
|
284
|
+
<div className="px-4 py-3">
|
|
285
|
+
<div className="flex items-center gap-1 text-[10px] uppercase text-muted-foreground">
|
|
286
|
+
<Gauge className="size-3 text-amber-200" />
|
|
287
|
+
Tempo
|
|
288
|
+
</div>
|
|
289
|
+
<div className="mt-2 font-mono text-sm font-semibold text-foreground">
|
|
290
|
+
{stats.slow} slow
|
|
291
|
+
</div>
|
|
292
|
+
<div className="mt-1 truncate font-mono text-[11px] text-muted-foreground">
|
|
293
|
+
{stats.tokenLabel}
|
|
294
|
+
</div>
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
117
301
|
export function resolveSessionContextScope({
|
|
118
302
|
pinnedSessionId,
|
|
119
303
|
selectedSession,
|
|
@@ -227,6 +411,39 @@ function formatMetadataValue(value: InspectorGroupMember["metadata"][string]): s
|
|
|
227
411
|
return JSON.stringify(value);
|
|
228
412
|
}
|
|
229
413
|
|
|
414
|
+
function formatMemberMetadata(member: InspectorGroupMember | null, key: string): string | null {
|
|
415
|
+
if (member === null) return null;
|
|
416
|
+
const value = member.metadata[key];
|
|
417
|
+
if (value === undefined) return null;
|
|
418
|
+
return formatMetadataValue(value);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function evidenceChipClass(value: string): string {
|
|
422
|
+
const normalized = value.toLowerCase();
|
|
423
|
+
if (
|
|
424
|
+
normalized.includes("reject") ||
|
|
425
|
+
normalized.includes("fail") ||
|
|
426
|
+
normalized.includes("error")
|
|
427
|
+
) {
|
|
428
|
+
return "border-red-400/35 bg-red-500/10 text-red-100";
|
|
429
|
+
}
|
|
430
|
+
if (
|
|
431
|
+
normalized.includes("partial") ||
|
|
432
|
+
normalized.includes("pending") ||
|
|
433
|
+
normalized.includes("slow")
|
|
434
|
+
) {
|
|
435
|
+
return "border-amber-300/35 bg-amber-400/10 text-amber-100";
|
|
436
|
+
}
|
|
437
|
+
if (
|
|
438
|
+
normalized.includes("pass") ||
|
|
439
|
+
normalized.includes("complete") ||
|
|
440
|
+
normalized.includes("accept")
|
|
441
|
+
) {
|
|
442
|
+
return "border-emerald-400/30 bg-emerald-500/10 text-emerald-100";
|
|
443
|
+
}
|
|
444
|
+
return "border-border/70 bg-background/70 text-muted-foreground";
|
|
445
|
+
}
|
|
446
|
+
|
|
230
447
|
function getMemberMetadataRows(member: InspectorGroupMember): { key: string; value: string }[] {
|
|
231
448
|
const rows: { key: string; value: string }[] = [];
|
|
232
449
|
for (const key of MEMBER_METADATA_KEYS) {
|
|
@@ -240,75 +457,234 @@ function getMemberMetadataRows(member: InspectorGroupMember): { key: string; val
|
|
|
240
457
|
|
|
241
458
|
function SessionMembershipPanel({
|
|
242
459
|
memberships,
|
|
460
|
+
defaultOpen = false,
|
|
243
461
|
}: {
|
|
244
462
|
memberships: SessionMembershipEvidence[];
|
|
463
|
+
defaultOpen?: boolean;
|
|
245
464
|
}): JSX.Element | null {
|
|
465
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
246
466
|
if (memberships.length === 0) return null;
|
|
467
|
+
const primary = memberships[0];
|
|
468
|
+
const primaryMember = primary?.member ?? null;
|
|
469
|
+
const score = formatMemberMetadata(primaryMember, "score");
|
|
470
|
+
const decision = formatMemberMetadata(primaryMember, "decision");
|
|
471
|
+
const finalStatus = formatMemberMetadata(primaryMember, "final_status");
|
|
472
|
+
const summaryChips = [
|
|
473
|
+
score === null ? null : { label: `score ${score}`, value: score },
|
|
474
|
+
decision === null ? null : { label: decision, value: decision },
|
|
475
|
+
finalStatus === null ? null : { label: finalStatus, value: finalStatus },
|
|
476
|
+
primaryMember?.model === null || primaryMember?.model === undefined
|
|
477
|
+
? null
|
|
478
|
+
: { label: primaryMember.model, value: primaryMember.model },
|
|
479
|
+
].filter(
|
|
480
|
+
(value): value is { label: string; value: string } =>
|
|
481
|
+
value !== null && value.label.length > 0 && value.value.length > 0,
|
|
482
|
+
);
|
|
247
483
|
|
|
248
484
|
return (
|
|
249
|
-
<div className="mb-
|
|
250
|
-
<
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
485
|
+
<div className="mb-3 overflow-hidden rounded-md border border-border bg-[#0c0d0f] text-xs">
|
|
486
|
+
<button
|
|
487
|
+
type="button"
|
|
488
|
+
onClick={() => setOpen((value) => !value)}
|
|
489
|
+
className="flex w-full items-center gap-3 px-3 py-2 text-left transition-colors hover:bg-muted/30"
|
|
490
|
+
aria-expanded={open}
|
|
491
|
+
>
|
|
492
|
+
<div className="min-w-0 flex-1">
|
|
493
|
+
<div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
|
|
494
|
+
<span className="font-semibold text-foreground">Case File</span>
|
|
495
|
+
{primary !== undefined && (
|
|
496
|
+
<span className="truncate text-muted-foreground" title={primary.groupTitle}>
|
|
497
|
+
{primary.groupTitle}
|
|
498
|
+
</span>
|
|
499
|
+
)}
|
|
500
|
+
<span className="font-mono text-[10px] uppercase text-muted-foreground">
|
|
501
|
+
{memberships.length} group{memberships.length === 1 ? "" : "s"}
|
|
502
|
+
</span>
|
|
503
|
+
</div>
|
|
504
|
+
{summaryChips.length > 0 && (
|
|
505
|
+
<div className="mt-1 flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
|
|
506
|
+
{summaryChips.slice(0, 4).map((chip) => (
|
|
263
507
|
<span
|
|
264
|
-
|
|
265
|
-
|
|
508
|
+
key={chip.label}
|
|
509
|
+
className={cn(
|
|
510
|
+
"rounded border px-1.5 py-0.5 font-mono text-[10px]",
|
|
511
|
+
evidenceChipClass(chip.value),
|
|
512
|
+
)}
|
|
266
513
|
>
|
|
267
|
-
{
|
|
514
|
+
{chip.label}
|
|
268
515
|
</span>
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
516
|
+
))}
|
|
517
|
+
</div>
|
|
518
|
+
)}
|
|
519
|
+
</div>
|
|
520
|
+
{open ? (
|
|
521
|
+
<ChevronUp className="size-4 shrink-0 text-muted-foreground" />
|
|
522
|
+
) : (
|
|
523
|
+
<ChevronDown className="size-4 shrink-0 text-muted-foreground" />
|
|
524
|
+
)}
|
|
525
|
+
</button>
|
|
526
|
+
{open && (
|
|
527
|
+
<div className="space-y-2 border-t border-border/70 px-3 py-2">
|
|
528
|
+
{memberships.map((membership) => {
|
|
529
|
+
const member = membership.member;
|
|
530
|
+
const metadataRows = getMemberMetadataRows(member);
|
|
531
|
+
return (
|
|
532
|
+
<div key={`${membership.groupId}:${member.id}`} className="min-w-0">
|
|
533
|
+
<div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
|
|
534
|
+
<span
|
|
535
|
+
className="truncate font-medium text-foreground"
|
|
536
|
+
title={membership.groupTitle}
|
|
537
|
+
>
|
|
538
|
+
{membership.groupTitle}
|
|
275
539
|
</span>
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
<div className="mt-1 flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1 text-muted-foreground">
|
|
279
|
-
<span className="font-mono" title={member.sessionId}>
|
|
280
|
-
{truncateSessionId(member.sessionId)}
|
|
281
|
-
</span>
|
|
282
|
-
{member.label !== null && <span>{member.label}</span>}
|
|
283
|
-
{member.provider !== null && <span>{member.provider}</span>}
|
|
284
|
-
{member.model !== null && <span className="font-mono">{member.model}</span>}
|
|
285
|
-
{member.runId !== null && (
|
|
286
|
-
<span className="font-mono" title={member.runId}>
|
|
287
|
-
run {truncateSessionId(member.runId)}
|
|
540
|
+
<span className="font-mono text-[10px] uppercase text-muted-foreground">
|
|
541
|
+
{membership.groupStatus}
|
|
288
542
|
</span>
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
543
|
+
{member.status !== null && (
|
|
544
|
+
<span className="font-mono text-[10px] text-muted-foreground">
|
|
545
|
+
member {member.status}
|
|
546
|
+
</span>
|
|
547
|
+
)}
|
|
548
|
+
</div>
|
|
549
|
+
<div className="mt-1 flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1 text-muted-foreground">
|
|
550
|
+
<span className="font-mono" title={member.sessionId}>
|
|
551
|
+
{truncateSessionId(member.sessionId)}
|
|
552
|
+
</span>
|
|
553
|
+
{member.label !== null && <span>{member.label}</span>}
|
|
554
|
+
{member.provider !== null && <span>{member.provider}</span>}
|
|
555
|
+
{member.model !== null && <span className="font-mono">{member.model}</span>}
|
|
556
|
+
{member.runId !== null && (
|
|
557
|
+
<span className="font-mono" title={member.runId}>
|
|
558
|
+
run {truncateSessionId(member.runId)}
|
|
559
|
+
</span>
|
|
560
|
+
)}
|
|
561
|
+
</div>
|
|
562
|
+
{metadataRows.length > 0 && (
|
|
563
|
+
<div className="mt-2 grid gap-1 sm:grid-cols-2 xl:grid-cols-3">
|
|
564
|
+
{metadataRows.map((row) => (
|
|
299
565
|
<div
|
|
300
|
-
|
|
301
|
-
|
|
566
|
+
key={row.key}
|
|
567
|
+
className="min-w-0 rounded border border-border/60 px-2 py-1"
|
|
302
568
|
>
|
|
303
|
-
{row.
|
|
569
|
+
<div className="font-mono text-[10px] text-muted-foreground">{row.key}</div>
|
|
570
|
+
<div
|
|
571
|
+
className="truncate font-mono text-[11px] text-foreground"
|
|
572
|
+
title={row.value}
|
|
573
|
+
>
|
|
574
|
+
{row.value}
|
|
575
|
+
</div>
|
|
304
576
|
</div>
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
)
|
|
311
|
-
|
|
577
|
+
))}
|
|
578
|
+
</div>
|
|
579
|
+
)}
|
|
580
|
+
</div>
|
|
581
|
+
);
|
|
582
|
+
})}
|
|
583
|
+
</div>
|
|
584
|
+
)}
|
|
585
|
+
</div>
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
export function formatPaginationStatus({
|
|
590
|
+
logs,
|
|
591
|
+
pagination,
|
|
592
|
+
}: {
|
|
593
|
+
logs: readonly CapturedLog[];
|
|
594
|
+
pagination: LogPaginationControls;
|
|
595
|
+
}): string {
|
|
596
|
+
if (pagination.isLoading && logs.length === 0) return "Loading logs...";
|
|
597
|
+
if (logs.length === 0) return "No logs on this page";
|
|
598
|
+
|
|
599
|
+
const first = logs[0];
|
|
600
|
+
const last = logs[logs.length - 1];
|
|
601
|
+
const range =
|
|
602
|
+
first === undefined || last === undefined ? "Current page" : `#${first.id}-#${last.id}`;
|
|
603
|
+
const total = pagination.total === null ? "logs" : `${pagination.total} logs`;
|
|
604
|
+
return `Showing ${range} of ${total}`;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function LogPaginationButton({
|
|
608
|
+
label,
|
|
609
|
+
title,
|
|
610
|
+
icon,
|
|
611
|
+
disabled,
|
|
612
|
+
onClick,
|
|
613
|
+
}: {
|
|
614
|
+
label: string;
|
|
615
|
+
title: string;
|
|
616
|
+
icon: JSX.Element;
|
|
617
|
+
disabled: boolean;
|
|
618
|
+
onClick: () => void;
|
|
619
|
+
}): JSX.Element {
|
|
620
|
+
return (
|
|
621
|
+
<button
|
|
622
|
+
type="button"
|
|
623
|
+
onClick={onClick}
|
|
624
|
+
disabled={disabled}
|
|
625
|
+
className="inline-flex h-8 items-center gap-1.5 rounded-md px-2.5 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:cursor-not-allowed disabled:opacity-45"
|
|
626
|
+
title={title}
|
|
627
|
+
>
|
|
628
|
+
{icon}
|
|
629
|
+
<span>{label}</span>
|
|
630
|
+
</button>
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function LogPaginationBar({
|
|
635
|
+
logs,
|
|
636
|
+
pagination,
|
|
637
|
+
embedded = false,
|
|
638
|
+
}: {
|
|
639
|
+
logs: readonly CapturedLog[];
|
|
640
|
+
pagination: LogPaginationControls | undefined;
|
|
641
|
+
embedded?: boolean;
|
|
642
|
+
}): JSX.Element | null {
|
|
643
|
+
if (pagination === undefined) return null;
|
|
644
|
+
const disabled = pagination.isLoading;
|
|
645
|
+
|
|
646
|
+
return (
|
|
647
|
+
<div
|
|
648
|
+
className={cn(
|
|
649
|
+
"flex min-h-9 flex-col gap-2 px-1 py-1 sm:flex-row sm:items-center",
|
|
650
|
+
embedded
|
|
651
|
+
? "border-t border-border/70 pt-2"
|
|
652
|
+
: "mb-3 rounded-md border border-border bg-muted/10 px-2",
|
|
653
|
+
)}
|
|
654
|
+
>
|
|
655
|
+
<div className="w-full min-w-0 px-1 font-mono text-[11px] text-muted-foreground sm:flex-1">
|
|
656
|
+
{formatPaginationStatus({ logs, pagination })}
|
|
657
|
+
{pagination.isLoading && logs.length > 0 && <span className="ml-2">Loading...</span>}
|
|
658
|
+
</div>
|
|
659
|
+
<div className="flex w-full items-center gap-1 overflow-x-auto sm:w-auto">
|
|
660
|
+
<LogPaginationButton
|
|
661
|
+
label="Oldest"
|
|
662
|
+
title="Load the oldest logs in this session"
|
|
663
|
+
icon={<ChevronsLeft className="size-3.5" />}
|
|
664
|
+
disabled={disabled || !pagination.hasOlder}
|
|
665
|
+
onClick={pagination.onOldest}
|
|
666
|
+
/>
|
|
667
|
+
<LogPaginationButton
|
|
668
|
+
label="Older"
|
|
669
|
+
title="Load older logs"
|
|
670
|
+
icon={<ChevronLeft className="size-3.5" />}
|
|
671
|
+
disabled={disabled || !pagination.hasOlder}
|
|
672
|
+
onClick={pagination.onOlder}
|
|
673
|
+
/>
|
|
674
|
+
<LogPaginationButton
|
|
675
|
+
label="Newer"
|
|
676
|
+
title="Load newer logs"
|
|
677
|
+
icon={<ChevronRight className="size-3.5" />}
|
|
678
|
+
disabled={disabled || !pagination.hasNewer}
|
|
679
|
+
onClick={pagination.onNewer}
|
|
680
|
+
/>
|
|
681
|
+
<LogPaginationButton
|
|
682
|
+
label="Newest"
|
|
683
|
+
title="Load the newest logs in this session"
|
|
684
|
+
icon={<ChevronsRight className="size-3.5" />}
|
|
685
|
+
disabled={disabled || !pagination.hasNewer}
|
|
686
|
+
onClick={pagination.onNewest}
|
|
687
|
+
/>
|
|
312
688
|
</div>
|
|
313
689
|
</div>
|
|
314
690
|
);
|
|
@@ -413,34 +789,7 @@ function CopyableCommand({ command }: { command: string }): JSX.Element {
|
|
|
413
789
|
className="text-muted-foreground hover:text-foreground transition-colors shrink-0 cursor-pointer"
|
|
414
790
|
aria-label="Copy command"
|
|
415
791
|
>
|
|
416
|
-
{copied ?
|
|
417
|
-
<svg
|
|
418
|
-
width="16"
|
|
419
|
-
height="16"
|
|
420
|
-
viewBox="0 0 24 24"
|
|
421
|
-
fill="none"
|
|
422
|
-
stroke="currentColor"
|
|
423
|
-
strokeWidth="2"
|
|
424
|
-
strokeLinecap="round"
|
|
425
|
-
strokeLinejoin="round"
|
|
426
|
-
>
|
|
427
|
-
<path d="M20 6 9 17l-5-5" />
|
|
428
|
-
</svg>
|
|
429
|
-
) : (
|
|
430
|
-
<svg
|
|
431
|
-
width="16"
|
|
432
|
-
height="16"
|
|
433
|
-
viewBox="0 0 24 24"
|
|
434
|
-
fill="none"
|
|
435
|
-
stroke="currentColor"
|
|
436
|
-
strokeWidth="2"
|
|
437
|
-
strokeLinecap="round"
|
|
438
|
-
strokeLinejoin="round"
|
|
439
|
-
>
|
|
440
|
-
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
|
|
441
|
-
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
|
|
442
|
-
</svg>
|
|
443
|
-
)}
|
|
792
|
+
{copied ? <Check className="size-4" /> : <Copy className="size-4" />}
|
|
444
793
|
</button>
|
|
445
794
|
</div>
|
|
446
795
|
);
|
|
@@ -466,6 +815,125 @@ function McpReadyBadge(): JSX.Element {
|
|
|
466
815
|
);
|
|
467
816
|
}
|
|
468
817
|
|
|
818
|
+
const CRAB_VARIANT_COLORS = [
|
|
819
|
+
"text-amber-500",
|
|
820
|
+
"text-rose-500",
|
|
821
|
+
"text-sky-500",
|
|
822
|
+
"text-emerald-500",
|
|
823
|
+
"text-violet-500",
|
|
824
|
+
"text-orange-500",
|
|
825
|
+
"text-cyan-500",
|
|
826
|
+
"text-pink-500",
|
|
827
|
+
"text-lime-500",
|
|
828
|
+
"text-blue-500",
|
|
829
|
+
"text-yellow-500",
|
|
830
|
+
"text-fuchsia-500",
|
|
831
|
+
] as const;
|
|
832
|
+
|
|
833
|
+
function CrabFamily({
|
|
834
|
+
compact,
|
|
835
|
+
entrancePhase,
|
|
836
|
+
}: {
|
|
837
|
+
compact: boolean;
|
|
838
|
+
entrancePhase: "hidden" | "playing" | "done";
|
|
839
|
+
}): JSX.Element {
|
|
840
|
+
if (compact) {
|
|
841
|
+
return <CrabLogo className="size-7 text-amber-500" />;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
return (
|
|
845
|
+
<span className="flex shrink-0 items-end gap-1 group cursor-default" aria-hidden="true">
|
|
846
|
+
<CrabLogo className="size-10 text-amber-500 transition-all duration-300 group-hover:scale-125 group-hover:-translate-y-1.5" />
|
|
847
|
+
<span className="hidden items-end gap-0.5 sm:flex">
|
|
848
|
+
{crabVariants.map((Crab, i) => {
|
|
849
|
+
const color = CRAB_VARIANT_COLORS[i] ?? "text-amber-500";
|
|
850
|
+
const entranceClass =
|
|
851
|
+
entrancePhase === "hidden"
|
|
852
|
+
? "opacity-0 scale-0"
|
|
853
|
+
: entrancePhase === "playing"
|
|
854
|
+
? "animate-crab-piano-pop"
|
|
855
|
+
: "";
|
|
856
|
+
return (
|
|
857
|
+
<Crab
|
|
858
|
+
key={i}
|
|
859
|
+
className={`size-5 ${color} transition-all duration-300 ease-out group-hover:scale-125 group-hover:-translate-y-1 ${entranceClass}`}
|
|
860
|
+
style={{
|
|
861
|
+
transitionDelay: `${i * 50}ms`,
|
|
862
|
+
...(entrancePhase === "playing" ? { animationDelay: `${i * 400}ms` } : {}),
|
|
863
|
+
}}
|
|
864
|
+
/>
|
|
865
|
+
);
|
|
866
|
+
})}
|
|
867
|
+
</span>
|
|
868
|
+
</span>
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function BrandHeader({
|
|
873
|
+
compact,
|
|
874
|
+
crabEntrancePhase,
|
|
875
|
+
}: {
|
|
876
|
+
compact: boolean;
|
|
877
|
+
crabEntrancePhase: "hidden" | "playing" | "done";
|
|
878
|
+
}): JSX.Element {
|
|
879
|
+
if (compact) {
|
|
880
|
+
return (
|
|
881
|
+
<div className="pb-3">
|
|
882
|
+
<div className="flex items-start justify-between gap-3">
|
|
883
|
+
<div className="flex min-w-0 items-start gap-2">
|
|
884
|
+
<CrabFamily compact={true} entrancePhase={crabEntrancePhase} />
|
|
885
|
+
<div className="min-w-0">
|
|
886
|
+
<div className="flex min-w-0 items-baseline gap-2">
|
|
887
|
+
<span className="truncate text-base font-bold">Inspector</span>
|
|
888
|
+
<span className="hidden shrink-0 font-mono text-[11px] font-semibold text-muted-foreground sm:inline">
|
|
889
|
+
v{packageJson.version}
|
|
890
|
+
</span>
|
|
891
|
+
</div>
|
|
892
|
+
<div className="hidden text-[11px] font-semibold text-muted-foreground sm:block">
|
|
893
|
+
Local First. Evidence First.
|
|
894
|
+
</div>
|
|
895
|
+
</div>
|
|
896
|
+
</div>
|
|
897
|
+
<div className="flex shrink-0 items-center gap-1">
|
|
898
|
+
<GroupsDialog />
|
|
899
|
+
<SettingsDialog />
|
|
900
|
+
</div>
|
|
901
|
+
</div>
|
|
902
|
+
<div className="mt-2 hidden sm:block">
|
|
903
|
+
<McpReadyBadge />
|
|
904
|
+
</div>
|
|
905
|
+
</div>
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
return (
|
|
910
|
+
<div className="grid grid-cols-[1fr_auto_1fr] items-start gap-3 pb-6">
|
|
911
|
+
<div />
|
|
912
|
+
<h1 className="flex min-w-0 flex-col items-center gap-2 text-center">
|
|
913
|
+
<span className="flex max-w-[calc(100vw-7rem)] items-end gap-2 whitespace-nowrap">
|
|
914
|
+
<CrabFamily compact={false} entrancePhase={crabEntrancePhase} />
|
|
915
|
+
<span className="flex min-w-0 items-baseline gap-2 pl-1">
|
|
916
|
+
<span className="truncate text-lg font-bold">Agent Inspector</span>
|
|
917
|
+
<span className="shrink-0 font-mono text-xs font-semibold text-muted-foreground">
|
|
918
|
+
v{packageJson.version}
|
|
919
|
+
</span>
|
|
920
|
+
</span>
|
|
921
|
+
<Plus className="size-4 shrink-0 text-muted-foreground/70" aria-hidden="true" />
|
|
922
|
+
<McpLogo className="size-10 shrink-0" />
|
|
923
|
+
</span>
|
|
924
|
+
<span className="max-w-[calc(100vw-7rem)] text-sm font-semibold text-muted-foreground">
|
|
925
|
+
Local First. Evidence First.
|
|
926
|
+
</span>
|
|
927
|
+
<McpReadyBadge />
|
|
928
|
+
</h1>
|
|
929
|
+
<div className="justify-self-end flex items-center gap-1">
|
|
930
|
+
<GroupsDialog />
|
|
931
|
+
<SettingsDialog />
|
|
932
|
+
</div>
|
|
933
|
+
</div>
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
|
|
469
937
|
function SessionContextLogButton({ label, logId }: { label: string; logId: number }): JSX.Element {
|
|
470
938
|
return (
|
|
471
939
|
<button
|
|
@@ -654,6 +1122,18 @@ function SessionContextSummaryPanel({
|
|
|
654
1122
|
);
|
|
655
1123
|
}
|
|
656
1124
|
|
|
1125
|
+
function formatSessionContextOverview(summary: SessionContextSummary): string {
|
|
1126
|
+
const riskCount = summary.models.filter((model) => model.latest.health.level === "risk").length;
|
|
1127
|
+
if (riskCount > 0) return `${riskCount} risk`;
|
|
1128
|
+
|
|
1129
|
+
const optimizableCount = summary.models.filter(
|
|
1130
|
+
(model) => model.latest.health.level === "optimizable",
|
|
1131
|
+
).length;
|
|
1132
|
+
if (optimizableCount > 0) return `${optimizableCount} optimizable`;
|
|
1133
|
+
|
|
1134
|
+
return `${summary.models.length} model${summary.models.length === 1 ? "" : "s"}`;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
657
1137
|
function SessionContextBar({
|
|
658
1138
|
label,
|
|
659
1139
|
logs,
|
|
@@ -672,11 +1152,16 @@ function SessionContextBar({
|
|
|
672
1152
|
showBackLink: boolean;
|
|
673
1153
|
}): JSX.Element {
|
|
674
1154
|
const [copied, setCopied] = useState(false);
|
|
1155
|
+
const [detailsOpen, setDetailsOpen] = useState(false);
|
|
675
1156
|
const timeRange = useMemo(
|
|
676
1157
|
() => formatTimeRange(logs, timeDisplayFormat),
|
|
677
1158
|
[logs, timeDisplayFormat],
|
|
678
1159
|
);
|
|
679
1160
|
const userAgent = useMemo(() => getFirstUserAgent(logs), [logs]);
|
|
1161
|
+
const contextOverview = useMemo(
|
|
1162
|
+
() => formatSessionContextOverview(contextSummary),
|
|
1163
|
+
[contextSummary],
|
|
1164
|
+
);
|
|
680
1165
|
|
|
681
1166
|
const handleCopyLink = useCallback(() => {
|
|
682
1167
|
void copyTextToClipboard(window.location.href).then((success) => {
|
|
@@ -687,8 +1172,8 @@ function SessionContextBar({
|
|
|
687
1172
|
}, []);
|
|
688
1173
|
|
|
689
1174
|
return (
|
|
690
|
-
<div className="mb-
|
|
691
|
-
<div className="flex items-center gap-
|
|
1175
|
+
<div className="mb-3 rounded-md border border-border bg-muted/15 px-3 py-2 text-xs">
|
|
1176
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
692
1177
|
{showBackLink && (
|
|
693
1178
|
<a
|
|
694
1179
|
href="/"
|
|
@@ -699,7 +1184,7 @@ function SessionContextBar({
|
|
|
699
1184
|
<ArrowLeft className="size-3.5" />
|
|
700
1185
|
</a>
|
|
701
1186
|
)}
|
|
702
|
-
<div className="min-w-
|
|
1187
|
+
<div className="min-w-[180px] flex-1">
|
|
703
1188
|
<div className="flex min-w-0 items-center gap-2">
|
|
704
1189
|
<span className="font-mono font-semibold text-purple-400/90 truncate" title={label}>
|
|
705
1190
|
{truncateSessionId(label)}
|
|
@@ -725,6 +1210,22 @@ function SessionContextBar({
|
|
|
725
1210
|
)}
|
|
726
1211
|
</div>
|
|
727
1212
|
</div>
|
|
1213
|
+
{contextSummary.models.length > 0 && (
|
|
1214
|
+
<button
|
|
1215
|
+
type="button"
|
|
1216
|
+
onClick={() => setDetailsOpen((value) => !value)}
|
|
1217
|
+
className="inline-flex h-8 shrink-0 items-center gap-1.5 rounded-md border border-border bg-background/60 px-2.5 font-mono text-[11px] text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
|
1218
|
+
aria-expanded={detailsOpen}
|
|
1219
|
+
title={detailsOpen ? "Hide context details" : "Show context details"}
|
|
1220
|
+
>
|
|
1221
|
+
<span>{contextOverview}</span>
|
|
1222
|
+
{detailsOpen ? (
|
|
1223
|
+
<ChevronUp className="size-3.5" />
|
|
1224
|
+
) : (
|
|
1225
|
+
<ChevronDown className="size-3.5" />
|
|
1226
|
+
)}
|
|
1227
|
+
</button>
|
|
1228
|
+
)}
|
|
728
1229
|
<button
|
|
729
1230
|
type="button"
|
|
730
1231
|
onClick={handleCopyLink}
|
|
@@ -735,7 +1236,7 @@ function SessionContextBar({
|
|
|
735
1236
|
{copied ? <Check className="size-3.5" /> : <Copy className="size-3.5" />}
|
|
736
1237
|
</button>
|
|
737
1238
|
</div>
|
|
738
|
-
<SessionContextSummaryPanel summary={contextSummary} />
|
|
1239
|
+
{detailsOpen && <SessionContextSummaryPanel summary={contextSummary} />}
|
|
739
1240
|
</div>
|
|
740
1241
|
);
|
|
741
1242
|
}
|
|
@@ -763,6 +1264,8 @@ export type ProxyViewerProps = {
|
|
|
763
1264
|
timeDisplayFormat: TimeDisplayFormat;
|
|
764
1265
|
/** True while a pinned session page is waiting for its initial log scan. */
|
|
765
1266
|
isLoading?: boolean;
|
|
1267
|
+
/** Cursor controls for history pages. Undefined keeps the live stream UI compact. */
|
|
1268
|
+
pagination?: LogPaginationControls;
|
|
766
1269
|
/** Group/run member metadata attached to the pinned session id. */
|
|
767
1270
|
sessionMemberships?: SessionMembershipEvidence[];
|
|
768
1271
|
/** Hide the session filter dropdown. Used on `/session/$id` routes where
|
|
@@ -790,6 +1293,7 @@ export function ProxyViewer({
|
|
|
790
1293
|
slowResponseThresholdSeconds,
|
|
791
1294
|
timeDisplayFormat,
|
|
792
1295
|
isLoading = false,
|
|
1296
|
+
pagination,
|
|
793
1297
|
sessionMemberships = [],
|
|
794
1298
|
hideSessionFilter = false,
|
|
795
1299
|
pinnedSessionId,
|
|
@@ -870,6 +1374,23 @@ export function ProxyViewer({
|
|
|
870
1374
|
const cacheTrends = useMemo(() => computeCacheTrends(groups), [groups]);
|
|
871
1375
|
const comparisonPredecessors = useMemo(() => buildValidPredecessors(groups), [groups]);
|
|
872
1376
|
const exportActionVisibility = getExportActionVisibility(captureMode, logs.length);
|
|
1377
|
+
const isPinnedSessionPage = pinnedSessionId !== undefined;
|
|
1378
|
+
const activeScopeLabel =
|
|
1379
|
+
hasSessionContext && sessionContextScope !== null
|
|
1380
|
+
? truncateSessionId(sessionContextScope.label)
|
|
1381
|
+
: selectedSession === "__all__"
|
|
1382
|
+
? "All sessions"
|
|
1383
|
+
: truncateSessionId(selectedSession);
|
|
1384
|
+
const slateStats = useMemo(
|
|
1385
|
+
() =>
|
|
1386
|
+
buildSessionSlateStats({
|
|
1387
|
+
logs,
|
|
1388
|
+
isLoading,
|
|
1389
|
+
timeDisplayFormat,
|
|
1390
|
+
slowResponseThresholdSeconds,
|
|
1391
|
+
}),
|
|
1392
|
+
[isLoading, logs, slowResponseThresholdSeconds, timeDisplayFormat],
|
|
1393
|
+
);
|
|
873
1394
|
const handleCompareWithPrevious = useCallback(
|
|
874
1395
|
(log: CapturedLog) => {
|
|
875
1396
|
const predecessor = comparisonPredecessors.get(log.id);
|
|
@@ -879,297 +1400,282 @@ export function ProxyViewer({
|
|
|
879
1400
|
);
|
|
880
1401
|
|
|
881
1402
|
return (
|
|
882
|
-
<div className="
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
className="
|
|
898
|
-
|
|
899
|
-
>
|
|
900
|
-
<
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
"text-sky-500",
|
|
907
|
-
"text-emerald-500",
|
|
908
|
-
"text-violet-500",
|
|
909
|
-
"text-orange-500",
|
|
910
|
-
"text-cyan-500",
|
|
911
|
-
"text-pink-500",
|
|
912
|
-
"text-lime-500",
|
|
913
|
-
"text-blue-500",
|
|
914
|
-
"text-yellow-500",
|
|
915
|
-
"text-fuchsia-500",
|
|
916
|
-
][i];
|
|
917
|
-
const entranceClass =
|
|
918
|
-
crabEntrancePhase === "hidden"
|
|
919
|
-
? "opacity-0 scale-0"
|
|
920
|
-
: crabEntrancePhase === "playing"
|
|
921
|
-
? "animate-crab-piano-pop"
|
|
922
|
-
: "";
|
|
923
|
-
return (
|
|
924
|
-
<Crab
|
|
925
|
-
key={i}
|
|
926
|
-
className={`size-5 ${color} transition-all duration-300 ease-out group-hover:scale-125 group-hover:-translate-y-1 ${entranceClass}`}
|
|
927
|
-
style={{
|
|
928
|
-
transitionDelay: `${i * 50}ms`,
|
|
929
|
-
...(crabEntrancePhase === "playing"
|
|
930
|
-
? { animationDelay: `${i * 400}ms` }
|
|
931
|
-
: {}),
|
|
932
|
-
}}
|
|
933
|
-
/>
|
|
934
|
-
);
|
|
935
|
-
})}
|
|
936
|
-
</span>
|
|
937
|
-
</span>
|
|
938
|
-
<span className="flex min-w-0 items-baseline gap-2 pl-1">
|
|
939
|
-
<span className="truncate text-lg font-bold">Agent Inspector</span>
|
|
940
|
-
<span className="shrink-0 font-mono text-xs font-semibold text-muted-foreground">
|
|
941
|
-
v{packageJson.version}
|
|
942
|
-
</span>
|
|
943
|
-
</span>
|
|
944
|
-
<Plus className="size-4 shrink-0 text-muted-foreground/70" aria-hidden="true" />
|
|
945
|
-
<McpLogo className="size-10 shrink-0" />
|
|
946
|
-
</span>
|
|
947
|
-
<span className="max-w-[calc(100vw-7rem)] text-sm font-semibold text-muted-foreground">
|
|
948
|
-
Local First. Evidence First.
|
|
949
|
-
</span>
|
|
950
|
-
<McpReadyBadge />
|
|
951
|
-
</h1>
|
|
952
|
-
<div className="justify-self-end flex items-center gap-1">
|
|
953
|
-
<GroupsDialog />
|
|
954
|
-
<SettingsDialog />
|
|
955
|
-
</div>
|
|
956
|
-
</div>
|
|
1403
|
+
<div className="min-h-screen bg-[#070809] text-foreground">
|
|
1404
|
+
<div className="mx-auto grid min-h-screen max-w-[1920px] grid-cols-1 lg:grid-cols-[340px_minmax(0,1fr)] xl:grid-cols-[360px_minmax(0,1fr)]">
|
|
1405
|
+
<aside className="border-b border-border bg-[#090a0c] lg:sticky lg:top-0 lg:h-screen lg:overflow-y-auto lg:border-b-0 lg:border-r">
|
|
1406
|
+
<div className="space-y-4 p-4">
|
|
1407
|
+
<BrandHeader compact={true} crabEntrancePhase={crabEntrancePhase} />
|
|
1408
|
+
|
|
1409
|
+
<div className="grid grid-cols-3 gap-2 border-y border-border/70 py-3">
|
|
1410
|
+
<div className="min-w-0">
|
|
1411
|
+
<div className="font-mono text-lg font-semibold text-foreground">{logs.length}</div>
|
|
1412
|
+
<div className="text-[10px] uppercase text-muted-foreground">Requests</div>
|
|
1413
|
+
</div>
|
|
1414
|
+
<div className="min-w-0">
|
|
1415
|
+
<div className="font-mono text-lg font-semibold text-foreground">
|
|
1416
|
+
{formatTokens(hasSessionContext ? sessionContextTotalIn : totalIn)}
|
|
1417
|
+
</div>
|
|
1418
|
+
<div className="text-[10px] uppercase text-muted-foreground">Input</div>
|
|
1419
|
+
</div>
|
|
1420
|
+
<div className="min-w-0">
|
|
1421
|
+
<div className="font-mono text-lg font-semibold text-foreground">
|
|
1422
|
+
{formatTokens(hasSessionContext ? sessionContextTotalOut : totalOut)}
|
|
1423
|
+
</div>
|
|
1424
|
+
<div className="text-[10px] uppercase text-muted-foreground">Output</div>
|
|
1425
|
+
</div>
|
|
1426
|
+
</div>
|
|
957
1427
|
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1428
|
+
<div className="space-y-2">
|
|
1429
|
+
<div className="text-[10px] font-semibold uppercase tracking-normal text-muted-foreground">
|
|
1430
|
+
Workspace
|
|
1431
|
+
</div>
|
|
1432
|
+
<div className="rounded-md border border-border bg-[#0c0d0f] px-3 py-2">
|
|
1433
|
+
<div
|
|
1434
|
+
className="font-mono text-xs font-semibold text-purple-300"
|
|
1435
|
+
title={activeScopeLabel}
|
|
1436
|
+
>
|
|
1437
|
+
{activeScopeLabel}
|
|
1438
|
+
</div>
|
|
1439
|
+
<div className="mt-1 text-[11px] text-muted-foreground">
|
|
1440
|
+
{isPinnedSessionPage ? "Session workspace" : "Live workspace"}
|
|
1441
|
+
</div>
|
|
1442
|
+
</div>
|
|
1443
|
+
</div>
|
|
969
1444
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1445
|
+
{hasSessionContext && sessionContextScope !== null && (
|
|
1446
|
+
<SessionContextBar
|
|
1447
|
+
label={sessionContextScope.label}
|
|
1448
|
+
logs={sessionContextLogs}
|
|
1449
|
+
totalIn={sessionContextTotalIn}
|
|
1450
|
+
totalOut={sessionContextTotalOut}
|
|
1451
|
+
timeDisplayFormat={timeDisplayFormat}
|
|
1452
|
+
contextSummary={sessionContextSummary}
|
|
1453
|
+
showBackLink={sessionContextScope.showBackLink}
|
|
1454
|
+
/>
|
|
1455
|
+
)}
|
|
973
1456
|
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
<SelectTrigger className="flex-1 max-w-[350px] text-xs">
|
|
979
|
-
<SelectValue placeholder="All sessions" />
|
|
980
|
-
</SelectTrigger>
|
|
981
|
-
<SelectContent>
|
|
982
|
-
<SelectItem value="__all__">All sessions</SelectItem>
|
|
983
|
-
{sessions.map((s) => (
|
|
984
|
-
<SelectItem key={s} value={s}>
|
|
985
|
-
{truncateSessionId(s)}
|
|
986
|
-
</SelectItem>
|
|
987
|
-
))}
|
|
988
|
-
</SelectContent>
|
|
989
|
-
</Select>
|
|
990
|
-
)}
|
|
991
|
-
<Select value={selectedModel} onValueChange={onModelChange}>
|
|
992
|
-
<SelectTrigger className="flex-1 max-w-[250px] text-xs">
|
|
993
|
-
<SelectValue placeholder="All models" />
|
|
994
|
-
</SelectTrigger>
|
|
995
|
-
<SelectContent>
|
|
996
|
-
<SelectItem value="__all__">All models</SelectItem>
|
|
997
|
-
{models.map((m) => (
|
|
998
|
-
<SelectItem key={m} value={m}>
|
|
999
|
-
{m}
|
|
1000
|
-
</SelectItem>
|
|
1001
|
-
))}
|
|
1002
|
-
</SelectContent>
|
|
1003
|
-
</Select>
|
|
1004
|
-
{captureMode === "full" ? (
|
|
1005
|
-
<div className="flex items-center border border-border rounded-md overflow-hidden">
|
|
1006
|
-
<button
|
|
1007
|
-
type="button"
|
|
1008
|
-
onClick={() => onViewModeChange("simple")}
|
|
1009
|
-
className={`h-8 px-3 cursor-pointer transition-colors text-xs ${
|
|
1010
|
-
viewMode === "simple"
|
|
1011
|
-
? "bg-muted text-foreground"
|
|
1012
|
-
: "text-muted-foreground hover:bg-muted/50"
|
|
1013
|
-
}`}
|
|
1014
|
-
>
|
|
1015
|
-
Simple
|
|
1016
|
-
</button>
|
|
1017
|
-
<button
|
|
1018
|
-
type="button"
|
|
1019
|
-
onClick={() => onViewModeChange("full")}
|
|
1020
|
-
className={`h-8 px-3 cursor-pointer transition-colors text-xs ${
|
|
1021
|
-
viewMode === "full"
|
|
1022
|
-
? "bg-muted text-foreground"
|
|
1023
|
-
: "text-muted-foreground hover:bg-muted/50"
|
|
1024
|
-
}`}
|
|
1025
|
-
>
|
|
1026
|
-
Full
|
|
1027
|
-
</button>
|
|
1028
|
-
</div>
|
|
1029
|
-
) : (
|
|
1030
|
-
<div className="flex h-8 items-center rounded-md border border-border bg-muted px-3 text-xs text-foreground">
|
|
1031
|
-
Simple
|
|
1032
|
-
</div>
|
|
1033
|
-
)}
|
|
1034
|
-
<div className="flex-1" />
|
|
1035
|
-
{!hasSessionContext && (
|
|
1036
|
-
<span className="text-muted-foreground text-xs font-mono">
|
|
1037
|
-
{logs.length} request{logs.length !== 1 ? "s" : ""}
|
|
1038
|
-
{totalIn > 0 || totalOut > 0
|
|
1039
|
-
? ` - ${formatTokens(totalIn)} in / ${formatTokens(totalOut)} out`
|
|
1040
|
-
: ""}
|
|
1041
|
-
</span>
|
|
1042
|
-
)}
|
|
1043
|
-
{exportActionVisibility.redacted && (
|
|
1044
|
-
<button
|
|
1045
|
-
type="button"
|
|
1046
|
-
onClick={() => {
|
|
1047
|
-
void handleExport("redacted");
|
|
1048
|
-
}}
|
|
1049
|
-
disabled={exporting !== null}
|
|
1050
|
-
className="h-8 px-3 text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed inline-flex items-center gap-1.5 rounded-md hover:bg-muted"
|
|
1051
|
-
title="Export redacted logs as JSON ZIP"
|
|
1052
|
-
>
|
|
1053
|
-
{exporting === "redacted" ? (
|
|
1054
|
-
<span>Exporting...</span>
|
|
1055
|
-
) : (
|
|
1056
|
-
<>
|
|
1057
|
-
<Download className="size-3.5" />
|
|
1058
|
-
<span>Export</span>
|
|
1059
|
-
</>
|
|
1457
|
+
{hasSessionContext &&
|
|
1458
|
+
pinnedSessionId !== undefined &&
|
|
1459
|
+
sessionMemberships.length > 0 && (
|
|
1460
|
+
<SessionMembershipPanel memberships={sessionMemberships} />
|
|
1060
1461
|
)}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1462
|
+
|
|
1463
|
+
<div className="space-y-2">
|
|
1464
|
+
<div className="text-[10px] font-semibold uppercase tracking-normal text-muted-foreground">
|
|
1465
|
+
Filters
|
|
1466
|
+
</div>
|
|
1467
|
+
{!hideSessionFilter && (
|
|
1468
|
+
<Select value={selectedSession} onValueChange={onSessionChange}>
|
|
1469
|
+
<SelectTrigger className="h-9 w-full text-xs">
|
|
1470
|
+
<SelectValue placeholder="All sessions" />
|
|
1471
|
+
</SelectTrigger>
|
|
1472
|
+
<SelectContent>
|
|
1473
|
+
<SelectItem value="__all__">All sessions</SelectItem>
|
|
1474
|
+
{sessions.map((s) => (
|
|
1475
|
+
<SelectItem key={s} value={s}>
|
|
1476
|
+
{truncateSessionId(s)}
|
|
1477
|
+
</SelectItem>
|
|
1478
|
+
))}
|
|
1479
|
+
</SelectContent>
|
|
1480
|
+
</Select>
|
|
1481
|
+
)}
|
|
1482
|
+
<Select value={selectedModel} onValueChange={onModelChange}>
|
|
1483
|
+
<SelectTrigger className="h-9 w-full text-xs">
|
|
1484
|
+
<SelectValue placeholder="All models" />
|
|
1485
|
+
</SelectTrigger>
|
|
1486
|
+
<SelectContent>
|
|
1487
|
+
<SelectItem value="__all__">All models</SelectItem>
|
|
1488
|
+
{models.map((m) => (
|
|
1489
|
+
<SelectItem key={m} value={m}>
|
|
1490
|
+
{m}
|
|
1491
|
+
</SelectItem>
|
|
1492
|
+
))}
|
|
1493
|
+
</SelectContent>
|
|
1494
|
+
</Select>
|
|
1495
|
+
{captureMode === "full" ? (
|
|
1496
|
+
<div className="grid h-9 grid-cols-2 overflow-hidden rounded-md border border-border">
|
|
1497
|
+
<button
|
|
1498
|
+
type="button"
|
|
1499
|
+
onClick={() => onViewModeChange("simple")}
|
|
1500
|
+
className={cn(
|
|
1501
|
+
"text-xs transition-colors",
|
|
1502
|
+
viewMode === "simple"
|
|
1503
|
+
? "bg-muted text-foreground"
|
|
1504
|
+
: "text-muted-foreground hover:bg-muted/50",
|
|
1505
|
+
)}
|
|
1506
|
+
>
|
|
1507
|
+
Simple
|
|
1508
|
+
</button>
|
|
1509
|
+
<button
|
|
1510
|
+
type="button"
|
|
1511
|
+
onClick={() => onViewModeChange("full")}
|
|
1512
|
+
className={cn(
|
|
1513
|
+
"text-xs transition-colors",
|
|
1514
|
+
viewMode === "full"
|
|
1515
|
+
? "bg-muted text-foreground"
|
|
1516
|
+
: "text-muted-foreground hover:bg-muted/50",
|
|
1517
|
+
)}
|
|
1518
|
+
>
|
|
1519
|
+
Full
|
|
1520
|
+
</button>
|
|
1521
|
+
</div>
|
|
1075
1522
|
) : (
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
</>
|
|
1523
|
+
<div className="flex h-9 items-center rounded-md border border-border bg-muted px-3 text-xs text-foreground">
|
|
1524
|
+
Simple
|
|
1525
|
+
</div>
|
|
1080
1526
|
)}
|
|
1081
|
-
</
|
|
1082
|
-
)}
|
|
1083
|
-
<button
|
|
1084
|
-
type="button"
|
|
1085
|
-
onClick={onClearAll}
|
|
1086
|
-
className="h-8 px-3 text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer inline-flex items-center gap-1.5 rounded-md hover:bg-muted"
|
|
1087
|
-
title="Clear all logs"
|
|
1088
|
-
>
|
|
1089
|
-
<Trash2 className="size-3.5" />
|
|
1090
|
-
<span>Clear</span>
|
|
1091
|
-
</button>
|
|
1092
|
-
</div>
|
|
1093
|
-
{exportError !== null && (
|
|
1094
|
-
<div className="mb-4 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
|
|
1095
|
-
{exportError}
|
|
1527
|
+
</div>
|
|
1096
1528
|
</div>
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
{isLoading ? "Loading session data..." : "Session not found"}
|
|
1107
|
-
</p>
|
|
1108
|
-
<p className="text-xs font-mono bg-muted px-3 py-1 rounded inline-block max-w-[500px] break-all">
|
|
1109
|
-
{truncateSessionId(selectedSession)}
|
|
1110
|
-
</p>
|
|
1111
|
-
{sessionMemberships.length > 0 && (
|
|
1112
|
-
<div className="mx-auto max-w-3xl text-left">
|
|
1113
|
-
<SessionMembershipPanel memberships={sessionMemberships} />
|
|
1529
|
+
</aside>
|
|
1530
|
+
|
|
1531
|
+
<main className="min-w-0">
|
|
1532
|
+
<div className="sticky top-0 z-30 border-b border-border bg-[#08090b]/95 px-4 py-3 shadow-[0_12px_40px_rgba(0,0,0,0.24)] backdrop-blur lg:px-5">
|
|
1533
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
1534
|
+
<div className="min-w-[180px] flex-1">
|
|
1535
|
+
<div className="text-sm font-semibold text-foreground">Logs</div>
|
|
1536
|
+
<div className="mt-0.5 font-mono text-[11px] text-muted-foreground">
|
|
1537
|
+
{activeScopeLabel}
|
|
1114
1538
|
</div>
|
|
1539
|
+
</div>
|
|
1540
|
+
{exportActionVisibility.redacted && (
|
|
1541
|
+
<button
|
|
1542
|
+
type="button"
|
|
1543
|
+
onClick={() => {
|
|
1544
|
+
void handleExport("redacted");
|
|
1545
|
+
}}
|
|
1546
|
+
disabled={exporting !== null}
|
|
1547
|
+
className="inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
|
1548
|
+
title="Export redacted logs as JSON ZIP"
|
|
1549
|
+
>
|
|
1550
|
+
{exporting === "redacted" ? (
|
|
1551
|
+
<span>Exporting...</span>
|
|
1552
|
+
) : (
|
|
1553
|
+
<>
|
|
1554
|
+
<Download className="size-3.5" />
|
|
1555
|
+
<span>Export</span>
|
|
1556
|
+
</>
|
|
1557
|
+
)}
|
|
1558
|
+
</button>
|
|
1115
1559
|
)}
|
|
1116
|
-
{
|
|
1117
|
-
<
|
|
1118
|
-
|
|
1119
|
-
{
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1560
|
+
{exportActionVisibility.raw && (
|
|
1561
|
+
<button
|
|
1562
|
+
type="button"
|
|
1563
|
+
onClick={() => {
|
|
1564
|
+
void handleExport("raw");
|
|
1565
|
+
}}
|
|
1566
|
+
disabled={exporting !== null}
|
|
1567
|
+
className="inline-flex h-8 items-center gap-1.5 rounded-md px-2.5 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
|
1568
|
+
title="Export raw logs without redaction"
|
|
1569
|
+
>
|
|
1570
|
+
{exporting === "raw" ? (
|
|
1571
|
+
<span>Exporting...</span>
|
|
1123
1572
|
) : (
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
>
|
|
1129
|
-
Show all sessions
|
|
1130
|
-
</button>
|
|
1573
|
+
<>
|
|
1574
|
+
<FileJson className="size-3.5" />
|
|
1575
|
+
<span>Raw</span>
|
|
1576
|
+
</>
|
|
1131
1577
|
)}
|
|
1132
|
-
</
|
|
1578
|
+
</button>
|
|
1133
1579
|
)}
|
|
1580
|
+
<button
|
|
1581
|
+
type="button"
|
|
1582
|
+
onClick={onClearAll}
|
|
1583
|
+
className="inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
|
1584
|
+
title="Clear all logs"
|
|
1585
|
+
>
|
|
1586
|
+
<Trash2 className="size-3.5" />
|
|
1587
|
+
<span>Clear</span>
|
|
1588
|
+
</button>
|
|
1134
1589
|
</div>
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
<
|
|
1138
|
-
|
|
1139
|
-
<div className="flex flex-col items-center gap-2">
|
|
1140
|
-
<CopyableCommand command="ANTHROPIC_BASE_URL=http://localhost:25947/proxy <your-tool>" />
|
|
1141
|
-
<CopyableCommand command="OPENAI_BASE_URL=http://localhost:25947/proxy <your-tool>" />
|
|
1590
|
+
<LogPaginationBar logs={logs} pagination={pagination} embedded={true} />
|
|
1591
|
+
{exportError !== null && (
|
|
1592
|
+
<div className="mt-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
|
|
1593
|
+
{exportError}
|
|
1142
1594
|
</div>
|
|
1143
|
-
|
|
1144
|
-
Container note: if your AI tool runs in a different container or host than Agent
|
|
1145
|
-
Inspector, replace localhost with an address reachable from that tool and verify the
|
|
1146
|
-
network/firewall path.
|
|
1147
|
-
</p>
|
|
1148
|
-
</div>
|
|
1149
|
-
)
|
|
1150
|
-
) : (
|
|
1151
|
-
<div
|
|
1152
|
-
ref={logListWrapperRef}
|
|
1153
|
-
tabIndex={0}
|
|
1154
|
-
className="flex flex-col gap-2 focus:outline-none"
|
|
1155
|
-
>
|
|
1156
|
-
<div ref={logListRef}>
|
|
1157
|
-
<ConversationGroupList
|
|
1158
|
-
groups={groups}
|
|
1159
|
-
viewMode={viewMode}
|
|
1160
|
-
strip={strip}
|
|
1161
|
-
slowResponseThresholdSeconds={slowResponseThresholdSeconds}
|
|
1162
|
-
cacheTrends={cacheTrends}
|
|
1163
|
-
onCompareWithPrevious={handleCompareWithPrevious}
|
|
1164
|
-
comparisonPredecessors={comparisonPredecessors}
|
|
1165
|
-
onClearGroup={onClearGroup}
|
|
1166
|
-
standalone={groups.length === 1}
|
|
1167
|
-
hasPinnedSessionContext={hasSessionContext}
|
|
1168
|
-
timeDisplayFormat={timeDisplayFormat}
|
|
1169
|
-
/>
|
|
1170
|
-
</div>
|
|
1595
|
+
)}
|
|
1171
1596
|
</div>
|
|
1172
|
-
|
|
1597
|
+
|
|
1598
|
+
<div className="px-4 py-4 lg:px-5">
|
|
1599
|
+
<SessionOpeningSlate stats={slateStats} />
|
|
1600
|
+
{logs.length === 0 ? (
|
|
1601
|
+
selectedSession !== "__all__" ? (
|
|
1602
|
+
<div className="py-16 text-center text-muted-foreground">
|
|
1603
|
+
<div className="mx-auto max-w-xl space-y-4 rounded-md border border-border bg-muted/10 px-4 py-8">
|
|
1604
|
+
<p className="text-sm font-medium">
|
|
1605
|
+
{isLoading ? "Loading session data..." : "Session not found"}
|
|
1606
|
+
</p>
|
|
1607
|
+
<p className="inline-block max-w-full rounded bg-muted px-3 py-1 font-mono text-xs break-all">
|
|
1608
|
+
{truncateSessionId(selectedSession)}
|
|
1609
|
+
</p>
|
|
1610
|
+
{sessionMemberships.length > 0 && (
|
|
1611
|
+
<div className="text-left">
|
|
1612
|
+
<SessionMembershipPanel memberships={sessionMemberships} />
|
|
1613
|
+
</div>
|
|
1614
|
+
)}
|
|
1615
|
+
{!isLoading && (
|
|
1616
|
+
<p className="text-xs">
|
|
1617
|
+
This session may have been cleared or never existed.{" "}
|
|
1618
|
+
{hideSessionFilter ? (
|
|
1619
|
+
<a href="/" className="underline transition-colors hover:text-foreground">
|
|
1620
|
+
Back to all sessions
|
|
1621
|
+
</a>
|
|
1622
|
+
) : (
|
|
1623
|
+
<button
|
|
1624
|
+
type="button"
|
|
1625
|
+
onClick={() => onSessionChange("__all__")}
|
|
1626
|
+
className="underline transition-colors hover:text-foreground"
|
|
1627
|
+
>
|
|
1628
|
+
Show all sessions
|
|
1629
|
+
</button>
|
|
1630
|
+
)}
|
|
1631
|
+
</p>
|
|
1632
|
+
)}
|
|
1633
|
+
</div>
|
|
1634
|
+
</div>
|
|
1635
|
+
) : (
|
|
1636
|
+
<div className="py-16 text-center text-muted-foreground">
|
|
1637
|
+
<div className="mx-auto max-w-2xl space-y-4 rounded-md border border-border bg-muted/10 px-4 py-8">
|
|
1638
|
+
<p className="text-sm">No requests captured yet.</p>
|
|
1639
|
+
<div className="flex flex-col items-center gap-2">
|
|
1640
|
+
<CopyableCommand command="ANTHROPIC_BASE_URL=http://localhost:25947/proxy <your-tool>" />
|
|
1641
|
+
<CopyableCommand command="OPENAI_BASE_URL=http://localhost:25947/proxy <your-tool>" />
|
|
1642
|
+
</div>
|
|
1643
|
+
<p className="mx-auto max-w-xl text-xs leading-relaxed text-muted-foreground">
|
|
1644
|
+
Container note: if your AI tool runs in a different container or host than
|
|
1645
|
+
Agent Inspector, replace localhost with an address reachable from that tool
|
|
1646
|
+
and verify the network/firewall path.
|
|
1647
|
+
</p>
|
|
1648
|
+
</div>
|
|
1649
|
+
</div>
|
|
1650
|
+
)
|
|
1651
|
+
) : (
|
|
1652
|
+
<div
|
|
1653
|
+
ref={logListWrapperRef}
|
|
1654
|
+
tabIndex={0}
|
|
1655
|
+
className="flex flex-col gap-2 focus:outline-none"
|
|
1656
|
+
>
|
|
1657
|
+
<div ref={logListRef}>
|
|
1658
|
+
<ConversationGroupList
|
|
1659
|
+
groups={groups}
|
|
1660
|
+
viewMode={viewMode}
|
|
1661
|
+
strip={strip}
|
|
1662
|
+
slowResponseThresholdSeconds={slowResponseThresholdSeconds}
|
|
1663
|
+
cacheTrends={cacheTrends}
|
|
1664
|
+
onCompareWithPrevious={handleCompareWithPrevious}
|
|
1665
|
+
comparisonPredecessors={comparisonPredecessors}
|
|
1666
|
+
onClearGroup={onClearGroup}
|
|
1667
|
+
standalone={groups.length === 1}
|
|
1668
|
+
hasPinnedSessionContext={hasSessionContext}
|
|
1669
|
+
timeDisplayFormat={timeDisplayFormat}
|
|
1670
|
+
/>
|
|
1671
|
+
</div>
|
|
1672
|
+
<div className="mt-3">
|
|
1673
|
+
<LogPaginationBar logs={logs} pagination={pagination} />
|
|
1674
|
+
</div>
|
|
1675
|
+
</div>
|
|
1676
|
+
)}
|
|
1677
|
+
</div>
|
|
1678
|
+
</main>
|
|
1173
1679
|
</div>
|
|
1174
1680
|
|
|
1175
1681
|
{/* Compare drawer: sibling of the log list, not a route change. */}
|