@tonyclaw/agent-inspector 2.1.15 → 2.1.17

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 (73) hide show
  1. package/.output/cli.js +57 -13
  2. package/.output/nitro.json +1 -1
  3. package/.output/public/assets/{CompareDrawer-BcEd6V-V.js → CompareDrawer-B9sLBHw5.js} +1 -1
  4. package/.output/public/assets/ProxyViewerContainer-DbWjc_BE.js +106 -0
  5. package/.output/public/assets/{ReplayDialog-BNpC0548.js → ReplayDialog-D9I9W9n3.js} +1 -1
  6. package/.output/public/assets/{RequestAnatomy-Ds1uRLVB.js → RequestAnatomy-DGXK_Rii.js} +1 -1
  7. package/.output/public/assets/{ResponseView-7KPVqKl5.js → ResponseView-df_JzwsS.js} +2 -2
  8. package/.output/public/assets/{StreamingChunkSequence-BHQT261s.js → StreamingChunkSequence-i3DM5IKL.js} +1 -1
  9. package/.output/public/assets/_sessionId-CTpCHdFh.js +1 -0
  10. package/.output/public/assets/index-Blqvndcn.js +1 -0
  11. package/.output/public/assets/index-Cs79WRQj.css +1 -0
  12. package/.output/public/assets/{index-DjKt8XKe.js → index-Dqc2r1ea.js} +1 -1
  13. package/.output/public/assets/{json-viewer-CkCu-rka.js → json-viewer-D3QWQlSB.js} +1 -1
  14. package/.output/public/assets/{main-DpD1N0S8.js → main-ZhxhOCmF.js} +2 -2
  15. package/.output/server/_libs/lucide-react.mjs +255 -201
  16. package/.output/server/{_sessionId-DF9Sy8cP.mjs → _sessionId-YFrP0lZP.mjs} +2 -2
  17. package/.output/server/_ssr/{CompareDrawer-BoxztaO7.mjs → CompareDrawer-qC3adEeq.mjs} +3 -3
  18. package/.output/server/_ssr/{ProxyViewerContainer-CRBkqFlJ.mjs → ProxyViewerContainer-DCZIJC2b.mjs} +2732 -1849
  19. package/.output/server/_ssr/{ReplayDialog-Cc1dyDuK.mjs → ReplayDialog-CvX4XYCi.mjs} +4 -4
  20. package/.output/server/_ssr/{RequestAnatomy-CMGSsz5Z.mjs → RequestAnatomy-HQfrxjO1.mjs} +3 -3
  21. package/.output/server/_ssr/{ResponseView-Cp10DM1D.mjs → ResponseView-QSlSD11e.mjs} +3 -3
  22. package/.output/server/_ssr/{StreamingChunkSequence-B1VGxy3A.mjs → StreamingChunkSequence-DTAiwlnI.mjs} +3 -3
  23. package/.output/server/_ssr/{index-47XVPghS.mjs → index-D_EzfXaN.mjs} +2 -2
  24. package/.output/server/_ssr/index.mjs +2 -2
  25. package/.output/server/_ssr/{json-viewer-zDE2rrmJ.mjs → json-viewer-CrIX0e0Q.mjs} +3 -3
  26. package/.output/server/_ssr/{router-DVeuZFqI.mjs → router-__o2wrfO.mjs} +505 -136
  27. package/.output/server/{_tanstack-start-manifest_v-Bp8JxtPW.mjs → _tanstack-start-manifest_v-DbqyLpsI.mjs} +1 -1
  28. package/.output/server/index.mjs +63 -63
  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 +535 -163
  34. package/src/components/ProxyViewerContainer.tsx +77 -67
  35. package/src/components/alerts/AlertsDialog.tsx +2 -1
  36. package/src/components/clients/ClientLogo.tsx +1 -1
  37. package/src/components/groups/GroupsDialog.tsx +19 -1
  38. package/src/components/providers/ProviderCard.tsx +133 -32
  39. package/src/components/providers/ProviderForm.tsx +61 -29
  40. package/src/components/providers/ProviderLogo.tsx +6 -1
  41. package/src/components/providers/ProvidersPanel.tsx +7 -7
  42. package/src/components/providers/SettingsDialog.tsx +13 -6
  43. package/src/components/proxy-viewer/AgentTraceSummary.tsx +6 -6
  44. package/src/components/proxy-viewer/ConversationGroup.tsx +43 -32
  45. package/src/components/proxy-viewer/ConversationGroupList.tsx +452 -89
  46. package/src/components/proxy-viewer/ConversationHeader.tsx +153 -120
  47. package/src/components/proxy-viewer/LogEntry.tsx +130 -43
  48. package/src/components/proxy-viewer/LogEntryHeader.tsx +357 -354
  49. package/src/components/proxy-viewer/ProviderLogoStack.tsx +2 -2
  50. package/src/components/proxy-viewer/RequestToolsPanel.tsx +9 -9
  51. package/src/components/proxy-viewer/ThreadConnector.tsx +102 -83
  52. package/src/components/proxy-viewer/TurnGroup.tsx +33 -69
  53. package/src/components/proxy-viewer/TurnGroupList.tsx +3 -0
  54. package/src/components/proxy-viewer/bodyHydration.ts +56 -0
  55. package/src/components/proxy-viewer/viewerState.ts +0 -4
  56. package/src/components/ui/icon-trigger.ts +2 -0
  57. package/src/components/ui/transient-toast.tsx +104 -0
  58. package/src/lib/providerTestContract.ts +56 -10
  59. package/src/lib/providerTestPrompt.ts +23 -1
  60. package/src/lib/sessionInfoContract.ts +4 -0
  61. package/src/lib/upstreamUrl.ts +105 -1
  62. package/src/mcp/server.ts +71 -27
  63. package/src/mcp/toolHandlers.ts +24 -5
  64. package/src/proxy/formats/openai/alibabaProvider.ts +0 -4
  65. package/src/proxy/sessionInfo.ts +31 -2
  66. package/src/proxy/sessionSupervisor.ts +20 -1
  67. package/src/proxy/store.ts +1 -1
  68. package/src/routes/api/providers.$providerId.test.log.ts +93 -14
  69. package/styles/globals.css +180 -180
  70. package/.output/public/assets/ProxyViewerContainer-h851qWNp.js +0 -106
  71. package/.output/public/assets/_sessionId-DWePGjnS.js +0 -1
  72. package/.output/public/assets/index-CI1-G8ua.js +0 -1
  73. package/.output/public/assets/index-DdhFqPsI.css +0 -1
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  type ChangeEvent,
3
3
  type JSX,
