@tonyclaw/agent-inspector 2.1.16 → 2.1.18

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.
Files changed (55) hide show
  1. package/.output/cli.js +57 -13
  2. package/.output/nitro.json +1 -1
  3. package/.output/public/assets/{CompareDrawer-Ccrrcx1j.js → CompareDrawer-Fqqw5Lci.js} +1 -1
  4. package/.output/public/assets/ProxyViewerContainer-CP_RzwW9.js +106 -0
  5. package/.output/public/assets/{ReplayDialog-C7axhr-l.js → ReplayDialog-hYhz_i6W.js} +1 -1
  6. package/.output/public/assets/{RequestAnatomy-WOptg8j_.js → RequestAnatomy-CTuWbtwC.js} +1 -1
  7. package/.output/public/assets/{ResponseView-CSAcxh_M.js → ResponseView-CsZ7S5Gv.js} +2 -2
  8. package/.output/public/assets/{StreamingChunkSequence-DJS5KhPx.js → StreamingChunkSequence-PIbJMotU.js} +1 -1
  9. package/.output/public/assets/_sessionId-CUGA9qU3.js +1 -0
  10. package/.output/public/assets/{index-DMPNh46t.js → index-F7mynwuH.js} +1 -1
  11. package/.output/public/assets/index-G4dTUobw.js +1 -0
  12. package/.output/public/assets/index-gvYkgMPF.css +1 -0
  13. package/.output/public/assets/{json-viewer-inX5QSa3.js → json-viewer-DCXtNmmD.js} +1 -1
  14. package/.output/public/assets/{main-aCAKYGSD.js → main-BJZf7_Ib.js} +2 -2
  15. package/.output/server/_libs/lucide-react.mjs +220 -202
  16. package/.output/server/{_sessionId-C0fhvQnn.mjs → _sessionId-DjSHDNh8.mjs} +2 -2
  17. package/.output/server/_ssr/{CompareDrawer-CO-Ti5sg.mjs → CompareDrawer-Dal52IFo.mjs} +3 -3
  18. package/.output/server/_ssr/{ProxyViewerContainer-N4J7uBCX.mjs → ProxyViewerContainer-CVqFZGff.mjs} +1833 -1366
  19. package/.output/server/_ssr/{ReplayDialog-J1VxC0In.mjs → ReplayDialog-BOEgdMes.mjs} +4 -4
  20. package/.output/server/_ssr/{RequestAnatomy-DBH-F3rq.mjs → RequestAnatomy-gch7_vCV.mjs} +2 -2
  21. package/.output/server/_ssr/{ResponseView-DLFwV7Hd.mjs → ResponseView-lEWPy-H4.mjs} +3 -3
  22. package/.output/server/_ssr/{StreamingChunkSequence-BywmuND0.mjs → StreamingChunkSequence-DnudbJ1O.mjs} +2 -2
  23. package/.output/server/_ssr/{index-CStymIc0.mjs → index-Cza8zDrW.mjs} +2 -2
  24. package/.output/server/_ssr/index.mjs +2 -2
  25. package/.output/server/_ssr/{json-viewer-DlQu0N1x.mjs → json-viewer-C2lIVEX3.mjs} +3 -3
  26. package/.output/server/_ssr/{router-CQ4Zxhmr.mjs → router-bF9uzd1k.mjs} +939 -113
  27. package/.output/server/{_tanstack-start-manifest_v-BHeJBGfo.mjs → _tanstack-start-manifest_v-ErvW-mtr.mjs} +1 -1
  28. package/.output/server/index.mjs +74 -74
  29. package/README.md +57 -2
  30. package/package.json +1 -1
  31. package/src/cli/startupOutput.ts +16 -0
  32. package/src/cli.ts +61 -15
  33. package/src/components/ProxyViewer.tsx +395 -64
  34. package/src/components/providers/ProviderCard.tsx +132 -31
  35. package/src/components/providers/ProviderForm.tsx +43 -11
  36. package/src/components/providers/ProvidersPanel.tsx +1 -1
  37. package/src/components/proxy-viewer/ConversationHeader.tsx +133 -112
  38. package/src/components/proxy-viewer/LogEntry.tsx +109 -26
  39. package/src/components/proxy-viewer/LogEntryHeader.tsx +339 -329
  40. package/src/components/proxy-viewer/ThreadConnector.tsx +49 -46
  41. package/src/components/proxy-viewer/TurnGroup.tsx +22 -62
  42. package/src/components/proxy-viewer/bodyHydration.ts +56 -0
  43. package/src/lib/providerTestContract.ts +56 -10
  44. package/src/lib/providerTestPrompt.ts +23 -1
  45. package/src/lib/upstreamUrl.ts +105 -1
  46. package/src/mcp/server.ts +275 -0
  47. package/src/mcp/toolHandlers.ts +381 -4
  48. package/src/proxy/logSearch.ts +3 -0
  49. package/src/proxy/store.ts +40 -15
  50. package/src/routes/api/logs.ts +8 -2
  51. package/src/routes/api/providers.$providerId.test.log.ts +85 -10
  52. package/.output/public/assets/ProxyViewerContainer-DLqClc9A.js +0 -106
  53. package/.output/public/assets/_sessionId-DUSalzKH.js +0 -1
  54. package/.output/public/assets/index-BPpA21dY.css +0 -1
  55. package/.output/public/assets/index-Bt0Az2I2.js +0 -1
