@tonyclaw/agent-inspector 2.1.14 → 2.1.16

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 (70) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/CompareDrawer-Ccrrcx1j.js +1 -0
  3. package/.output/public/assets/ProxyViewerContainer-DLqClc9A.js +106 -0
  4. package/.output/public/assets/ReplayDialog-C7axhr-l.js +1 -0
  5. package/.output/public/assets/{RequestAnatomy-BNahe83D.js → RequestAnatomy-WOptg8j_.js} +1 -1
  6. package/.output/public/assets/{ResponseView-DSOnGqi6.js → ResponseView-CSAcxh_M.js} +2 -2
  7. package/.output/public/assets/{StreamingChunkSequence-BEKTDklB.js → StreamingChunkSequence-DJS5KhPx.js} +1 -1
  8. package/.output/public/assets/_sessionId-DUSalzKH.js +1 -0
  9. package/.output/public/assets/index-BPpA21dY.css +1 -0
  10. package/.output/public/assets/index-Bt0Az2I2.js +1 -0
  11. package/.output/public/assets/{index-DWOkqdCa.js → index-DMPNh46t.js} +1 -1
  12. package/.output/public/assets/{json-viewer-C2JpgcW0.js → json-viewer-inX5QSa3.js} +1 -1
  13. package/.output/public/assets/{main-CSONBwwn.js → main-aCAKYGSD.js} +2 -2
  14. package/.output/server/_libs/lucide-react.mjs +224 -194
  15. package/.output/server/{_sessionId-CPAa37n5.mjs → _sessionId-C0fhvQnn.mjs} +2 -2
  16. package/.output/server/_ssr/{CompareDrawer-ceW5VxMo.mjs → CompareDrawer-CO-Ti5sg.mjs} +36 -15
  17. package/.output/server/_ssr/{ProxyViewerContainer-CDfEE_w-.mjs → ProxyViewerContainer-N4J7uBCX.mjs} +1153 -494
  18. package/.output/server/_ssr/{ReplayDialog-V0s_eEbR.mjs → ReplayDialog-J1VxC0In.mjs} +14 -7
  19. package/.output/server/_ssr/{RequestAnatomy-f1ccwR9d.mjs → RequestAnatomy-DBH-F3rq.mjs} +3 -3
  20. package/.output/server/_ssr/{ResponseView-BIRrqG4H.mjs → ResponseView-DLFwV7Hd.mjs} +3 -3
  21. package/.output/server/_ssr/{StreamingChunkSequence-V3JFjCgX.mjs → StreamingChunkSequence-BywmuND0.mjs} +3 -3
  22. package/.output/server/_ssr/{index-DsykulzS.mjs → index-CStymIc0.mjs} +2 -2
  23. package/.output/server/_ssr/index.mjs +2 -2
  24. package/.output/server/_ssr/{json-viewer-Dcnm0Ivf.mjs → json-viewer-DlQu0N1x.mjs} +3 -3
  25. package/.output/server/_ssr/{router-4bdm6Mt2.mjs → router-CQ4Zxhmr.mjs} +118 -47
  26. package/.output/server/{_tanstack-start-manifest_v-Xp4CO64V.mjs → _tanstack-start-manifest_v-BHeJBGfo.mjs} +1 -1
  27. package/.output/server/index.mjs +75 -75
  28. package/package.json +1 -1
  29. package/src/components/ProxyViewer.tsx +174 -107
  30. package/src/components/ProxyViewerContainer.tsx +77 -67
  31. package/src/components/alerts/AlertsDialog.tsx +2 -1
  32. package/src/components/clients/ClientLogo.tsx +132 -0
  33. package/src/components/groups/GroupsDialog.tsx +37 -14
  34. package/src/components/providers/ProviderCard.tsx +1 -1
  35. package/src/components/providers/ProviderForm.tsx +18 -18
  36. package/src/components/providers/ProviderLogo.tsx +6 -1
  37. package/src/components/providers/ProvidersPanel.tsx +6 -6
  38. package/src/components/providers/SettingsDialog.tsx +13 -6
  39. package/src/components/proxy-viewer/AgentTraceSummary.tsx +62 -29
  40. package/src/components/proxy-viewer/CompareDrawer.tsx +38 -8
  41. package/src/components/proxy-viewer/ConversationGroup.tsx +43 -32
  42. package/src/components/proxy-viewer/ConversationGroupList.tsx +452 -89
  43. package/src/components/proxy-viewer/ConversationHeader.tsx +65 -61
  44. package/src/components/proxy-viewer/LogEntry.tsx +31 -18
  45. package/src/components/proxy-viewer/LogEntryHeader.tsx +42 -49
  46. package/src/components/proxy-viewer/ProviderLogoStack.tsx +60 -0
  47. package/src/components/proxy-viewer/ReplayDialog.tsx +8 -2
  48. package/src/components/proxy-viewer/RequestToolsPanel.tsx +9 -9
  49. package/src/components/proxy-viewer/ThreadConnector.tsx +22 -11
  50. package/src/components/proxy-viewer/TurnGroup.tsx +30 -36
  51. package/src/components/proxy-viewer/TurnGroupList.tsx +3 -0
  52. package/src/components/proxy-viewer/viewerState.ts +5 -6
  53. package/src/components/ui/icon-trigger.ts +2 -0
  54. package/src/components/ui/transient-toast.tsx +104 -0
  55. package/src/lib/sessionInfoContract.ts +5 -0
  56. package/src/lib/stopReason.ts +48 -16
  57. package/src/mcp/server.ts +71 -27
  58. package/src/mcp/toolHandlers.ts +23 -4
  59. package/src/proxy/formats/openai/alibabaProvider.ts +0 -4
  60. package/src/proxy/sessionInfo.ts +36 -3
  61. package/src/proxy/sessionSupervisor.ts +20 -1
  62. package/src/proxy/store.ts +1 -1
  63. package/src/routes/api/providers.$providerId.test.log.ts +8 -4
  64. package/styles/globals.css +180 -180
  65. package/.output/public/assets/CompareDrawer-DjgjIFx7.js +0 -1
  66. package/.output/public/assets/ProxyViewerContainer-CWUQZLYy.js +0 -106
  67. package/.output/public/assets/ReplayDialog-CU0Tbb2c.js +0 -1
  68. package/.output/public/assets/_sessionId-Cif8JZdn.js +0 -1
  69. package/.output/public/assets/index-D_WfwzUi.js +0 -1
  70. package/.output/public/assets/index-DtLuQrs0.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,