4
+ type ReactNode,
4
5
  useCallback,
5
6
  useEffect,
6
7
  useMemo,
@@ -24,8 +25,10 @@ import {
24
25
  Download,
25
26
  FileJson,
26
27
  Gauge,
28
+ Flag,
27
29
  Plus,
28
30
  Siren,
31
+ Timer,
29
32
  Trash2,
30
33
  Upload,
31
34
  } from "lucide-react";
@@ -37,6 +40,7 @@ import { copyTextToClipboard } from "../lib/clipboard";
37
40
  import type { InspectorGroupEvidence, InspectorGroupMember } from "../lib/groupContract";
38
41
  import type { CaptureMode, TimeDisplayFormat } from "../lib/runtimeConfig";
39
42
  import { formatTimestampRange } from "../lib/timeDisplay";
43
+ import { extractStopReason, type StopReason } from "../lib/stopReason";
40
44
  import { cn, formatContextWindowTokens, formatTokens } from "../lib/utils";
41
45
  import { useProviders } from "../lib/useProviders";
42
46
  import packageJson from "../../package.json";
@@ -47,6 +51,7 @@ import { crabVariants } from "./ui/crab-variants";
47
51
  import { McpLogo } from "./ui/mcp-logo";
48
52
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
49
53
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
54
+ import type { TransientToastInput } from "./ui/transient-toast";
50
55
  import { AlertsDialog } from "./alerts/AlertsDialog";
51
56
  import { GroupsDialog } from "./groups/GroupsDialog";
52
57
  import { SettingsDialog } from "./providers/SettingsDialog";
@@ -54,7 +59,7 @@ import { computeCacheTrends } from "./proxy-viewer/cacheTrend";
54
59
  import { LazyCompareDrawer } from "./proxy-viewer/lazy";
55
60
  import { dispatchLogFocusRequest } from "./proxy-viewer/logFocus";
56
61
  import { ConversationGroupList } from "./proxy-viewer/ConversationGroupList";
57
- import { buildValidPredecessors } from "./proxy-viewer/viewerState";
62
+ import { buildTurnGroups, buildValidPredecessors } from "./proxy-viewer/viewerState";
58
63
  import { useKeyboardNavigation } from "./proxy-viewer/useKeyboardNavigation";
59
64
  import { SegmentBar } from "./proxy-viewer/anatomy/SegmentBar";