@@ -25,8 +25,10 @@ import {
25
25
  Download,
26
26
  FileJson,
27
27
  Gauge,
28
+ Flag,
28
29
  Plus,
29
30
  Siren,
31
+ Timer,
30
32
  Trash2,
31
33
  Upload,
32
34
  } from "lucide-react";
@@ -38,6 +40,7 @@ import { copyTextToClipboard } from "../lib/clipboard";
38
40
  import type { InspectorGroupEvidence, InspectorGroupMember } from "../lib/groupContract";
39
41
  import type { CaptureMode, TimeDisplayFormat } from "../lib/runtimeConfig";
40
42
  import { formatTimestampRange } from "../lib/timeDisplay";
43
+ import { extractStopReason, type StopReason } from "../lib/stopReason";
41
44
  import { cn, formatContextWindowTokens, formatTokens } from "../lib/utils";
42
45
  import { useProviders } from "../lib/useProviders";
43
46
  import packageJson from "../../package.json";
@@ -56,7 +59,7 @@ import { computeCacheTrends } from "./proxy-viewer/cacheTrend";
56
59
  import { LazyCompareDrawer } from "./proxy-viewer/lazy";
57
60
  import { dispatchLogFocusRequest } from "./proxy-viewer/logFocus";
58
61
  import { ConversationGroupList } from "./proxy-viewer/ConversationGroupList";
59
- import { buildValidPredecessors } from "./proxy-viewer/viewerState";
62
+ import { buildTurnGroups, buildValidPredecessors } from "./proxy-viewer/viewerState";
60
63
  import { useKeyboardNavigation } from "./proxy-viewer/useKeyboardNavigation";
61
64
  import { SegmentBar } from "./proxy-viewer/anatomy/SegmentBar";