@@ -40,13 +41,14 @@ import { formatTimestampRange } from "../lib/timeDisplay";
40
41
  import { cn, formatContextWindowTokens, formatTokens } from "../lib/utils";
41
42
  import { useProviders } from "../lib/useProviders";
42
43
  import packageJson from "../../package.json";
43
- import { groupLogsByConversation } from "./proxy-viewer";
44
+ import { groupLogsByConversation, type ConversationGroupData } from "./proxy-viewer";
44
45
 
45
46
  import { CrabLogo } from "./ui/crab-logo";
46
47
  import { crabVariants } from "./ui/crab-variants";
47
48
  import { McpLogo } from "./ui/mcp-logo";
48
49
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
49
50
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
51
+ import type { TransientToastInput } from "./ui/transient-toast";
50
52
  import { AlertsDialog } from "./alerts/AlertsDialog";
51
53
  import { GroupsDialog } from "./groups/GroupsDialog";
52
54
  import { SettingsDialog } from "./providers/SettingsDialog";
@@ -72,6 +74,25 @@ function truncateSessionId(id: string): string {
72
74
  return `${id.slice(0, 12)}...${id.slice(-12)}`;
73
75
  }
74
76
 
77
+ function buildDisplayNumberByLogId(groups: readonly ConversationGroupData[]): Map<number, number> {
78
+ const result = new Map<number, number>();
79
+ for (const group of groups) {
80
+ for (let index = 0; index < group.logs.length; index += 1) {
81
+ const log = group.logs[index];
82
+ if (log === undefined) continue;
83
+ result.set(log.id, index + 1);
84
+ }
85
+ }
86
+ return result;
87
+ }
88
+
89
+ function displayNumberForLog(
90
+ log: CapturedLog,
91
+ displayNumberByLogId: ReadonlyMap<number, number>,
92
+ ): number {
93
+ return displayNumberByLogId.get(log.id) ?? log.id;
94
+ }
95
+
75
96
  export function shouldShowRawExport(captureMode: CaptureMode): boolean {
76
97
  return captureMode === "full";
77
98
  }
@@ -247,7 +268,7 @@ function slateStatusClass(status: SessionSlateStatus): string {
247
268
  case "watch":
248
269
  return "border-amber-300/35 bg-amber-400/10 text-amber-100";
249
270
  case "clear":
250
- return "border-emerald-400/30 bg-emerald-500/10 text-emerald-100";
271
+ return "border-slate-300/20 bg-slate-300/[0.08] text-slate-100";
251
272
  }
252
273
  }
253
274
 
@@ -255,9 +276,9 @@ function SessionOpeningSlate({ stats }: { stats: SessionSlateStats }): JSX.Eleme
255
276
  if (stats.requests === 0) return null;
256
277
 