60
65
  import type {
@@ -169,16 +174,34 @@ function getFirstUserAgent(logs: CapturedLog[]): string | null {
169
174
  }
170
175
 
171
176
  type SessionSlateStatus = "rolling" | "clear" | "watch" | "failed";
177
+ type SessionSlateMetricTone = "neutral" | "cool" | "watch" | "danger";
178
+
179
+ type ConversationLogs = {
180
+ logs: CapturedLog[];
181
+ };
172
182
 
173
183
  type SessionSlateStats = {
174
184
  status: SessionSlateStatus;
175
185
  statusLabel: string;
176
186
  modelLabel: string;
187
+ providerLabel: string;
177
188
  requests: number;
189
+ turns: number;
178
190
  failures: number;
191
+ pending: number;
179
192
  slow: number;
193
+ lengthStops: number;
180
194
  timeRange: string | null;
181
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;
182
205
  };
183
206
 
184
207
  function hasLogFailure(log: CapturedLog): boolean {
@@ -199,25 +222,237 @@ function getModelLabel(logs: readonly CapturedLog[]): string {
199
222
  return `${sorted.length} models`;
200
223
  }
201
224
 
202
- 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({
203
409
  logs,
410
+ conversationGroups,
204
411
  isLoading,
205
412
  timeDisplayFormat,
206
413
  slowResponseThresholdSeconds,
207
414
  }: {
208
415
  logs: CapturedLog[];
416
+ conversationGroups: readonly ConversationLogs[];
209
417
  isLoading: boolean;
210
418
  timeDisplayFormat: TimeDisplayFormat;
211
419
  slowResponseThresholdSeconds: number;
212
420
  }): SessionSlateStats {
213
421
  let failures = 0;
422
+ let pending = 0;
214
423
  let slow = 0;
424
+ let lengthStops = 0;
215
425
  let totalIn = 0;
216
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;
217
436
  const slowThresholdMs = slowResponseThresholdSeconds * 1000;
218
437
 
219
- for (const log of logs) {
220
- 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;
221
456
  if (
222
457
  slowResponseThresholdSeconds > 0 &&
223
458
  log.elapsedMs !== null &&
@@ -227,15 +462,30 @@ function buildSessionSlateStats({
227
462
  }
228
463
  if (log.inputTokens !== null) totalIn += log.inputTokens;
229
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
+ }
230
479
  }
231
480
 
232
- const status: SessionSlateStatus = isLoading
233
- ? "rolling"
234
- : failures > 0
235
- ? "failed"
236
- : slow > 0
237
- ? "watch"
238
- : "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";
239
489
  const statusLabel =
240
490
  status === "rolling"
241
491
  ? "Rolling"
@@ -244,87 +494,169 @@ function buildSessionSlateStats({
244
494
  : status === "watch"
245
495
  ? "Watch"
246
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
+ });
247
509
 
248
510
  return {
249
511
  status,
250
512
  statusLabel,
251
513
  modelLabel: getModelLabel(logs),
514
+ providerLabel: getProviderLabel(logs),
252
515
  requests: logs.length,
516
+ turns: countSessionTurns(logs, conversationGroups),
253
517
  failures,
518
+ pending,
254
519
  slow,
520
+ lengthStops,
255
521
  timeRange: formatTimeRange(logs, timeDisplayFormat),
256
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)}`,
257
535
  };
258
536
  }
259
537
 
260
- 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 {
261
552
  switch (status) {
262
553
  case "rolling":
263
- return "border-cyan-400/35 bg-cyan-500/10 text-cyan-200";
554
+ return "cool";
264
555
  case "failed":
265
- return "border-red-400/35 bg-red-500/10 text-red-100";
556
+ return "danger";
266
557
  case "watch":
267
- return "border-amber-300/35 bg-amber-400/10 text-amber-100";
558
+ return "watch";
268
559
  case "clear":
269
- return "border-emerald-400/30 bg-emerald-500/10 text-emerald-100";
560
+ return "neutral";
270
561
  }
271
562
  }
272
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
+
273
597
  function SessionOpeningSlate({ stats }: { stats: SessionSlateStats }): JSX.Element | null {
274
598
  if (stats.requests === 0) return null;
275
599
 
276
600
  return (
277
- <div className="mb-4 rounded-md border border-border bg-[#0c0d0f] shadow-[0_14px_50px_rgba(0,0,0,0.25)]">
278
- <div className="grid grid-cols-2 gap-0 md:grid-cols-[minmax(220px,1.2fr)_repeat(4,minmax(120px,1fr))]">
279
- <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">
280
- <div className="flex items-center gap-2 text-[10px] font-semibold uppercase text-muted-foreground">
281
- <Clapperboard className="size-3.5 text-cyan-300" />
282
- Opening Slate
283
- </div>
284
- <div className="mt-2 min-w-0 truncate font-mono text-sm font-semibold text-foreground">
285
- {stats.modelLabel}
286
- </div>
287
- <div className="mt-1 truncate font-mono text-[11px] text-muted-foreground">
288
- {stats.timeRange ?? "Timeline pending"}
289
- </div>
290
- </div>
291
- <div className="border-r border-b border-border/70 px-4 py-3 md:border-b-0">
292
- <div className="text-[10px] uppercase text-muted-foreground">Status</div>
293
- <div
294
- className={cn(
295
- "mt-2 inline-flex h-7 items-center rounded-md border px-2.5 font-mono text-xs font-semibold",
296
- slateStatusClass(stats.status),
297
- )}
298
- >
299
- {stats.statusLabel}
300
- </div>
301
- </div>
302
- <div className="border-b border-border/70 px-4 py-3 md:border-b-0 md:border-r">
303
- <div className="text-[10px] uppercase text-muted-foreground">Requests</div>
304
- <div className="mt-2 font-mono text-xl font-semibold text-foreground">
305
- {stats.requests}
306
- </div>
307
- </div>
308
- <div className="border-r border-border/70 px-4 py-3 md:border-r">
309
- <div className="flex items-center gap-1 text-[10px] uppercase text-muted-foreground">
310
- <Siren className="size-3 text-red-300" />
311
- Failure
312
- </div>
313
- <div className="mt-2 font-mono text-xl font-semibold text-red-100">{stats.failures}</div>
314
- </div>
315
- <div className="px-4 py-3">
316
- <div className="flex items-center gap-1 text-[10px] uppercase text-muted-foreground">
317
- <Gauge className="size-3 text-amber-200" />
318
- Tempo
319
- </div>
320
- <div className="mt-2 font-mono text-sm font-semibold text-foreground">
321
- {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>
322
612
  </div>
323
- <div className="mt-1 truncate font-mono text-[11px] text-muted-foreground">
324
- {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>
325
616
  </div>
326
617
  </div>
327
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>
328
660
  </div>
329
661
  );
330
662
  }
@@ -411,7 +743,7 @@ function sessionRiskBarClass(risk: ContextRiskLevel): string {
411
743
  case "unknown":
412
744
  return "bg-muted-foreground/50";
413
745
  case "ok":
414
- return "bg-emerald-400";
746
+ return "bg-cyan-300";
415
747
  }
416
748
  }
417
749
 
@@ -470,7 +802,7 @@ function evidenceChipClass(value: string): string {
470
802
  normalized.includes("complete") ||
471
803
  normalized.includes("accept")
472
804
  ) {
473
- return "border-emerald-400/30 bg-emerald-500/10 text-emerald-100";
805
+ return "border-cyan-300/25 bg-cyan-400/10 text-cyan-100";
474
806
  }
475
807
  return "border-border/70 bg-background/70 text-muted-foreground";
476
808
  }
@@ -513,11 +845,11 @@ function SessionMembershipPanel({
513
845
  );
514
846
 
515
847
  return (
516
- <div className="mb-3 overflow-hidden rounded-md border border-border bg-[#0c0d0f] text-xs">
848
+ <div className="border border-border bg-card shadow-sm mb-3 rounded-[8px] text-xs">
517
849
  <button
518
850
  type="button"
519
851
  onClick={() => setOpen((value) => !value)}
520
- className="flex w-full items-center gap-3 px-3 py-2 text-left transition-colors hover:bg-muted/30"
852
+ className="flex w-full items-center gap-3 px-3 py-2 text-left transition-colors hover:bg-white/[0.04]"
521
853
  aria-expanded={open}
522
854
  >
523
855
  <div className="min-w-0 flex-1">
@@ -555,7 +887,7 @@ function SessionMembershipPanel({
555
887
  )}
556
888
  </button>
557
889
  {open && (
558
- <div className="space-y-2 border-t border-border/70 px-3 py-2">
890
+ <div className="space-y-2 border-t border-white/10 px-3 py-2">
559
891
  {memberships.map((membership) => {
560
892
  const member = membership.member;
561
893
  const metadataRows = getMemberMetadataRows(member);
@@ -595,7 +927,7 @@ function SessionMembershipPanel({
595
927
  {metadataRows.map((row) => (
596
928
  <div
597
929
  key={row.key}
598
- className="min-w-0 rounded border border-border/60 px-2 py-1"
930
+ className="border border-border bg-muted/40 min-w-0 rounded-md px-2 py-1"
599
931
  >
600
932
  <div className="font-mono text-[10px] text-muted-foreground">{row.key}</div>
601
933
  <div
@@ -653,7 +985,7 @@ function LogPaginationButton({
653
985
  type="button"
654
986
  onClick={onClick}
655
987
  disabled={disabled}
656
- 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"
988
+ className="border border-input bg-background hover:bg-accent hover:text-accent-foreground inline-flex h-8 items-center gap-1.5 rounded-md px-2.5 text-xs text-muted-foreground transition-colors hover:text-foreground disabled:cursor-not-allowed disabled:opacity-45"
657
989
  title={title}
658
990
  >
659
991
  {icon}
@@ -679,8 +1011,8 @@ function LogPaginationBar({
679
1011
  className={cn(
680
1012
  "flex min-h-9 flex-col gap-2 px-1 py-1 sm:flex-row sm:items-center",
681
1013
  embedded
682
- ? "border-t border-border/70 pt-2"
683
- : "mb-3 rounded-md border border-border bg-muted/10 px-2",
1014
+ ? "border-t border-white/10 pt-2"
1015
+ : "border border-border bg-card shadow-sm mb-3 rounded-[8px] px-2",
684
1016
  )}
685
1017
  >
686
1018
  <div className="w-full min-w-0 px-1 font-mono text-[11px] text-muted-foreground sm:flex-1">
@@ -738,7 +1070,7 @@ function sessionRiskClass(risk: ContextRiskLevel): string {
738
1070
  case "unknown":
739
1071
  return "text-muted-foreground";
740
1072
  case "ok":
741
- return "text-emerald-300";
1073
+ return "text-cyan-200";
742
1074
  }
743
1075
  }
744
1076
 
@@ -749,7 +1081,7 @@ function sessionHealthClass(level: ContextHealthLevel): string {
749
1081
  case "optimizable":
750
1082
  return "border-amber-400/25 bg-amber-400/8 text-amber-100";
751
1083
  case "ok":
752
- return "border-emerald-400/25 bg-emerald-400/8 text-emerald-100";
1084
+ return "border-cyan-300/25 bg-cyan-400/8 text-cyan-100";
753
1085
  }
754
1086
  }
755
1087
 
@@ -812,12 +1144,12 @@ function CopyableCommand({ command }: { command: string }): JSX.Element {
812
1144
  }, [command]);
813
1145
 
814
1146
  return (
815
- <div className="inline-flex items-center gap-2 bg-muted rounded-md px-3 py-2">
816
- <pre className="text-blue-500 font-mono text-sm m-0">{command}</pre>
1147
+ <div className="border border-border bg-muted/40 inline-flex max-w-full items-center gap-2 rounded-md px-3 py-2">
1148
+ <pre className="m-0 overflow-x-auto font-mono text-sm text-cyan-200">{command}</pre>
817
1149
  <button
818
1150
  type="button"
819
1151
  onClick={handleCopy}
820
- className="text-muted-foreground hover:text-foreground transition-colors shrink-0 cursor-pointer"
1152
+ className="shrink-0 cursor-pointer text-muted-foreground transition-colors hover:text-foreground"
821
1153
  aria-label="Copy command"
822
1154
  >
823
1155
  {copied ? <Check className="size-4" /> : <Copy className="size-4" />}
@@ -831,8 +1163,8 @@ function McpReadyBadge(): JSX.Element {
831
1163
  <TooltipProvider>
832
1164
  <Tooltip>
833
1165
  <TooltipTrigger asChild>
834
- <span className="inline-flex h-7 items-center gap-2 rounded-md border border-cyan-400/30 bg-cyan-500/10 px-2.5 font-mono text-[11px] font-medium text-cyan-300 shadow-[0_0_16px_rgba(34,211,238,0.08)]">
835
- <span className="size-1.5 rounded-full bg-emerald-300 shadow-[0_0_8px_rgba(110,231,183,0.8)]" />
1166
+ <span className="border border-border bg-muted/40 inline-flex h-7 items-center gap-2 rounded-md px-2.5 font-mono text-[11px] font-medium text-cyan-200">
1167
+ <span className="size-1.5 rounded-full bg-cyan-200 shadow-[0_0_8px_rgba(125,211,252,0.75)]" />
836
1168
  MCP Ready
837
1169
  <span className="hidden text-cyan-200/70 sm:inline">/api/mcp</span>
838
1170
  </span>
@@ -846,6 +1178,40 @@ function McpReadyBadge(): JSX.Element {
846
1178
  );
847
1179
  }
848
1180
 
1181
+ function SidebarPanel({
1182
+ label,
1183
+ children,
1184
+ className,
1185
+ }: {
1186
+ label: string;
1187
+ children: ReactNode;
1188
+ className?: string;
1189
+ }): JSX.Element {
1190
+ return (
1191
+ <section className={cn("border border-border bg-card shadow-sm rounded-[8px]", className)}>
1192
+ <div className="border-b border-white/10 px-3 py-2">
1193
+ <div className="font-mono text-[10px] font-semibold uppercase tracking-[0.14em] text-white/40">
1194
+ {label}
1195
+ </div>
1196
+ </div>
1197
+ <div className="p-3">{children}</div>
1198
+ </section>
1199
+ );
1200
+ }
1201
+
1202
+ function SidebarMetric({ label, value }: { label: string; value: string | number }): JSX.Element {
1203
+ return (
1204
+ <div className="border border-border bg-muted/40 min-w-0 rounded-md px-2 py-2">
1205
+ <div className="truncate font-mono text-lg font-semibold leading-6 text-slate-100">
1206
+ {value}
1207
+ </div>
1208
+ <div className="mt-0.5 truncate font-mono text-[10px] uppercase tracking-[0.12em] text-white/35">
1209
+ {label}
1210
+ </div>
1211
+ </div>
1212
+ );
1213
+ }
1214
+
849
1215
  const CRAB_VARIANT_COLORS = [
850
1216
  "text-amber-500",
851
1217
  "text-rose-500",
@@ -909,7 +1275,7 @@ function BrandHeader({
909
1275
  }): JSX.Element {
910
1276
  if (compact) {
911
1277
  return (
912
- <div className="pb-3">
1278
+ <div className="border border-border bg-card shadow-sm rounded-[8px] p-3">
913
1279
  <div className="flex items-start justify-between gap-3">
914
1280
  <div className="flex min-w-0 items-start gap-2">
915
1281
  <CrabFamily compact={true} entrancePhase={crabEntrancePhase} />
@@ -972,7 +1338,7 @@ function SessionContextLogButton({ label, logId }: { label: string; logId: numbe
972
1338
  <button
973
1339
  type="button"
974
1340
  onClick={() => openLogContext(logId)}
975
- className="inline-flex h-5 items-center rounded px-1 font-mono text-[10px] text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
1341
+ className="inline-flex h-5 items-center rounded px-1 font-mono text-[10px] text-muted-foreground transition-colors hover:bg-white/[0.06] hover:text-foreground"
976
1342
  title={`Open Context for ${formatLogReference(logId)}`}
977
1343
  >
978
1344
  {label} {formatLogReference(logId)}
@@ -1009,7 +1375,7 @@ function SessionContextProgressBar({
1009
1375
  title="Open latest Context details"
1010
1376
  >
1011
1377
  <span
1012
- className="relative block h-2.5 overflow-hidden rounded-full border border-border/70 bg-background transition-colors group-hover:border-foreground/30"
1378
+ className="relative block h-2.5 overflow-hidden rounded-full border border-white/10 bg-black/25 transition-colors group-hover:border-cyan-200/35"
1013
1379
  role="meter"
1014
1380
  aria-label={ariaLabel}
1015
1381
  aria-valuemin={0}
@@ -1044,7 +1410,7 @@ function SessionModelContextRow({ summary }: { summary: SessionModelContextSumma
1044
1410
  };
1045
1411
 
1046
1412
  return (
1047
- <div className="grid gap-x-4 gap-y-2 border-t border-border/60 py-2 first:border-t-0 lg:grid-cols-[minmax(180px,1fr)_minmax(260px,1fr)_minmax(260px,1.4fr)]">
1413
+ <div className="grid gap-x-4 gap-y-2 border-t border-white/10 py-2 first:border-t-0 lg:grid-cols-[minmax(180px,1fr)_minmax(260px,1fr)_minmax(260px,1.4fr)]">
1048
1414
  <div className="min-w-0">
1049
1415
  <div className="flex min-w-0 items-center gap-2">
1050
1416
  <button
@@ -1099,7 +1465,7 @@ function SessionModelContextRow({ summary }: { summary: SessionModelContextSumma
1099
1465
  <TrendIcon
1100
1466
  className={cn(
1101
1467
  "size-3",
1102
- trend !== null && trend > 0 ? "text-amber-300" : "text-emerald-300",
1468
+ trend !== null && trend > 0 ? "text-amber-300" : "text-cyan-300",
1103
1469
  )}
1104
1470
  />
1105
1471
  )}
@@ -1137,7 +1503,7 @@ function SessionContextSummaryPanel({
1137
1503
  }): JSX.Element | null {
1138
1504
  if (summary.models.length === 0) return null;
1139
1505
  return (
1140
- <div className="mt-3 border-t border-border/70 pt-2">
1506
+ <div className="mt-3 border-t border-white/10 pt-2">
1141
1507
  <div className="mb-1 flex items-center justify-between gap-2">
1142
1508
  <div className="text-[11px] font-semibold uppercase tracking-normal text-muted-foreground">
1143
1509
  Session Context
@@ -1205,12 +1571,12 @@ function SessionContextBar({
1205
1571
  }, []);
1206
1572
 
1207
1573
  return (
1208
- <div className="mb-3 rounded-md border border-border bg-muted/15 px-3 py-2 text-xs">
1574
+ <div className="border border-border bg-card shadow-sm mb-3 rounded-[8px] px-3 py-2 text-xs">
1209
1575
  <div className="flex flex-wrap items-center gap-2">
1210
1576
  {showBackLink && (
1211
1577
  <a
1212
1578
  href="/"
1213
- className="inline-flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
1579
+ className="border border-input bg-background hover:bg-accent hover:text-accent-foreground inline-flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:text-foreground"
1214
1580
  aria-label="Back to all sessions"
1215
1581
  title="Back to all sessions"
1216
1582
  >
@@ -1247,7 +1613,7 @@ function SessionContextBar({
1247
1613
  <button
1248
1614
  type="button"
1249
1615
  onClick={() => setDetailsOpen((value) => !value)}
1250
- 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"
1616
+ className="border border-input bg-background hover:bg-accent hover:text-accent-foreground inline-flex h-8 shrink-0 items-center gap-1.5 rounded-md px-2.5 font-mono text-[11px] text-muted-foreground transition-colors hover:text-foreground"
1251
1617
  aria-expanded={detailsOpen}
1252
1618
  title={detailsOpen ? "Hide context details" : "Show context details"}
1253
1619
  >
@@ -1262,7 +1628,7 @@ function SessionContextBar({
1262
1628
  <button
1263
1629
  type="button"
1264
1630
  onClick={handleCopyLink}
1265
- className="inline-flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
1631
+ className="border border-input bg-background hover:bg-accent hover:text-accent-foreground inline-flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:text-foreground"
1266
1632
  aria-label={copied ? "Copied session link" : "Copy session link"}
1267
1633
  title={copied ? "Copied session link" : "Copy session link"}
1268
1634
  >
@@ -1286,6 +1652,8 @@ export type ProxyViewerProps = {
1286
1652
  /** Clear only the logs whose ids are passed. Called by the per-group
1287
1653
  * Clear button on each conversation header. */
1288
1654
  onClearGroup: (ids: number[]) => void;
1655
+ /** Surface short-lived action feedback without adding sticky page chrome. */
1656
+ onNotify?: (notice: TransientToastInput) => void;
1289
1657
  onImportLogs?: (file: File) => Promise<ImportLogsResponse>;
1290
1658
  viewMode: "simple" | "full";
1291
1659
  captureMode: CaptureMode;
@@ -1319,6 +1687,7 @@ export function ProxyViewer({
1319
1687
  onModelChange,
1320
1688
  onClearAll,
1321
1689
  onClearGroup,
1690
+ onNotify,
1322
1691
  onImportLogs,
1323
1692
  viewMode,
1324
1693
  captureMode,
@@ -1375,13 +1744,17 @@ export function ProxyViewer({
1375
1744
  try {
1376
1745
  const result = await exportLogsAsZip(logs, mode);
1377
1746
  if (!result.ok) {
1378
- setExportError(result.message);
1747
+ if (onNotify !== undefined) {
1748
+ onNotify({ message: result.message, tone: "error" });
1749
+ } else {
1750
+ setExportError(result.message);
1751
+ }
1379
1752
  }
1380
1753
  } finally {
1381
1754
  setExporting(null);
1382
1755
  }
1383
1756
  },
1384
- [logs],
1757
+ [logs, onNotify],
1385
1758
  );
1386
1759
  const handleImportClick = useCallback(() => {
1387
1760
  importInputRef.current?.click();
@@ -1399,18 +1772,26 @@ export function ProxyViewer({
1399
1772
  void onImportLogs(file)
1400
1773
  .then((result) => {
1401
1774
  const skipped = result.skipped > 0 ? `, skipped ${String(result.skipped)}` : "";
1402
- setImportStatus(
1403
- `Imported ${String(result.imported)} logs as ${result.sessionId}${skipped}.`,
1404
- );
1775
+ const message = `Imported ${String(result.imported)} logs as ${result.sessionId}${skipped}.`;
1776
+ if (onNotify !== undefined) {
1777
+ onNotify({ message, tone: "success" });
1778
+ } else {
1779
+ setImportStatus(message);
1780
+ }
1405
1781
  })
1406
1782
  .catch((err: unknown) => {
1407
- setImportError(err instanceof Error ? err.message : "Failed to import logs");
1783
+ const message = err instanceof Error ? err.message : "Failed to import logs";
1784
+ if (onNotify !== undefined) {
1785
+ onNotify({ message, tone: "error" });
1786
+ } else {
1787
+ setImportError(message);
1788
+ }
1408
1789
  })
1409
1790
  .finally(() => {
1410
1791
  setImporting(false);
1411
1792
  });
1412
1793
  },
1413
- [onImportLogs],
1794
+ [onImportLogs, onNotify],
1414
1795
  );
1415
1796
  // Close the compare drawer when the user changes the session or model
1416
1797
  // filter, since the predecessor relationship may no longer be meaningful.
@@ -1452,11 +1833,12 @@ export function ProxyViewer({
1452
1833
  () =>
1453
1834
  buildSessionSlateStats({
1454
1835
  logs,
1836
+ conversationGroups: groups,
1455
1837
  isLoading,
1456
1838
  timeDisplayFormat,
1457
1839
  slowResponseThresholdSeconds,
1458
1840
  }),
1459
- [isLoading, logs, slowResponseThresholdSeconds, timeDisplayFormat],
1841
+ [groups, isLoading, logs, slowResponseThresholdSeconds, timeDisplayFormat],
1460
1842
  );
1461
1843
  const handleCompareWithPrevious = useCallback(
1462
1844
  (log: CapturedLog) => {
@@ -1467,38 +1849,30 @@ export function ProxyViewer({
1467
1849
  );
1468
1850
 
1469
1851
  return (
1470
- <div className="min-h-screen w-full bg-[#070809] text-foreground">
1852
+ <div className="bg-background min-h-screen w-full text-foreground">
1471
1853
  <div className="grid min-h-screen w-full grid-cols-1 lg:grid-cols-[340px_minmax(0,1fr)] xl:grid-cols-[360px_minmax(0,1fr)]">
1472
- <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">
1854
+ <aside className="bg-background border-b lg:sticky lg:top-0 lg:h-screen lg:overflow-y-auto lg:border-b-0 lg:border-r">
1473
1855
  <div className="space-y-4 p-4">
1474
1856
  <BrandHeader compact={true} crabEntrancePhase={crabEntrancePhase} />
1475
1857
 
1476
- <div className="grid grid-cols-3 gap-2 border-y border-border/70 py-3">
1477
- <div className="min-w-0">
1478
- <div className="font-mono text-lg font-semibold text-foreground">{logs.length}</div>
1479
- <div className="text-[10px] uppercase text-muted-foreground">Requests</div>
1480
- </div>
1481
- <div className="min-w-0">
1482
- <div className="font-mono text-lg font-semibold text-foreground">
1483
- {formatTokens(hasSessionContext ? sessionContextTotalIn : totalIn)}
1484
- </div>
1485
- <div className="text-[10px] uppercase text-muted-foreground">Input</div>
1486
- </div>
1487
- <div className="min-w-0">
1488
- <div className="font-mono text-lg font-semibold text-foreground">
1489
- {formatTokens(hasSessionContext ? sessionContextTotalOut : totalOut)}
1490
- </div>
1491
- <div className="text-[10px] uppercase text-muted-foreground">Output</div>
1858
+ <SidebarPanel label="Session">
1859
+ <div className="grid grid-cols-3 gap-2">
1860
+ <SidebarMetric label="Requests" value={logs.length} />
1861
+ <SidebarMetric
1862
+ label="Input"
1863
+ value={formatTokens(hasSessionContext ? sessionContextTotalIn : totalIn)}
1864
+ />
1865
+ <SidebarMetric
1866
+ label="Output"
1867
+ value={formatTokens(hasSessionContext ? sessionContextTotalOut : totalOut)}
1868
+ />
1492
1869
  </div>
1493
- </div>
1870
+ </SidebarPanel>
1494
1871
 
1495
- <div className="space-y-2">
1496
- <div className="text-[10px] font-semibold uppercase tracking-normal text-muted-foreground">
1497
- Workspace
1498
- </div>
1499
- <div className="rounded-md border border-border bg-[#0c0d0f] px-3 py-2">
1872
+ <SidebarPanel label="Workspace">
1873
+ <div className="border border-border bg-muted/40 rounded-md px-3 py-2">
1500
1874
  <div
1501
- className="font-mono text-xs font-semibold text-purple-300"
1875
+ className="truncate font-mono text-xs font-semibold text-cyan-100"
1502
1876
  title={activeScopeLabel}
1503
1877
  >
1504
1878
  {activeScopeLabel}
@@ -1507,7 +1881,7 @@ export function ProxyViewer({
1507
1881
  {isPinnedSessionPage ? "Session workspace" : "Live workspace"}
1508
1882
  </div>
1509
1883
  </div>
1510
- </div>
1884
+ </SidebarPanel>
1511
1885
 
1512
1886
  {hasSessionContext && sessionContextScope !== null && (
1513
1887
  <SessionContextBar
@@ -1527,44 +1901,43 @@ export function ProxyViewer({
1527
1901
  <SessionMembershipPanel memberships={sessionMemberships} />
1528
1902
  )}
1529
1903
 
1530
- <div className="space-y-2">
1531
- <div className="text-[10px] font-semibold uppercase tracking-normal text-muted-foreground">
1532
- Filters
1533
- </div>
1534
- {!hideSessionFilter && (
1535
- <Select value={selectedSession} onValueChange={onSessionChange}>
1536
- <SelectTrigger className="h-9 w-full text-xs">
1537
- <SelectValue placeholder="All sessions" />
1904
+ <SidebarPanel label="Filters">
1905
+ <div className="space-y-2">
1906
+ {!hideSessionFilter && (
1907
+ <Select value={selectedSession} onValueChange={onSessionChange}>
1908
+ <SelectTrigger className="border border-input bg-background hover:bg-accent hover:text-accent-foreground h-9 w-full text-xs">
1909
+ <SelectValue placeholder="All sessions" />
1910
+ </SelectTrigger>
1911
+ <SelectContent>
1912
+ <SelectItem value="__all__">All sessions</SelectItem>
1913
+ {sessions.map((s) => (
1914
+ <SelectItem key={s} value={s}>
1915
+ {truncateSessionId(s)}
1916
+ </SelectItem>
1917
+ ))}
1918
+ </SelectContent>
1919
+ </Select>
1920
+ )}
1921
+ <Select value={selectedModel} onValueChange={onModelChange}>
1922
+ <SelectTrigger className="border border-input bg-background hover:bg-accent hover:text-accent-foreground h-9 w-full text-xs">
1923
+ <SelectValue placeholder="All models" />
1538
1924
  </SelectTrigger>
1539
1925
  <SelectContent>
1540
- <SelectItem value="__all__">All sessions</SelectItem>
1541
- {sessions.map((s) => (
1542
- <SelectItem key={s} value={s}>
1543
- {truncateSessionId(s)}
1926
+ <SelectItem value="__all__">All models</SelectItem>
1927
+ {models.map((m) => (
1928
+ <SelectItem key={m} value={m}>
1929
+ {m}
1544
1930
  </SelectItem>
1545
1931
  ))}
1546
1932
  </SelectContent>
1547
1933
  </Select>
1548
- )}
1549
- <Select value={selectedModel} onValueChange={onModelChange}>
1550
- <SelectTrigger className="h-9 w-full text-xs">
1551
- <SelectValue placeholder="All models" />
1552
- </SelectTrigger>
1553
- <SelectContent>
1554
- <SelectItem value="__all__">All models</SelectItem>
1555
- {models.map((m) => (
1556
- <SelectItem key={m} value={m}>
1557
- {m}
1558
- </SelectItem>
1559
- ))}
1560
- </SelectContent>
1561
- </Select>
1562
- </div>
1934
+ </div>
1935
+ </SidebarPanel>
1563
1936
  </div>
1564
1937
  </aside>
1565
1938
 
1566
1939
  <main className="min-w-0">
1567
- <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">
1940
+ <div className="border border-border bg-background/95 shadow-sm sticky top-0 z-30 border-b px-4 py-3 lg:px-5">
1568
1941
  <div className="flex flex-wrap items-center gap-2">
1569
1942
  <div className="min-w-[180px] flex-1">
1570
1943
  <div className="text-sm font-semibold text-foreground">Logs</div>
@@ -1585,7 +1958,7 @@ export function ProxyViewer({
1585
1958
  type="button"
1586
1959
  onClick={handleImportClick}
1587
1960
  disabled={importing}
1588
- 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"
1961
+ className="border border-input bg-background hover:bg-accent hover:text-accent-foreground inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-xs text-muted-foreground transition-colors hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
1589
1962
  title="Import Agent Inspector export"
1590
1963
  >
1591
1964
  {importing ? (
@@ -1606,7 +1979,7 @@ export function ProxyViewer({
1606
1979
  void handleExport("redacted");
1607
1980
  }}
1608
1981
  disabled={exporting !== null}
1609
- 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"
1982
+ className="border border-input bg-background hover:bg-accent hover:text-accent-foreground inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-xs text-muted-foreground transition-colors hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
1610
1983
  title="Export redacted logs as JSON ZIP"
1611
1984
  >
1612
1985
  {exporting === "redacted" ? (
@@ -1626,7 +1999,7 @@ export function ProxyViewer({
1626
1999
  void handleExport("raw");
1627
2000
  }}
1628
2001
  disabled={exporting !== null}
1629
- 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"
2002
+ className="border border-input bg-background hover:bg-accent hover:text-accent-foreground inline-flex h-8 items-center gap-1.5 rounded-md px-2.5 text-xs text-muted-foreground transition-colors hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
1630
2003
  title="Export raw logs without redaction"
1631
2004
  >
1632
2005
  {exporting === "raw" ? (
@@ -1642,7 +2015,7 @@ export function ProxyViewer({
1642
2015
  <button
1643
2016
  type="button"
1644
2017
  onClick={onClearAll}
1645
- 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"
2018
+ className="border border-input bg-background hover:bg-accent hover:text-accent-foreground inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-xs text-muted-foreground transition-colors hover:text-foreground"
1646
2019
  title="Clear all logs"
1647
2020
  >
1648
2021
  <Trash2 className="size-3.5" />
@@ -1650,18 +2023,18 @@ export function ProxyViewer({
1650
2023
  </button>
1651
2024
  </div>
1652
2025
  <LogPaginationBar logs={logs} pagination={pagination} embedded={true} />
1653
- {exportError !== null && (
2026
+ {onNotify === undefined && exportError !== null && (
1654
2027
  <div className="mt-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
1655
2028
  {exportError}
1656
2029
  </div>
1657
2030
  )}
1658
- {importError !== null && (
2031
+ {onNotify === undefined && importError !== null && (
1659
2032
  <div className="mt-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
1660
2033
  {importError}
1661
2034
  </div>
1662
2035
  )}
1663
- {importStatus !== null && importError === null && (
1664
- <div className="mt-2 rounded-md border border-emerald-500/25 bg-emerald-500/10 px-3 py-2 text-xs text-emerald-200">
2036
+ {onNotify === undefined && importStatus !== null && importError === null && (
2037
+ <div className="mt-2 rounded-md border border-cyan-400/25 bg-cyan-500/10 px-3 py-2 text-xs text-cyan-100">
1665
2038
  {importStatus}
1666
2039
  </div>
1667
2040
  )}
@@ -1672,7 +2045,7 @@ export function ProxyViewer({
1672
2045
  {logs.length === 0 ? (
1673
2046
  selectedSession !== "__all__" ? (
1674
2047
  <div className="py-16 text-center text-muted-foreground">
1675
- <div className="mx-auto max-w-xl space-y-4 rounded-md border border-border bg-muted/10 px-4 py-8">
2048
+ <div className="border border-border bg-card shadow-sm mx-auto max-w-xl space-y-4 rounded-[8px] px-4 py-8">
1676
2049
  <p className="text-sm font-medium">
1677
2050
  {isLoading ? "Loading session data..." : "Session not found"}
1678
2051
  </p>
@@ -1706,7 +2079,7 @@ export function ProxyViewer({
1706
2079
  </div>
1707
2080
  ) : (
1708
2081
  <div className="py-16 text-center text-muted-foreground">
1709
- <div className="mx-auto max-w-2xl space-y-4 rounded-md border border-border bg-muted/10 px-4 py-8">
2082
+ <div className="border border-border bg-card shadow-sm mx-auto max-w-2xl space-y-4 rounded-[8px] px-4 py-8">
1710
2083
  <p className="text-sm">No requests captured yet.</p>
1711
2084
  <div className="flex flex-col items-center gap-2">
1712
2085
  <CopyableCommand command="ANTHROPIC_BASE_URL=http://localhost:25947/proxy <your-tool>" />
@@ -1736,8 +2109,7 @@ export function ProxyViewer({
1736
2109
  onCompareWithPrevious={handleCompareWithPrevious}
1737
2110
  comparisonPredecessors={comparisonPredecessors}
1738
2111
  onClearGroup={onClearGroup}
1739
- standalone={groups.length === 1}
1740
- hasPinnedSessionContext={hasSessionContext}
2112
+ defaultExpanded={groups.length === 1}
1741
2113
  timeDisplayFormat={timeDisplayFormat}
1742
2114
  />
1743
2115
  </div>