62
65
  import type {
@@ -171,16 +174,34 @@ function getFirstUserAgent(logs: CapturedLog[]): string | null {
171
174
  }
172
175
 
173
176
  type SessionSlateStatus = "rolling" | "clear" | "watch" | "failed";
177
+ type SessionSlateMetricTone = "neutral" | "cool" | "watch" | "danger";
178
+
179
+ type ConversationLogs = {
180
+ logs: CapturedLog[];
181
+ };
174
182
 
175
183
  type SessionSlateStats = {
176
184
  status: SessionSlateStatus;
177
185
  statusLabel: string;
178
186
  modelLabel: string;
187
+ providerLabel: string;
179
188
  requests: number;
189
+ turns: number;
180
190
  failures: number;
191
+ pending: number;
181
192
  slow: number;
193
+ lengthStops: number;
182
194
  timeRange: string | null;
183
195
  tokenLabel: string;
196
+ finishLabel: string;
197
+ finishDetail: string;
198
+ finishTone: SessionSlateMetricTone;
199
+ outcomeLabel: string;
200
+ outcomeDetail: string;
201
+ outcomeTone: SessionSlateMetricTone;
202
+ slowestLabel: string;
203
+ slowestDetail: string;
204
+ heaviestDetail: string;
184
205
  };
185
206
 
186
207
  function hasLogFailure(log: CapturedLog): boolean {
@@ -201,25 +222,237 @@ function getModelLabel(logs: readonly CapturedLog[]): string {
201
222
  return `${sorted.length} models`;
202
223
  }
203
224
 
204
- function buildSessionSlateStats({
225
+ function getProviderLabel(logs: readonly CapturedLog[]): string {
226
+ const providers = new Set<string>();
227
+ for (const log of logs) {
228
+ if (
229
+ log.providerName !== null &&
230
+ log.providerName !== undefined &&
231
+ log.providerName.length > 0
232
+ ) {
233
+ providers.add(log.providerName);
234
+ }
235
+ }
236
+ const sorted = [...providers].sort();
237
+ if (sorted.length === 0) return "Provider unknown";
238
+ if (sorted.length === 1) return sorted[0] ?? "Provider unknown";
239
+ return `${sorted.length} providers`;
240
+ }
241
+
242
+ function formatElapsedMs(ms: number): string {
243
+ if (ms < 1000) return `${ms}ms`;
244
+ if (ms < 60_000) {
245
+ const seconds = ms / 1000;
246
+ return `${seconds < 10 ? seconds.toFixed(1) : seconds.toFixed(0)}s`;
247
+ }
248
+ const totalSeconds = Math.round(ms / 1000);
249
+ const minutes = Math.floor(totalSeconds / 60);
250
+ const seconds = totalSeconds % 60;
251
+ return `${minutes}m ${seconds}s`;
252
+ }
253
+
254
+ function formatTokenPair(inputTokens: number, outputTokens: number): string {
255
+ return `${formatTokens(inputTokens)} in / ${formatTokens(outputTokens)} out`;
256
+ }
257
+
258
+ function countSessionTurns(
259
+ logs: CapturedLog[],
260
+ conversationGroups: readonly ConversationLogs[],
261
+ ): number {
262
+ if (logs.length === 0) return 0;
263
+ if (conversationGroups.length === 0) return buildTurnGroups(logs).length;
264
+
265
+ let turns = 0;
266
+ for (const group of conversationGroups) {
267
+ turns += buildTurnGroups(group.logs).length;
268
+ }
269
+ return turns;
270
+ }
271
+
272
+ type FinishSummary = {
273
+ label: string;
274
+ detail: string;
275
+ tone: SessionSlateMetricTone;
276
+ };
277
+
278
+ type SessionOutcomeSummary = {
279
+ label: string;
280
+ detail: string;
281
+ tone: SessionSlateMetricTone;
282
+ };
283
+
284
+ type FailureSummary = {
285
+ logNumber: number;
286
+ status: number | null;
287
+ error: string | null;
288
+ };
289
+
290
+ function summarizeFinishReason({
291
+ stopReason,
292
+ pending,
293
+ lastFailureLogNumber,
294
+ }: {
295
+ stopReason: StopReason;
296
+ pending: number;
297
+ lastFailureLogNumber: number | null;
298
+ }): FinishSummary {
299
+ if (pending > 0) {
300
+ return {
301
+ label: "In flight",
302
+ detail: `${pending} request${pending === 1 ? "" : "s"} pending`,
303
+ tone: "cool",
304
+ };
305
+ }
306
+
307
+ if (lastFailureLogNumber !== null && stopReason === null) {
308
+ return {
309
+ label: "Error",
310
+ detail: `Last failed request #${lastFailureLogNumber}`,
311
+ tone: "danger",
312
+ };
313
+ }
314
+
315
+ switch (stopReason) {
316
+ case "end_turn":
317
+ return { label: "End turn", detail: "Assistant completed the turn", tone: "neutral" };
318
+ case "stop":
319
+ return { label: "Stop", detail: "Model reached a stop condition", tone: "neutral" };
320
+ case "length":
321
+ return { label: "Output limit", detail: "Turn ended at max output tokens", tone: "watch" };
322
+ case "tool_use":
323
+ return { label: "Tool call", detail: "Assistant requested a tool", tone: "cool" };
324
+ case null:
325
+ return { label: "Unknown", detail: "No finish reason captured", tone: "neutral" };
326
+ }
327
+ }
328
+
329
+ function isClientAbortError(error: string | null): boolean {
330
+ if (error === null) return false;
331
+ const lower = error.toLowerCase();
332
+ return (
333
+ lower.includes("abort") ||
334
+ lower.includes("cancel") ||
335
+ lower.includes("client disconnected") ||
336
+ lower.includes("stream disconnected") ||
337
+ lower.includes("connection closed")
338
+ );
339
+ }
340
+
341
+ function summarizeSessionOutcome({
342
+ pending,
343
+ failures,
344
+ lengthStops,
345
+ stopReason,
346
+ lastFailure,
347
+ }: {
348
+ pending: number;
349
+ failures: number;
350
+ lengthStops: number;
351
+ stopReason: StopReason;
352
+ lastFailure: FailureSummary | null;
353
+ }): SessionOutcomeSummary {
354
+ if (
355
+ lastFailure !== null &&
356
+ (lastFailure.status === 499 || isClientAbortError(lastFailure.error))
357
+ ) {
358
+ return {
359
+ label: "Client aborted",
360
+ detail: `Request #${lastFailure.logNumber} ended before completion`,
361
+ tone: "watch",
362
+ };
363
+ }
364
+
365
+ if (pending > 0) {
366
+ return {
367
+ label: "In flight",
368
+ detail: `${pending} request${pending === 1 ? "" : "s"} still running`,
369
+ tone: "cool",
370
+ };
371
+ }
372
+
373
+ if (lastFailure !== null && lastFailure.status !== null && lastFailure.status >= 500) {
374
+ return {
375
+ label: "Provider error",
376
+ detail: `Request #${lastFailure.logNumber} returned HTTP ${lastFailure.status}`,
377
+ tone: "danger",
378
+ };
379
+ }
380
+
381
+ if (failures > 0 && lastFailure !== null) {
382
+ return {
383
+ label: "Failed",
384
+ detail: `Request #${lastFailure.logNumber} needs attention`,
385
+ tone: "danger",
386
+ };
387
+ }
388
+
389
+ if (lengthStops > 0 || stopReason === "length") {
390
+ return {
391
+ label: "Output limited",
392
+ detail: "Model stopped at the output token limit",
393
+ tone: "watch",
394
+ };
395
+ }
396
+
397
+ switch (stopReason) {
398
+ case "end_turn":
399
+ case "stop":
400
+ return { label: "Completed", detail: "Assistant completed normally", tone: "neutral" };
401
+ case "tool_use":
402
+ return { label: "Tool requested", detail: "Assistant handed off to a tool", tone: "cool" };
403
+ case null:
404
+ return { label: "Unknown", detail: "No terminal signal captured", tone: "neutral" };
405
+ }
406
+ }
407
+
408
+ export function buildSessionSlateStats({
205
409
  logs,
410
+ conversationGroups,
206
411
  isLoading,
207
412
  timeDisplayFormat,
208
413
  slowResponseThresholdSeconds,
209
414
  }: {
210
415
  logs: CapturedLog[];
416
+ conversationGroups: readonly ConversationLogs[];
211
417
  isLoading: boolean;
212
418
  timeDisplayFormat: TimeDisplayFormat;
213
419
  slowResponseThresholdSeconds: number;
214
420
  }): SessionSlateStats {
215
421
  let failures = 0;
422
+ let pending = 0;
216
423
  let slow = 0;
424
+ let lengthStops = 0;
217
425
  let totalIn = 0;
218
426
  let totalOut = 0;
427
+ let terminalStopReason: StopReason = null;
428
+ let lastFailureLogNumber: number | null = null;
429
+ let lastFailure: FailureSummary | null = null;
430
+ let slowestMs: number | null = null;
431
+ let slowestLogNumber: number | null = null;
432
+ let heaviestTokens = 0;
433
+ let heaviestInputTokens = 0;
434
+ let heaviestOutputTokens = 0;
435
+ let heaviestLogNumber: number | null = null;
219
436
  const slowThresholdMs = slowResponseThresholdSeconds * 1000;
220
437
 
221
- for (const log of logs) {
222
- if (hasLogFailure(log)) failures += 1;
438
+ for (let index = 0; index < logs.length; index += 1) {
439
+ const log = logs[index];
440
+ if (log === undefined) continue;
441
+ const logNumber = index + 1;
442
+ const stopReason = extractStopReason(log);
443
+ if (stopReason !== null) terminalStopReason = stopReason;
444
+ if (stopReason === "length") lengthStops += 1;
445
+
446
+ if (hasLogFailure(log)) {
447
+ failures += 1;
448
+ lastFailureLogNumber = logNumber;
449
+ lastFailure = {
450
+ logNumber,
451
+ status: log.responseStatus,
452
+ error: log.error ?? null,
453
+ };
454
+ }
455
+ if (log.responseStatus === null) pending += 1;
223
456
  if (
224
457
  slowResponseThresholdSeconds > 0 &&
225
458
  log.elapsedMs !== null &&
@@ -229,15 +462,30 @@ function buildSessionSlateStats({
229
462
  }
230
463
  if (log.inputTokens !== null) totalIn += log.inputTokens;
231
464
  if (log.outputTokens !== null) totalOut += log.outputTokens;
465
+ if (log.elapsedMs !== null && (slowestMs === null || log.elapsedMs > slowestMs)) {
466
+ slowestMs = log.elapsedMs;
467
+ slowestLogNumber = logNumber;
468
+ }
469
+
470
+ const inputTokens = log.inputTokens ?? 0;
471
+ const outputTokens = log.outputTokens ?? 0;
472
+ const requestTokens = inputTokens + outputTokens;
473
+ if (requestTokens > heaviestTokens) {
474
+ heaviestTokens = requestTokens;
475
+ heaviestInputTokens = inputTokens;
476
+ heaviestOutputTokens = outputTokens;
477
+ heaviestLogNumber = logNumber;
478
+ }
232
479
  }
233
480
 
234
- const status: SessionSlateStatus = isLoading
235
- ? "rolling"
236
- : failures > 0
237
- ? "failed"
238
- : slow > 0
239
- ? "watch"
240
- : "clear";
481
+ const status: SessionSlateStatus =
482
+ isLoading || (pending > 0 && failures === 0)
483
+ ? "rolling"
484
+ : failures > 0
485
+ ? "failed"
486
+ : slow > 0 || lengthStops > 0
487
+ ? "watch"
488
+ : "clear";
241
489
  const statusLabel =
242
490
  status === "rolling"
243
491
  ? "Rolling"
@@ -246,87 +494,169 @@ function buildSessionSlateStats({
246
494
  : status === "watch"
247
495
  ? "Watch"
248
496
  : "Clear";
497
+ const finish = summarizeFinishReason({
498
+ stopReason: terminalStopReason,
499
+ pending,
500
+ lastFailureLogNumber,
501
+ });
502
+ const outcome = summarizeSessionOutcome({
503
+ pending,
504
+ failures,
505
+ lengthStops,
506
+ stopReason: terminalStopReason,
507
+ lastFailure,
508
+ });
249
509
 
250
510
  return {
251
511
  status,
252
512
  statusLabel,
253
513
  modelLabel: getModelLabel(logs),
514
+ providerLabel: getProviderLabel(logs),
254
515
  requests: logs.length,
516
+ turns: countSessionTurns(logs, conversationGroups),
255
517
  failures,
518
+ pending,
256
519
  slow,
520
+ lengthStops,
257
521
  timeRange: formatTimeRange(logs, timeDisplayFormat),
258
522
  tokenLabel: `${formatTokens(totalIn)} in / ${formatTokens(totalOut)} out`,
523
+ finishLabel: finish.label,
524
+ finishDetail: finish.detail,
525
+ finishTone: finish.tone,
526
+ outcomeLabel: outcome.label,
527
+ outcomeDetail: outcome.detail,
528
+ outcomeTone: outcome.tone,
529
+ slowestLabel: slowestMs === null ? "n/a" : formatElapsedMs(slowestMs),
530
+ slowestDetail: slowestLogNumber === null ? "Latency pending" : `Request #${slowestLogNumber}`,
531
+ heaviestDetail:
532
+ heaviestLogNumber === null
533
+ ? "Tokens pending"
534
+ : `#${heaviestLogNumber} ${formatTokenPair(heaviestInputTokens, heaviestOutputTokens)}`,
259
535
  };
260
536
  }
261
537
 
262
- function slateStatusClass(status: SessionSlateStatus): string {
538
+ function slateMetricToneClass(tone: SessionSlateMetricTone): string {
539
+ switch (tone) {
540
+ case "cool":
541
+ return "text-cyan-100";
542
+ case "danger":
543
+ return "text-red-100";
544
+ case "watch":
545
+ return "text-amber-100";
546
+ case "neutral":
547
+ return "text-foreground";
548
+ }
549
+ }
550
+
551
+ function statusMetricTone(status: SessionSlateStatus): SessionSlateMetricTone {
263
552
  switch (status) {
264
553
  case "rolling":
265
- return "border-cyan-400/35 bg-cyan-500/10 text-cyan-200";
554
+ return "cool";
266
555
  case "failed":
267
- return "border-red-400/35 bg-red-500/10 text-red-100";
556
+ return "danger";
268
557
  case "watch":
269
- return "border-amber-300/35 bg-amber-400/10 text-amber-100";
558
+ return "watch";
270
559
  case "clear":
271
- return "border-slate-300/20 bg-slate-300/[0.08] text-slate-100";
560
+ return "neutral";
272
561
  }
273
562
  }
274
563
 
564
+ function requestHealthDetail(stats: SessionSlateStats): string {
565
+ if (stats.pending > 0) return `${stats.pending} pending / ${stats.failures} failed`;
566
+ if (stats.lengthStops > 0) return `${stats.lengthStops} output limit / ${stats.failures} failed`;
567
+ return `${stats.failures} failed / ${stats.slow} slow`;
568
+ }
569
+
570
+ function SessionSlateMetric({
571
+ label,
572
+ value,
573
+ detail,
574
+ icon,
575
+ tone = "neutral",
576
+ }: {
577
+ label: string;
578
+ value: string;
579
+ detail: string;
580
+ icon?: ReactNode;
581
+ tone?: SessionSlateMetricTone;
582
+ }): JSX.Element {
583
+ return (
584
+ <div className="min-w-0 border-t border-white/10 px-4 py-3">
585
+ <div className="flex items-center gap-1 text-[10px] uppercase text-muted-foreground">
586
+ {icon}
587
+ {label}
588
+ </div>
589
+ <div className={cn("mt-2 font-mono text-sm font-semibold", slateMetricToneClass(tone))}>
590
+ {value}
591
+ </div>
592
+ <div className="mt-1 font-mono text-[11px] leading-snug text-muted-foreground">{detail}</div>
593
+ </div>
594
+ );
595
+ }
596
+
275
597
  function SessionOpeningSlate({ stats }: { stats: SessionSlateStats }): JSX.Element | null {
276
598
  if (stats.requests === 0) return null;
277
599
 
278
600
  return (
279
- <div className="border border-border bg-card shadow-sm mb-4 rounded-[8px]">
280
- <div className="grid grid-cols-2 gap-0 md:grid-cols-[minmax(220px,1.2fr)_repeat(4,minmax(120px,1fr))]">
281
- <div className="col-span-2 border-b border-white/10 px-4 py-3 md:col-span-1 md:border-b-0 md:border-r">
282
- <div className="flex items-center gap-2 text-[10px] font-semibold uppercase text-muted-foreground">
283
- <Clapperboard className="size-3.5 text-cyan-300" />
284
- Opening Slate
285
- </div>
286
- <div className="mt-2 min-w-0 truncate font-mono text-sm font-semibold text-foreground">
287
- {stats.modelLabel}
288
- </div>
289
- <div className="mt-1 truncate font-mono text-[11px] text-muted-foreground">
290
- {stats.timeRange ?? "Timeline pending"}
291
- </div>
292
- </div>
293
- <div className="border-r border-b border-white/10 px-4 py-3 md:border-b-0">
294
- <div className="text-[10px] uppercase text-muted-foreground">Status</div>
295
- <div
296
- className={cn(
297
- "mt-2 inline-flex h-7 items-center rounded-md border px-2.5 font-mono text-xs font-semibold",
298
- slateStatusClass(stats.status),
299
- )}
300
- >
301
- {stats.statusLabel}
302
- </div>
303
- </div>
304
- <div className="border-b border-white/10 px-4 py-3 md:border-b-0 md:border-r">
305
- <div className="text-[10px] uppercase text-muted-foreground">Requests</div>
306
- <div className="mt-2 font-mono text-xl font-semibold text-foreground">
307
- {stats.requests}
308
- </div>
309
- </div>
310
- <div className="border-r border-white/10 px-4 py-3 md:border-r">
311
- <div className="flex items-center gap-1 text-[10px] uppercase text-muted-foreground">
312
- <Siren className="size-3 text-red-300" />
313
- Failure
314
- </div>
315
- <div className="mt-2 font-mono text-xl font-semibold text-red-100">{stats.failures}</div>
316
- </div>
317
- <div className="px-4 py-3">
318
- <div className="flex items-center gap-1 text-[10px] uppercase text-muted-foreground">
319
- <Gauge className="size-3 text-amber-200" />
320
- Tempo
321
- </div>
322
- <div className="mt-2 font-mono text-sm font-semibold text-foreground">
323
- {stats.slow} slow
601
+ <div className="border border-border bg-card shadow-sm mb-4 overflow-hidden rounded-[8px]">
602
+ <div className="px-4 py-3">
603
+ <div className="flex flex-wrap items-end justify-between gap-3">
604
+ <div className="min-w-0">
605
+ <div className="flex items-center gap-2 text-[10px] font-semibold uppercase text-muted-foreground">
606
+ <Clapperboard className="size-3.5 text-slate-300" />
607
+ Flight Recorder
608
+ </div>
609
+ <div className="mt-2 min-w-0 font-mono text-sm font-semibold text-foreground">
610
+ {stats.modelLabel}
611
+ </div>
324
612
  </div>
325
- <div className="mt-1 truncate font-mono text-[11px] text-muted-foreground">
326
- {stats.tokenLabel}
613
+ <div className="flex min-w-0 flex-wrap items-center gap-x-4 gap-y-1 text-[11px] text-muted-foreground">
614
+ <span className="min-w-0 font-mono">{stats.providerLabel}</span>
615
+ <span className="min-w-0 font-mono">{stats.timeRange ?? "Timeline pending"}</span>
327
616
  </div>
328
617
  </div>
329
618
  </div>
619
+ <div className="grid grid-cols-2 border-t border-white/10 md:grid-cols-3 2xl:grid-cols-6">
620
+ <SessionSlateMetric
621
+ label="Status"
622
+ value={stats.statusLabel}
623
+ detail={requestHealthDetail(stats)}
624
+ tone={statusMetricTone(stats.status)}
625
+ />
626
+ <SessionSlateMetric
627
+ label="Turns"
628
+ value={String(stats.turns)}
629
+ detail={stats.finishLabel}
630
+ icon={<Flag className="size-3 text-muted-foreground" />}
631
+ tone={stats.finishTone}
632
+ />
633
+ <SessionSlateMetric
634
+ label="Requests"
635
+ value={String(stats.requests)}
636
+ detail={`${stats.failures} failed / ${stats.slow} slow`}
637
+ icon={<Siren className="size-3 text-muted-foreground" />}
638
+ tone={stats.failures > 0 ? "danger" : "neutral"}
639
+ />
640
+ <SessionSlateMetric
641
+ label="Peak"
642
+ value={stats.slowestLabel}
643
+ detail={stats.slowestDetail}
644
+ icon={<Timer className="size-3 text-muted-foreground" />}
645
+ tone={stats.slow > 0 ? "watch" : "neutral"}
646
+ />
647
+ <SessionSlateMetric
648
+ label="Tokens"
649
+ value={stats.tokenLabel}
650
+ detail={stats.heaviestDetail}
651
+ icon={<Gauge className="size-3 text-muted-foreground" />}
652
+ />
653
+ <SessionSlateMetric
654
+ label="Outcome"
655
+ value={stats.outcomeLabel}
656
+ detail={stats.outcomeDetail}
657
+ tone={stats.outcomeTone}
658
+ />
659
+ </div>
330
660
  </div>
331
661
  );
332
662
  }
@@ -1503,11 +1833,12 @@ export function ProxyViewer({
1503
1833
  () =>
1504
1834
  buildSessionSlateStats({
1505
1835
  logs,
1836
+ conversationGroups: groups,
1506
1837
  isLoading,
1507
1838
  timeDisplayFormat,
1508
1839
  slowResponseThresholdSeconds,
1509
1840
  }),
1510
- [isLoading, logs, slowResponseThresholdSeconds, timeDisplayFormat],
1841
+ [groups, isLoading, logs, slowResponseThresholdSeconds, timeDisplayFormat],
1511
1842
  );
1512
1843
  const handleCompareWithPrevious = useCallback(
1513
1844
  (log: CapturedLog) => {