257
278
  return (
258
- <div className="mb-4 rounded-md border border-border bg-[#0c0d0f] shadow-[0_14px_50px_rgba(0,0,0,0.25)]">
279
+ <div className="border border-border bg-card shadow-sm mb-4 rounded-[8px]">
259
280
  <div className="grid grid-cols-2 gap-0 md:grid-cols-[minmax(220px,1.2fr)_repeat(4,minmax(120px,1fr))]">
260
- <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">
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">
261
282
  <div className="flex items-center gap-2 text-[10px] font-semibold uppercase text-muted-foreground">
262
283
  <Clapperboard className="size-3.5 text-cyan-300" />
263
284
  Opening Slate
@@ -269,7 +290,7 @@ function SessionOpeningSlate({ stats }: { stats: SessionSlateStats }): JSX.Eleme
269
290
  {stats.timeRange ?? "Timeline pending"}
270
291
  </div>
271
292
  </div>
272
- <div className="border-r border-b border-border/70 px-4 py-3 md:border-b-0">
293
+ <div className="border-r border-b border-white/10 px-4 py-3 md:border-b-0">
273
294
  <div className="text-[10px] uppercase text-muted-foreground">Status</div>
274
295
  <div
275
296
  className={cn(
@@ -280,13 +301,13 @@ function SessionOpeningSlate({ stats }: { stats: SessionSlateStats }): JSX.Eleme
280
301
  {stats.statusLabel}
281
302
  </div>
282
303
  </div>
283
- <div className="border-b border-border/70 px-4 py-3 md:border-b-0 md:border-r">
304
+ <div className="border-b border-white/10 px-4 py-3 md:border-b-0 md:border-r">
284
305
  <div className="text-[10px] uppercase text-muted-foreground">Requests</div>
285
306
  <div className="mt-2 font-mono text-xl font-semibold text-foreground">
286
307
  {stats.requests}
287
308
  </div>
288
309
  </div>
289
- <div className="border-r border-border/70 px-4 py-3 md:border-r">
310
+ <div className="border-r border-white/10 px-4 py-3 md:border-r">
290
311
  <div className="flex items-center gap-1 text-[10px] uppercase text-muted-foreground">
291
312
  <Siren className="size-3 text-red-300" />
292
313
  Failure
@@ -392,7 +413,7 @@ function sessionRiskBarClass(risk: ContextRiskLevel): string {
392
413
  case "unknown":
393
414
  return "bg-muted-foreground/50";
394
415
  case "ok":
395
- return "bg-emerald-400";
416
+ return "bg-cyan-300";
396
417
  }
397
418
  }
398
419
 
@@ -451,7 +472,7 @@ function evidenceChipClass(value: string): string {
451
472
  normalized.includes("complete") ||
452
473
  normalized.includes("accept")
453
474
  ) {
454
- return "border-emerald-400/30 bg-emerald-500/10 text-emerald-100";
475
+ return "border-cyan-300/25 bg-cyan-400/10 text-cyan-100";
455
476
  }
456
477
  return "border-border/70 bg-background/70 text-muted-foreground";
457
478
  }
@@ -494,11 +515,11 @@ function SessionMembershipPanel({
494
515
  );
495
516
 
496
517
  return (
497
- <div className="mb-3 overflow-hidden rounded-md border border-border bg-[#0c0d0f] text-xs">
518
+ <div className="border border-border bg-card shadow-sm mb-3 rounded-[8px] text-xs">
498
519
  <button
499
520
  type="button"
500
521
  onClick={() => setOpen((value) => !value)}
501
- className="flex w-full items-center gap-3 px-3 py-2 text-left transition-colors hover:bg-muted/30"
522
+ className="flex w-full items-center gap-3 px-3 py-2 text-left transition-colors hover:bg-white/[0.04]"
502
523
  aria-expanded={open}
503
524
  >
504
525
  <div className="min-w-0 flex-1">
@@ -536,7 +557,7 @@ function SessionMembershipPanel({
536
557
  )}
537
558
  </button>
538
559
  {open && (
539
- <div className="space-y-2 border-t border-border/70 px-3 py-2">
560
+ <div className="space-y-2 border-t border-white/10 px-3 py-2">
540
561
  {memberships.map((membership) => {
541
562
  const member = membership.member;
542
563
  const metadataRows = getMemberMetadataRows(member);
@@ -576,7 +597,7 @@ function SessionMembershipPanel({
576
597
  {metadataRows.map((row) => (
577
598
  <div
578
599
  key={row.key}
579
- className="min-w-0 rounded border border-border/60 px-2 py-1"
600
+ className="border border-border bg-muted/40 min-w-0 rounded-md px-2 py-1"
580
601
  >
581
602
  <div className="font-mono text-[10px] text-muted-foreground">{row.key}</div>
582
603
  <div
@@ -634,7 +655,7 @@ function LogPaginationButton({
634
655
  type="button"
635
656
  onClick={onClick}
636
657
  disabled={disabled}
637
- 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"
658
+ 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"
638
659
  title={title}
639
660
  >
640
661
  {icon}
@@ -660,8 +681,8 @@ function LogPaginationBar({
660
681
  className={cn(
661
682
  "flex min-h-9 flex-col gap-2 px-1 py-1 sm:flex-row sm:items-center",
662
683
  embedded
663
- ? "border-t border-border/70 pt-2"
664
- : "mb-3 rounded-md border border-border bg-muted/10 px-2",
684
+ ? "border-t border-white/10 pt-2"
685
+ : "border border-border bg-card shadow-sm mb-3 rounded-[8px] px-2",
665
686
  )}
666
687
  >
667
688
  <div className="w-full min-w-0 px-1 font-mono text-[11px] text-muted-foreground sm:flex-1">
@@ -719,7 +740,7 @@ function sessionRiskClass(risk: ContextRiskLevel): string {
719
740
  case "unknown":
720
741
  return "text-muted-foreground";
721
742
  case "ok":
722
- return "text-emerald-300";
743
+ return "text-cyan-200";
723
744
  }
724
745
  }
725
746
 
@@ -730,7 +751,7 @@ function sessionHealthClass(level: ContextHealthLevel): string {
730
751
  case "optimizable":
731
752
  return "border-amber-400/25 bg-amber-400/8 text-amber-100";
732
753
  case "ok":
733
- return "border-emerald-400/25 bg-emerald-400/8 text-emerald-100";
754
+ return "border-cyan-300/25 bg-cyan-400/8 text-cyan-100";
734
755
  }
735
756
  }
736
757
 
@@ -793,12 +814,12 @@ function CopyableCommand({ command }: { command: string }): JSX.Element {
793
814
  }, [command]);
794
815
 
795
816
  return (
796
- <div className="inline-flex items-center gap-2 bg-muted rounded-md px-3 py-2">
797
- <pre className="text-blue-500 font-mono text-sm m-0">{command}</pre>
817
+ <div className="border border-border bg-muted/40 inline-flex max-w-full items-center gap-2 rounded-md px-3 py-2">
818
+ <pre className="m-0 overflow-x-auto font-mono text-sm text-cyan-200">{command}</pre>
798
819
  <button
799
820
  type="button"
800
821
  onClick={handleCopy}
801
- className="text-muted-foreground hover:text-foreground transition-colors shrink-0 cursor-pointer"
822
+ className="shrink-0 cursor-pointer text-muted-foreground transition-colors hover:text-foreground"
802
823
  aria-label="Copy command"
803
824
  >
804
825
  {copied ? <Check className="size-4" /> : <Copy className="size-4" />}
@@ -812,8 +833,8 @@ function McpReadyBadge(): JSX.Element {
812
833
  <TooltipProvider>
813
834
  <Tooltip>
814
835
  <TooltipTrigger asChild>
815
- <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)]">
816
- <span className="size-1.5 rounded-full bg-emerald-300 shadow-[0_0_8px_rgba(110,231,183,0.8)]" />
836
+ <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">
837
+ <span className="size-1.5 rounded-full bg-cyan-200 shadow-[0_0_8px_rgba(125,211,252,0.75)]" />
817
838
  MCP Ready
818
839
  <span className="hidden text-cyan-200/70 sm:inline">/api/mcp</span>
819
840
  </span>
@@ -827,6 +848,40 @@ function McpReadyBadge(): JSX.Element {
827
848
  );
828
849
  }
829
850
 
851
+ function SidebarPanel({
852
+ label,
853
+ children,
854
+ className,
855
+ }: {
856
+ label: string;
857
+ children: ReactNode;
858
+ className?: string;
859
+ }): JSX.Element {
860
+ return (
861
+ <section className={cn("border border-border bg-card shadow-sm rounded-[8px]", className)}>
862
+ <div className="border-b border-white/10 px-3 py-2">
863
+ <div className="font-mono text-[10px] font-semibold uppercase tracking-[0.14em] text-white/40">
864
+ {label}
865
+ </div>
866
+ </div>
867
+ <div className="p-3">{children}</div>
868
+ </section>
869
+ );
870
+ }
871
+
872
+ function SidebarMetric({ label, value }: { label: string; value: string | number }): JSX.Element {
873
+ return (
874
+ <div className="border border-border bg-muted/40 min-w-0 rounded-md px-2 py-2">
875
+ <div className="truncate font-mono text-lg font-semibold leading-6 text-slate-100">
876
+ {value}
877
+ </div>
878
+ <div className="mt-0.5 truncate font-mono text-[10px] uppercase tracking-[0.12em] text-white/35">
879
+ {label}
880
+ </div>
881
+ </div>
882
+ );
883
+ }
884
+
830
885
  const CRAB_VARIANT_COLORS = [
831
886
  "text-amber-500",
832
887
  "text-rose-500",
@@ -890,7 +945,7 @@ function BrandHeader({
890
945
  }): JSX.Element {
891
946
  if (compact) {
892
947
  return (
893
- <div className="pb-3">
948
+ <div className="border border-border bg-card shadow-sm rounded-[8px] p-3">
894
949
  <div className="flex items-start justify-between gap-3">
895
950
  <div className="flex min-w-0 items-start gap-2">
896
951
  <CrabFamily compact={true} entrancePhase={crabEntrancePhase} />
@@ -953,7 +1008,7 @@ function SessionContextLogButton({ label, logId }: { label: string; logId: numbe
953
1008
  <button
954
1009
  type="button"
955
1010
  onClick={() => openLogContext(logId)}
956
- 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"
1011
+ 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"
957
1012
  title={`Open Context for ${formatLogReference(logId)}`}
958
1013
  >
959
1014
  {label} {formatLogReference(logId)}
@@ -990,7 +1045,7 @@ function SessionContextProgressBar({
990
1045
  title="Open latest Context details"
991
1046
  >
992
1047
  <span
993
- className="relative block h-2.5 overflow-hidden rounded-full border border-border/70 bg-background transition-colors group-hover:border-foreground/30"
1048
+ 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"
994
1049
  role="meter"
995
1050
  aria-label={ariaLabel}
996
1051
  aria-valuemin={0}
@@ -1025,7 +1080,7 @@ function SessionModelContextRow({ summary }: { summary: SessionModelContextSumma
1025
1080
  };
1026
1081
 
1027
1082
  return (
1028
- <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)]">
1083
+ <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)]">
1029
1084
  <div className="min-w-0">
1030
1085
  <div className="flex min-w-0 items-center gap-2">
1031
1086
  <button
@@ -1080,7 +1135,7 @@ function SessionModelContextRow({ summary }: { summary: SessionModelContextSumma
1080
1135
  <TrendIcon
1081
1136
  className={cn(
1082
1137
  "size-3",
1083
- trend !== null && trend > 0 ? "text-amber-300" : "text-emerald-300",
1138
+ trend !== null && trend > 0 ? "text-amber-300" : "text-cyan-300",
1084
1139
  )}
1085
1140
  />
1086
1141
  )}
@@ -1118,7 +1173,7 @@ function SessionContextSummaryPanel({
1118
1173
  }): JSX.Element | null {
1119
1174
  if (summary.models.length === 0) return null;
1120
1175
  return (
1121
- <div className="mt-3 border-t border-border/70 pt-2">
1176
+ <div className="mt-3 border-t border-white/10 pt-2">
1122
1177
  <div className="mb-1 flex items-center justify-between gap-2">
1123
1178
  <div className="text-[11px] font-semibold uppercase tracking-normal text-muted-foreground">
1124
1179
  Session Context
@@ -1186,12 +1241,12 @@ function SessionContextBar({
1186
1241
  }, []);
1187
1242
 
1188
1243
  return (
1189
- <div className="mb-3 rounded-md border border-border bg-muted/15 px-3 py-2 text-xs">
1244
+ <div className="border border-border bg-card shadow-sm mb-3 rounded-[8px] px-3 py-2 text-xs">
1190
1245
  <div className="flex flex-wrap items-center gap-2">
1191
1246
  {showBackLink && (
1192
1247
  <a
1193
1248
  href="/"
1194
- className="inline-flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
1249
+ 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"
1195
1250
  aria-label="Back to all sessions"
1196
1251
  title="Back to all sessions"
1197
1252
  >
@@ -1228,7 +1283,7 @@ function SessionContextBar({
1228
1283
  <button
1229
1284
  type="button"
1230
1285
  onClick={() => setDetailsOpen((value) => !value)}
1231
- 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"
1286
+ 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"
1232
1287
  aria-expanded={detailsOpen}
1233
1288
  title={detailsOpen ? "Hide context details" : "Show context details"}
1234
1289
  >
@@ -1243,7 +1298,7 @@ function SessionContextBar({
1243
1298
  <button
1244
1299
  type="button"
1245
1300
  onClick={handleCopyLink}
1246
- className="inline-flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
1301
+ 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"
1247
1302
  aria-label={copied ? "Copied session link" : "Copy session link"}
1248
1303
  title={copied ? "Copied session link" : "Copy session link"}
1249
1304
  >
@@ -1267,6 +1322,8 @@ export type ProxyViewerProps = {
1267
1322
  /** Clear only the logs whose ids are passed. Called by the per-group
1268
1323
  * Clear button on each conversation header. */
1269
1324
  onClearGroup: (ids: number[]) => void;
1325
+ /** Surface short-lived action feedback without adding sticky page chrome. */
1326
+ onNotify?: (notice: TransientToastInput) => void;
1270
1327
  onImportLogs?: (file: File) => Promise<ImportLogsResponse>;
1271
1328
  viewMode: "simple" | "full";
1272
1329
  captureMode: CaptureMode;
@@ -1300,6 +1357,7 @@ export function ProxyViewer({
1300
1357
  onModelChange,
1301
1358
  onClearAll,
1302
1359
  onClearGroup,
1360
+ onNotify,
1303
1361
  onImportLogs,
1304
1362
  viewMode,
1305
1363
  captureMode,
@@ -1356,13 +1414,17 @@ export function ProxyViewer({
1356
1414
  try {
1357
1415
  const result = await exportLogsAsZip(logs, mode);
1358
1416
  if (!result.ok) {
1359
- setExportError(result.message);
1417
+ if (onNotify !== undefined) {
1418
+ onNotify({ message: result.message, tone: "error" });
1419
+ } else {
1420
+ setExportError(result.message);
1421
+ }
1360
1422
  }
1361
1423
  } finally {
1362
1424
  setExporting(null);
1363
1425
  }
1364
1426
  },
1365
- [logs],
1427
+ [logs, onNotify],
1366
1428
  );
1367
1429
  const handleImportClick = useCallback(() => {
1368
1430
  importInputRef.current?.click();
@@ -1380,18 +1442,26 @@ export function ProxyViewer({
1380
1442
  void onImportLogs(file)
1381
1443
  .then((result) => {
1382
1444
  const skipped = result.skipped > 0 ? `, skipped ${String(result.skipped)}` : "";
1383
- setImportStatus(
1384
- `Imported ${String(result.imported)} logs as ${result.sessionId}${skipped}.`,
1385
- );
1445
+ const message = `Imported ${String(result.imported)} logs as ${result.sessionId}${skipped}.`;
1446
+ if (onNotify !== undefined) {
1447
+ onNotify({ message, tone: "success" });
1448
+ } else {
1449
+ setImportStatus(message);
1450
+ }
1386
1451
  })
1387
1452
  .catch((err: unknown) => {
1388
- setImportError(err instanceof Error ? err.message : "Failed to import logs");
1453
+ const message = err instanceof Error ? err.message : "Failed to import logs";
1454
+ if (onNotify !== undefined) {
1455
+ onNotify({ message, tone: "error" });
1456
+ } else {
1457
+ setImportError(message);
1458
+ }
1389
1459
  })
1390
1460
  .finally(() => {
1391
1461
  setImporting(false);
1392
1462
  });
1393
1463
  },
1394
- [onImportLogs],
1464
+ [onImportLogs, onNotify],
1395
1465
  );
1396
1466
  // Close the compare drawer when the user changes the session or model
1397
1467
  // filter, since the predecessor relationship may no longer be meaningful.
@@ -1404,6 +1474,7 @@ export function ProxyViewer({
1404
1474
  }, []);
1405
1475
 
1406
1476
  const groups = useMemo(() => groupLogsByConversation(logs), [logs]);
1477
+ const displayNumberByLogId = useMemo(() => buildDisplayNumberByLogId(groups), [groups]);
1407
1478
  const sessionContextScope = useMemo(
1408
1479
  () => resolveSessionContextScope({ pinnedSessionId, selectedSession, sessions, logs }),
1409
1480
  [logs, pinnedSessionId, selectedSession, sessions],
@@ -1447,38 +1518,30 @@ export function ProxyViewer({
1447
1518
  );
1448
1519
 
1449
1520
  return (
1450
- <div className="min-h-screen w-full bg-[#070809] text-foreground">
1521
+ <div className="bg-background min-h-screen w-full text-foreground">
1451
1522
  <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)]">
1452
- <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">
1523
+ <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">
1453
1524
  <div className="space-y-4 p-4">
1454
1525
  <BrandHeader compact={true} crabEntrancePhase={crabEntrancePhase} />
1455
1526
 
1456
- <div className="grid grid-cols-3 gap-2 border-y border-border/70 py-3">
1457
- <div className="min-w-0">
1458
- <div className="font-mono text-lg font-semibold text-foreground">{logs.length}</div>
1459
- <div className="text-[10px] uppercase text-muted-foreground">Requests</div>
1460
- </div>
1461
- <div className="min-w-0">
1462
- <div className="font-mono text-lg font-semibold text-foreground">
1463
- {formatTokens(hasSessionContext ? sessionContextTotalIn : totalIn)}
1464
- </div>
1465
- <div className="text-[10px] uppercase text-muted-foreground">Input</div>
1466
- </div>
1467
- <div className="min-w-0">
1468
- <div className="font-mono text-lg font-semibold text-foreground">
1469
- {formatTokens(hasSessionContext ? sessionContextTotalOut : totalOut)}
1470
- </div>
1471
- <div className="text-[10px] uppercase text-muted-foreground">Output</div>
1527
+ <SidebarPanel label="Session">
1528
+ <div className="grid grid-cols-3 gap-2">
1529
+ <SidebarMetric label="Requests" value={logs.length} />
1530
+ <SidebarMetric
1531
+ label="Input"
1532
+ value={formatTokens(hasSessionContext ? sessionContextTotalIn : totalIn)}
1533
+ />
1534
+ <SidebarMetric
1535
+ label="Output"
1536
+ value={formatTokens(hasSessionContext ? sessionContextTotalOut : totalOut)}
1537
+ />
1472
1538
  </div>
1473
- </div>
1539
+ </SidebarPanel>
1474
1540
 
1475
- <div className="space-y-2">
1476
- <div className="text-[10px] font-semibold uppercase tracking-normal text-muted-foreground">
1477
- Workspace
1478
- </div>
1479
- <div className="rounded-md border border-border bg-[#0c0d0f] px-3 py-2">
1541
+ <SidebarPanel label="Workspace">
1542
+ <div className="border border-border bg-muted/40 rounded-md px-3 py-2">
1480
1543
  <div
1481
- className="font-mono text-xs font-semibold text-purple-300"
1544
+ className="truncate font-mono text-xs font-semibold text-cyan-100"
1482
1545
  title={activeScopeLabel}
1483
1546
  >
1484
1547
  {activeScopeLabel}
@@ -1487,7 +1550,7 @@ export function ProxyViewer({
1487
1550
  {isPinnedSessionPage ? "Session workspace" : "Live workspace"}
1488
1551
  </div>
1489
1552
  </div>
1490
- </div>
1553
+ </SidebarPanel>
1491
1554
 
1492
1555
  {hasSessionContext && sessionContextScope !== null && (
1493
1556
  <SessionContextBar
@@ -1507,44 +1570,43 @@ export function ProxyViewer({
1507
1570
  <SessionMembershipPanel memberships={sessionMemberships} />
1508
1571
  )}
1509
1572
 
1510
- <div className="space-y-2">
1511
- <div className="text-[10px] font-semibold uppercase tracking-normal text-muted-foreground">
1512
- Filters
1513
- </div>
1514
- {!hideSessionFilter && (
1515
- <Select value={selectedSession} onValueChange={onSessionChange}>
1516
- <SelectTrigger className="h-9 w-full text-xs">
1517
- <SelectValue placeholder="All sessions" />
1573
+ <SidebarPanel label="Filters">
1574
+ <div className="space-y-2">
1575
+ {!hideSessionFilter && (
1576
+ <Select value={selectedSession} onValueChange={onSessionChange}>
1577
+ <SelectTrigger className="border border-input bg-background hover:bg-accent hover:text-accent-foreground h-9 w-full text-xs">
1578
+ <SelectValue placeholder="All sessions" />
1579
+ </SelectTrigger>
1580
+ <SelectContent>
1581
+ <SelectItem value="__all__">All sessions</SelectItem>
1582
+ {sessions.map((s) => (
1583
+ <SelectItem key={s} value={s}>
1584
+ {truncateSessionId(s)}
1585
+ </SelectItem>
1586
+ ))}
1587
+ </SelectContent>
1588
+ </Select>
1589
+ )}
1590
+ <Select value={selectedModel} onValueChange={onModelChange}>
1591
+ <SelectTrigger className="border border-input bg-background hover:bg-accent hover:text-accent-foreground h-9 w-full text-xs">
1592
+ <SelectValue placeholder="All models" />
1518
1593
  </SelectTrigger>
1519
1594
  <SelectContent>
1520
- <SelectItem value="__all__">All sessions</SelectItem>
1521
- {sessions.map((s) => (
1522
- <SelectItem key={s} value={s}>
1523
- {truncateSessionId(s)}
1595
+ <SelectItem value="__all__">All models</SelectItem>
1596
+ {models.map((m) => (
1597
+ <SelectItem key={m} value={m}>
1598
+ {m}
1524
1599
  </SelectItem>
1525
1600
  ))}
1526
1601
  </SelectContent>
1527
1602
  </Select>
1528
- )}
1529
- <Select value={selectedModel} onValueChange={onModelChange}>
1530
- <SelectTrigger className="h-9 w-full text-xs">
1531
- <SelectValue placeholder="All models" />
1532
- </SelectTrigger>
1533
- <SelectContent>
1534
- <SelectItem value="__all__">All models</SelectItem>
1535
- {models.map((m) => (
1536
- <SelectItem key={m} value={m}>
1537
- {m}
1538
- </SelectItem>
1539
- ))}
1540
- </SelectContent>
1541
- </Select>
1542
- </div>
1603
+ </div>
1604
+ </SidebarPanel>
1543
1605
  </div>
1544
1606
  </aside>
1545
1607
 
1546
1608
  <main className="min-w-0">
1547
- <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">
1609
+ <div className="border border-border bg-background/95 shadow-sm sticky top-0 z-30 border-b px-4 py-3 lg:px-5">
1548
1610
  <div className="flex flex-wrap items-center gap-2">
1549
1611
  <div className="min-w-[180px] flex-1">
1550
1612
  <div className="text-sm font-semibold text-foreground">Logs</div>
@@ -1565,7 +1627,7 @@ export function ProxyViewer({
1565
1627
  type="button"
1566
1628
  onClick={handleImportClick}
1567
1629
  disabled={importing}
1568
- 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"
1630
+ 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"
1569
1631
  title="Import Agent Inspector export"
1570
1632
  >
1571
1633
  {importing ? (
@@ -1586,7 +1648,7 @@ export function ProxyViewer({
1586
1648
  void handleExport("redacted");
1587
1649
  }}
1588
1650
  disabled={exporting !== null}
1589
- 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"
1651
+ 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"
1590
1652
  title="Export redacted logs as JSON ZIP"
1591
1653
  >
1592
1654
  {exporting === "redacted" ? (
@@ -1606,7 +1668,7 @@ export function ProxyViewer({
1606
1668
  void handleExport("raw");
1607
1669
  }}
1608
1670
  disabled={exporting !== null}
1609
- 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"
1671
+ 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"
1610
1672
  title="Export raw logs without redaction"
1611
1673
  >
1612
1674
  {exporting === "raw" ? (
@@ -1622,7 +1684,7 @@ export function ProxyViewer({
1622
1684
  <button
1623
1685
  type="button"
1624
1686
  onClick={onClearAll}
1625
- 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"
1687
+ 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"
1626
1688
  title="Clear all logs"
1627
1689
  >
1628
1690
  <Trash2 className="size-3.5" />
@@ -1630,18 +1692,18 @@ export function ProxyViewer({
1630
1692
  </button>
1631
1693
  </div>
1632
1694
  <LogPaginationBar logs={logs} pagination={pagination} embedded={true} />
1633
- {exportError !== null && (
1695
+ {onNotify === undefined && exportError !== null && (
1634
1696
  <div className="mt-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
1635
1697
  {exportError}
1636
1698
  </div>
1637
1699
  )}
1638
- {importError !== null && (
1700
+ {onNotify === undefined && importError !== null && (
1639
1701
  <div className="mt-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
1640
1702
  {importError}
1641
1703
  </div>
1642
1704
  )}
1643
- {importStatus !== null && importError === null && (
1644
- <div className="mt-2 rounded-md border border-emerald-500/25 bg-emerald-500/10 px-3 py-2 text-xs text-emerald-200">
1705
+ {onNotify === undefined && importStatus !== null && importError === null && (
1706
+ <div className="mt-2 rounded-md border border-cyan-400/25 bg-cyan-500/10 px-3 py-2 text-xs text-cyan-100">
1645
1707
  {importStatus}
1646
1708
  </div>
1647
1709
  )}
@@ -1652,7 +1714,7 @@ export function ProxyViewer({
1652
1714
  {logs.length === 0 ? (
1653
1715
  selectedSession !== "__all__" ? (
1654
1716
  <div className="py-16 text-center text-muted-foreground">
1655
- <div className="mx-auto max-w-xl space-y-4 rounded-md border border-border bg-muted/10 px-4 py-8">
1717
+ <div className="border border-border bg-card shadow-sm mx-auto max-w-xl space-y-4 rounded-[8px] px-4 py-8">
1656
1718
  <p className="text-sm font-medium">
1657
1719
  {isLoading ? "Loading session data..." : "Session not found"}
1658
1720
  </p>
@@ -1686,7 +1748,7 @@ export function ProxyViewer({
1686
1748
  </div>
1687
1749
  ) : (
1688
1750
  <div className="py-16 text-center text-muted-foreground">
1689
- <div className="mx-auto max-w-2xl space-y-4 rounded-md border border-border bg-muted/10 px-4 py-8">
1751
+ <div className="border border-border bg-card shadow-sm mx-auto max-w-2xl space-y-4 rounded-[8px] px-4 py-8">
1690
1752
  <p className="text-sm">No requests captured yet.</p>
1691
1753
  <div className="flex flex-col items-center gap-2">
1692
1754
  <CopyableCommand command="ANTHROPIC_BASE_URL=http://localhost:25947/proxy <your-tool>" />
@@ -1716,8 +1778,7 @@ export function ProxyViewer({
1716
1778
  onCompareWithPrevious={handleCompareWithPrevious}
1717
1779
  comparisonPredecessors={comparisonPredecessors}
1718
1780
  onClearGroup={onClearGroup}
1719
- standalone={groups.length === 1}
1720
- hasPinnedSessionContext={hasSessionContext}
1781
+ defaultExpanded={groups.length === 1}
1721
1782
  timeDisplayFormat={timeDisplayFormat}
1722
1783
  />
1723
1784
  </div>
@@ -1733,7 +1794,13 @@ export function ProxyViewer({
1733
1794
  {/* Compare drawer: sibling of the log list, not a route change. */}
1734
1795
  {comparePair !== null && (
1735
1796
  <Suspense fallback={null}>
1736
- <LazyCompareDrawer left={comparePair[0]} right={comparePair[1]} onClose={closeCompare} />
1797
+ <LazyCompareDrawer
1798
+ left={comparePair[0]}
1799
+ right={comparePair[1]}
1800
+ leftDisplayNumber={displayNumberForLog(comparePair[0], displayNumberByLogId)}
1801
+ rightDisplayNumber={displayNumberForLog(comparePair[1], displayNumberByLogId)}
1802
+ onClose={closeCompare}
1803
+ />
1737
1804
  </Suspense>
1738
1805
  )}
1739
1806
  </div>