@tonyclaw/agent-inspector 2.0.10 → 2.0.12

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 (47) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-BWMipEjS.js → CompareDrawer-ClM_uVRy.js} +1 -1
  3. package/.output/public/assets/ProxyViewerContainer-CTNNzXSa.js +114 -0
  4. package/.output/public/assets/{ReplayDialog-DaSzZsvM.js → ReplayDialog-7rp7N_KJ.js} +1 -1
  5. package/.output/public/assets/RequestAnatomy-sL2FFo_N.js +1 -0
  6. package/.output/public/assets/ResponseView-Cjlf3b6-.js +1 -0
  7. package/.output/public/assets/{StreamingChunkSequence-B4atnL99.js → StreamingChunkSequence-Ctx3sT3r.js} +1 -1
  8. package/.output/public/assets/_sessionId-DEWzDQ8-.js +1 -0
  9. package/.output/public/assets/index-DGIIhZTn.js +1 -0
  10. package/.output/public/assets/index-D_nZj9Vt.css +1 -0
  11. package/.output/public/assets/{main-DE6Y4-wV.js → main-FvIQopAy.js} +2 -2
  12. package/.output/server/_libs/lucide-react.mjs +51 -39
  13. package/.output/server/{_sessionId-61HO9rUI.mjs → _sessionId-BDSgtrsI.mjs} +2 -2
  14. package/.output/server/_ssr/{CompareDrawer-CSPqneYm.mjs → CompareDrawer-Cqvv0I3x.mjs} +3 -3
  15. package/.output/server/_ssr/{ProxyViewerContainer-DrSMa5O7.mjs → ProxyViewerContainer-C0YEoG8N.mjs} +1611 -183
  16. package/.output/server/_ssr/{ReplayDialog-Dn3naENv.mjs → ReplayDialog-CrsynUKI.mjs} +4 -4
  17. package/.output/server/_ssr/RequestAnatomy-CR_JUkcL.mjs +688 -0
  18. package/.output/server/_ssr/{ResponseView-CwXAWOoE.mjs → ResponseView-Dba2Et69.mjs} +3 -3
  19. package/.output/server/_ssr/{StreamingChunkSequence-DRSR2FfN.mjs → StreamingChunkSequence-BIMHLstI.mjs} +2 -2
  20. package/.output/server/_ssr/{index-D4zn5CAg.mjs → index-DveIUTm5.mjs} +2 -2
  21. package/.output/server/_ssr/index.mjs +2 -2
  22. package/.output/server/_ssr/{router-CSRaa_tu.mjs → router-D0yXVG0Q.mjs} +85 -22
  23. package/.output/server/{_tanstack-start-manifest_v-CfvVUYYj.mjs → _tanstack-start-manifest_v-pe0xJFuQ.mjs} +1 -1
  24. package/.output/server/index.mjs +70 -70
  25. package/package.json +1 -1
  26. package/src/components/ProxyViewer.tsx +453 -52
  27. package/src/components/providers/ProviderCard.tsx +45 -2
  28. package/src/components/providers/ProviderForm.tsx +245 -80
  29. package/src/components/providers/ProvidersPanel.tsx +7 -1
  30. package/src/components/proxy-viewer/ConversationGroup.tsx +18 -1
  31. package/src/components/proxy-viewer/LogEntry.tsx +25 -0
  32. package/src/components/proxy-viewer/TurnGroup.tsx +15 -0
  33. package/src/components/proxy-viewer/anatomy/RequestAnatomy.tsx +144 -80
  34. package/src/components/proxy-viewer/anatomy/contextIntelligence.ts +70 -5
  35. package/src/components/proxy-viewer/anatomy/sessionContextSummary.ts +196 -0
  36. package/src/components/proxy-viewer/logFocus.ts +33 -0
  37. package/src/lib/providerModelMetadata.ts +64 -12
  38. package/src/lib/utils.ts +7 -0
  39. package/src/proxy/providers.ts +25 -0
  40. package/src/routes/api/providers.ts +3 -0
  41. package/.output/public/assets/ProxyViewerContainer-DIAgurux.js +0 -114
  42. package/.output/public/assets/RequestAnatomy-BfQmPHCd.js +0 -1
  43. package/.output/public/assets/ResponseView-CblEWDMx.js +0 -1
  44. package/.output/public/assets/_sessionId-DUzYnZXV.js +0 -1
  45. package/.output/public/assets/index-CxhRsekH.css +0 -1
  46. package/.output/public/assets/index-jagRsQaV.js +0 -1
  47. package/.output/server/_ssr/RequestAnatomy-D1X3f3AX.mjs +0 -1322
@@ -1,6 +1,6 @@
1
- import { Info } from "lucide-react";
1
+ import { ChevronDown, ChevronRight, Info } from "lucide-react";
2
2
  import { type JSX, useMemo, useState } from "react";
3
- import { cn, formatTokens } from "../../../lib/utils";
3
+ import { cn, formatContextWindowTokens, formatTokens } from "../../../lib/utils";
4
4
  import { useProviders } from "../../../lib/useProviders";
5
5
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../../ui/tooltip";
6
6
  import {
@@ -9,6 +9,8 @@ import {
9
9
  type ContextDiagnostic,
10
10
  type DuplicateContextGroup,
11
11
  type DuplicateContextSegment,
12
+ type ContextHealth,
13
+ type ContextHealthLevel,
12
14
  type ContextIntelligence,
13
15
  type ContextRiskLevel,
14
16
  } from "./contextIntelligence";
@@ -63,6 +65,18 @@ const RISK_TEXT_CLASS: Record<ContextRiskLevel, string> = {
63
65
  danger: "text-red-300",
64
66
  };
65
67
 
68
+ const HEALTH_BADGE_CLASS: Record<ContextHealthLevel, string> = {
69
+ ok: "border-emerald-400/30 bg-emerald-400/8 text-emerald-200",
70
+ optimizable: "border-amber-400/30 bg-amber-400/8 text-amber-100",
71
+ risk: "border-red-400/35 bg-red-500/10 text-red-100",
72
+ };
73
+
74
+ const HEALTH_LABEL: Record<ContextHealthLevel, string> = {
75
+ ok: "OK",
76
+ optimizable: "Optimizable",
77
+ risk: "Risk",
78
+ };
79
+
66
80
  function formatPercent(value: number): string {
67
81
  if (value >= 10) return `${value.toFixed(0)}%`;
68
82
  if (value >= 1) return `${value.toFixed(1)}%`;
@@ -83,7 +97,7 @@ function formatRemainingTokens(value: number | null): string {
83
97
 
84
98
  function formatContextLimit(intelligence: ContextIntelligence): string {
85
99
  if (intelligence.contextWindow.tokens === null) return "Unknown";
86
- return formatTokens(intelligence.contextWindow.tokens);
100
+ return formatContextWindowTokens(intelligence.contextWindow.tokens);
87
101
  }
88
102
 
89
103
  function formatContextSource(intelligence: ContextIntelligence): string {
@@ -110,6 +124,33 @@ function diagnosticTone(diagnostic: ContextDiagnostic): string {
110
124
  }
111
125
  }
112
126
 
127
+ function formatContextHealth(health: ContextHealth): string {
128
+ const label = HEALTH_LABEL[health.level];
129
+ if (health.level === "ok") return label;
130
+ const countLabel =
131
+ health.level === "risk"
132
+ ? `${String(health.opportunityCount)} signal${health.opportunityCount === 1 ? "" : "s"}`
133
+ : `${String(health.opportunityCount)} opportunit${
134
+ health.opportunityCount === 1 ? "y" : "ies"
135
+ }`;
136
+ if (health.reclaimableTokens <= 0) return `${label} | ${countLabel}`;
137
+ return `${label} | ${countLabel} | ~${formatTokens(health.reclaimableTokens)} tokens`;
138
+ }
139
+
140
+ function ContextHealthBadge({ health }: { health: ContextHealth }): JSX.Element {
141
+ return (
142
+ <span
143
+ className={cn(
144
+ "inline-flex h-5 max-w-full items-center rounded border px-1.5 font-mono text-[10px]",
145
+ HEALTH_BADGE_CLASS[health.level],
146
+ )}
147
+ aria-label={`Context health ${formatContextHealth(health)}`}
148
+ >
149
+ <span className="truncate">{formatContextHealth(health)}</span>
150
+ </span>
151
+ );
152
+ }
153
+
113
154
  function ContextIntelligenceStrip({
114
155
  intelligence,
115
156
  }: {
@@ -243,6 +284,8 @@ function DuplicateContentPanel({
243
284
  segments: readonly AnatomySegment[];
244
285
  onSegmentActivate?: (segment: AnatomySegment) => void;
245
286
  }): JSX.Element | null {
287
+ const [expanded, setExpanded] = useState(false);
288
+
246
289
  if (groups.length === 0) return null;
247
290
 
248
291
  const visibleGroups = groups.slice(0, DUPLICATE_GROUP_DISPLAY_COUNT);
@@ -251,87 +294,105 @@ function DuplicateContentPanel({
251
294
 
252
295
  return (
253
296
  <section className="space-y-2 rounded border border-border/70 bg-muted/15 p-2">
254
- <div className="flex flex-wrap items-center justify-between gap-2">
255
- <div className="text-xs font-medium text-foreground">Duplicate Content</div>
256
- <div className="font-mono text-[11px] text-muted-foreground">
297
+ <button
298
+ type="button"
299
+ onClick={() => setExpanded((current) => !current)}
300
+ aria-expanded={expanded}
301
+ className="flex w-full flex-wrap items-center justify-between gap-2 rounded px-1 py-0.5 text-left transition-colors hover:bg-muted/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1"
302
+ >
303
+ <span className="inline-flex min-w-0 items-center gap-1.5">
304
+ {expanded ? (
305
+ <ChevronDown className="size-3.5 shrink-0 text-muted-foreground" />
306
+ ) : (
307
+ <ChevronRight className="size-3.5 shrink-0 text-muted-foreground" />
308
+ )}
309
+ <span className="truncate text-xs font-medium text-foreground">Duplicate Content</span>
310
+ </span>
311
+ <span className="font-mono text-[11px] text-muted-foreground">
257
312
  {String(groups.length)} group{groups.length === 1 ? "" : "s"} | ~
258
313
  {formatTokens(totalRepeatedTokens)} repeated
259
- </div>
260
- </div>
261
-
262
- <div className="space-y-2">
263
- {visibleGroups.map((group, groupIndex) => {
264
- const visibleSegments = group.segments.slice(0, DUPLICATE_SEGMENT_DISPLAY_COUNT);
265
- const hiddenSegmentCount = group.segments.length - visibleSegments.length;
266
- return (
267
- <div
268
- key={group.key}
269
- className="space-y-1.5 border-t border-border/50 pt-2 first:border-t-0 first:pt-0"
270
- >
271
- <div className="flex min-w-0 items-center gap-2 text-xs">
272
- <span className="w-5 shrink-0 text-right font-mono text-muted-foreground/70">
273
- {String(groupIndex + 1)}
274
- </span>
275
- <span className="min-w-0 flex-1 truncate text-foreground">{group.firstLabel}</span>
276
- <span className="shrink-0 font-mono text-muted-foreground">
277
- x{String(group.count)} | ~{formatTokens(group.repeatedTokens)} saved
278
- </span>
279
- </div>
314
+ </span>
315
+ </button>
316
+
317
+ {expanded && (
318
+ <>
319
+ <div className="space-y-2">
320
+ {visibleGroups.map((group, groupIndex) => {
321
+ const visibleSegments = group.segments.slice(0, DUPLICATE_SEGMENT_DISPLAY_COUNT);
322
+ const hiddenSegmentCount = group.segments.length - visibleSegments.length;
323
+ return (
324
+ <div
325
+ key={group.key}
326
+ className="space-y-1.5 border-t border-border/50 pt-2 first:border-t-0 first:pt-0"
327
+ >
328
+ <div className="flex min-w-0 items-center gap-2 text-xs">
329
+ <span className="w-5 shrink-0 text-right font-mono text-muted-foreground/70">
330
+ {String(groupIndex + 1)}
331
+ </span>
332
+ <span className="min-w-0 flex-1 truncate text-foreground">
333
+ {group.firstLabel}
334
+ </span>
335
+ <span className="shrink-0 font-mono text-muted-foreground">
336
+ x{String(group.count)} | ~{formatTokens(group.repeatedTokens)} saved
337
+ </span>
338
+ </div>
339
+
340
+ {group.preview.length > 0 && (
341
+ <div className="ml-7 line-clamp-2 break-words rounded bg-background/40 px-2 py-1 font-mono text-[11px] leading-4 text-muted-foreground">
342
+ {group.preview}
343
+ </div>
344
+ )}
280
345
 
281
- {group.preview.length > 0 && (
282
- <div className="ml-7 line-clamp-2 break-words rounded bg-background/40 px-2 py-1 font-mono text-[11px] leading-4 text-muted-foreground">
283
- {group.preview}
284
- </div>
285
- )}
286
-
287
- <div className="ml-7 flex flex-wrap gap-1">
288
- {visibleSegments.map((duplicateSegment, segmentIndex) => {
289
- const target = resolveDuplicateSegment(duplicateSegment, segments);
290
- const activate =
291
- target === null || onSegmentActivate === undefined
292
- ? undefined
293
- : onSegmentActivate;
294
- const label = `${duplicateSegment.label} ~${formatTokens(
295
- duplicateSegment.tokens,
296
- )}`;
297
- if (activate === undefined || target === null) {
298
- return (
299
- <span
300
- key={`${duplicateSegment.path}-${segmentIndex}`}
301
- className="inline-flex h-6 max-w-40 items-center rounded border border-border/60 px-1.5 text-[11px] text-muted-foreground"
302
- title={label}
303
- >
304
- <span className="truncate">{label}</span>
346
+ <div className="ml-7 flex flex-wrap gap-1">
347
+ {visibleSegments.map((duplicateSegment, segmentIndex) => {
348
+ const target = resolveDuplicateSegment(duplicateSegment, segments);
349
+ const activate =
350
+ target === null || onSegmentActivate === undefined
351
+ ? undefined
352
+ : onSegmentActivate;
353
+ const label = `${duplicateSegment.label} ~${formatTokens(
354
+ duplicateSegment.tokens,
355
+ )}`;
356
+ if (activate === undefined || target === null) {
357
+ return (
358
+ <span
359
+ key={`${duplicateSegment.path}-${segmentIndex}`}
360
+ className="inline-flex h-6 max-w-40 items-center rounded border border-border/60 px-1.5 text-[11px] text-muted-foreground"
361
+ title={label}
362
+ >
363
+ <span className="truncate">{label}</span>
364
+ </span>
365
+ );
366
+ }
367
+ return (
368
+ <button
369
+ key={`${duplicateSegment.path}-${segmentIndex}`}
370
+ type="button"
371
+ onClick={() => activate(target)}
372
+ className="inline-flex h-6 max-w-40 items-center rounded border border-border/60 px-1.5 text-[11px] text-muted-foreground hover:border-border hover:bg-muted/40 hover:text-foreground"
373
+ title="Jump to this duplicate block"
374
+ >
375
+ <span className="truncate">{label}</span>
376
+ </button>
377
+ );
378
+ })}
379
+ {hiddenSegmentCount > 0 && (
380
+ <span className="inline-flex h-6 items-center rounded border border-border/40 px-1.5 font-mono text-[11px] text-muted-foreground/70">
381
+ +{String(hiddenSegmentCount)}
305
382
  </span>
306
- );
307
- }
308
- return (
309
- <button
310
- key={`${duplicateSegment.path}-${segmentIndex}`}
311
- type="button"
312
- onClick={() => activate(target)}
313
- className="inline-flex h-6 max-w-40 items-center rounded border border-border/60 px-1.5 text-[11px] text-muted-foreground hover:border-border hover:bg-muted/40 hover:text-foreground"
314
- title="Jump to this duplicate block"
315
- >
316
- <span className="truncate">{label}</span>
317
- </button>
318
- );
319
- })}
320
- {hiddenSegmentCount > 0 && (
321
- <span className="inline-flex h-6 items-center rounded border border-border/40 px-1.5 font-mono text-[11px] text-muted-foreground/70">
322
- +{String(hiddenSegmentCount)}
323
- </span>
324
- )}
325
- </div>
326
- </div>
327
- );
328
- })}
329
- </div>
383
+ )}
384
+ </div>
385
+ </div>
386
+ );
387
+ })}
388
+ </div>
330
389
 
331
- {hiddenGroupCount > 0 && (
332
- <div className="border-t border-border/50 pt-2 font-mono text-[11px] text-muted-foreground">
333
- +{String(hiddenGroupCount)} more duplicate group{hiddenGroupCount === 1 ? "" : "s"}
334
- </div>
390
+ {hiddenGroupCount > 0 && (
391
+ <div className="border-t border-border/50 pt-2 font-mono text-[11px] text-muted-foreground">
392
+ +{String(hiddenGroupCount)} more duplicate group{hiddenGroupCount === 1 ? "" : "s"}
393
+ </div>
394
+ )}
395
+ </>
335
396
  )}
336
397
  </section>
337
398
  );
@@ -437,7 +498,10 @@ export function RequestAnatomy({
437
498
  <div className="px-4 py-3 space-y-3" data-testid="anatomy-root">
438
499
  <div className="flex flex-wrap items-start justify-between gap-3">
439
500
  <div className="min-w-0">
440
- <div className="text-sm font-semibold text-foreground">Request Context</div>
501
+ <div className="flex min-w-0 flex-wrap items-center gap-2">
502
+ <div className="text-sm font-semibold text-foreground">Request Context</div>
503
+ {intelligence !== null && <ContextHealthBadge health={intelligence.health} />}
504
+ </div>
441
505
  <div className={cn("mt-0.5 text-xs font-mono tabular-nums", summaryColorClass)}>
442
506
  Estimated ~{formatTokens(total)} tokens | {providerInputLabel} | {contextLimitLabel} |{" "}
443
507
  {usageLabel} | {segmentCountLabel}
@@ -8,6 +8,8 @@ export type ContextRiskLevel = "unknown" | "ok" | "watch" | "danger";
8
8
 
9
9
  export type ContextWindowSource = "request" | "provider" | "model-rule" | "unknown";
10
10
 
11
+ export type ContextHealthLevel = "ok" | "optimizable" | "risk";
12
+
11
13
  export type ContextWindow = {
12
14
  tokens: number | null;
13
15
  label: string;
@@ -46,6 +48,12 @@ export type DuplicateContextGroup = {
46
48
  segments: DuplicateContextSegment[];
47
49
  };
48
50
 
51
+ export type ContextHealth = {
52
+ level: ContextHealthLevel;
53
+ opportunityCount: number;
54
+ reclaimableTokens: number;
55
+ };
56
+
49
57
  export type ContextIntelligence = {
50
58
  model: string | null;
51
59
  contextWindow: ContextWindow;
@@ -60,6 +68,7 @@ export type ContextIntelligence = {
60
68
  largestRole: RoleUsage | null;
61
69
  diagnostics: ContextDiagnostic[];
62
70
  duplicateGroups: DuplicateContextGroup[];
71
+ health: ContextHealth;
63
72
  };
64
73
 
65
74
  type MatchMode = "prefix" | "includes";
@@ -158,7 +167,7 @@ const MODEL_CONTEXT_RULES: readonly ModelContextRule[] = [
158
167
  },
159
168
  {
160
169
  family: "DeepSeek V4",
161
- tokens: 1_000_000,
170
+ tokens: 1_048_576,
162
171
  mode: "prefix",
163
172
  patterns: ["deepseek-v4-pro", "deepseek-v4-flash"],
164
173
  },
@@ -170,15 +179,33 @@ const MODEL_CONTEXT_RULES: readonly ModelContextRule[] = [
170
179
  },
171
180
  {
172
181
  family: "MiniMax M3",
173
- tokens: 1_000_000,
182
+ tokens: 1_048_576,
174
183
  mode: "includes",
175
184
  patterns: ["minimax-m3"],
176
185
  },
177
186
  {
178
- family: "MiniMax M2",
187
+ family: "MiniMax M2.7",
179
188
  tokens: 204_800,
180
189
  mode: "includes",
181
- patterns: ["minimax-m2.7", "minimax-m2.5", "minimax-m2.1"],
190
+ patterns: ["minimax-m2.7"],
191
+ },
192
+ {
193
+ family: "MiniMax M2.5/M2.1",
194
+ tokens: 196_608,
195
+ mode: "includes",
196
+ patterns: ["minimax-m2.5", "minimax-m2.1"],
197
+ },
198
+ {
199
+ family: "GLM-5.2",
200
+ tokens: 1_048_576,
201
+ mode: "prefix",
202
+ patterns: ["glm-5.2"],
203
+ },
204
+ {
205
+ family: "GLM-5",
206
+ tokens: 202_752,
207
+ mode: "prefix",
208
+ patterns: ["glm-5", "glm-5.1"],
182
209
  },
183
210
  ];
184
211
 
@@ -551,6 +578,10 @@ function duplicateDiagnostic(
551
578
  };
552
579
  }
553
580
 
581
+ function duplicateReclaimableTokens(duplicateGroups: readonly DuplicateContextGroup[]): number {
582
+ return duplicateGroups.reduce((sum, group) => sum + group.repeatedTokens, 0);
583
+ }
584
+
554
585
  function diagnosticsForRolePressure(
555
586
  roleUsages: readonly RoleUsage[],
556
587
  total: number,
@@ -606,6 +637,33 @@ function diagnosticsForRolePressure(
606
637
  return diagnostics;
607
638
  }
608
639
 
640
+ function contextHealth({
641
+ risk,
642
+ contextWindow,
643
+ diagnostics,
644
+ duplicateGroups,
645
+ }: {
646
+ risk: ContextRiskLevel;
647
+ contextWindow: ContextWindow;
648
+ diagnostics: readonly ContextDiagnostic[];
649
+ duplicateGroups: readonly DuplicateContextGroup[];
650
+ }): ContextHealth {
651
+ const diagnosticKinds = new Set(diagnostics.map((diagnostic) => diagnostic.kind));
652
+ let opportunityCount = diagnosticKinds.size;
653
+ if (risk === "watch" || risk === "danger") opportunityCount += 1;
654
+ if (contextWindow.source === "unknown") opportunityCount += 1;
655
+
656
+ const hasDangerDiagnostic = diagnostics.some((diagnostic) => diagnostic.severity === "danger");
657
+ const level: ContextHealthLevel =
658
+ risk === "danger" || hasDangerDiagnostic ? "risk" : opportunityCount > 0 ? "optimizable" : "ok";
659
+
660
+ return {
661
+ level,
662
+ opportunityCount,
663
+ reclaimableTokens: duplicateReclaimableTokens(duplicateGroups),
664
+ };
665
+ }
666
+
609
667
  export function analyzeContextIntelligence({
610
668
  segments,
611
669
  inputTokens,
@@ -648,6 +706,7 @@ export function analyzeContextIntelligence({
648
706
  messageSegmentCount,
649
707
  );
650
708
  if (duplicate !== null) diagnostics.unshift(duplicate);
709
+ const risk = riskLevel(usagePercent);
651
710
 
652
711
  return {
653
712
  model: requestModel,
@@ -658,11 +717,17 @@ export function analyzeContextIntelligence({
658
717
  remainingInputTokens,
659
718
  remainingAfterReserveTokens,
660
719
  usagePercent,
661
- riskLevel: riskLevel(usagePercent),
720
+ riskLevel: risk,
662
721
  roleUsages,
663
722
  largestRole,
664
723
  diagnostics,
665
724
  duplicateGroups,
725
+ health: contextHealth({
726
+ risk,
727
+ contextWindow,
728
+ diagnostics,
729
+ duplicateGroups,
730
+ }),
666
731
  };
667
732
  }
668
733
 
@@ -0,0 +1,196 @@
1
+ import type { ProviderConfig } from "../../../lib/providerContract";
2
+ import type { CapturedLog } from "../../../proxy/schemas";
3
+ import { getLogFormatAdapter, resolveLogFormat } from "../log-formats";
4
+ import {
5
+ analyzeContextIntelligence,
6
+ type ContextHealth,
7
+ type ContextRiskLevel,
8
+ type ContextWindow,
9
+ type RoleUsage,
10
+ } from "./contextIntelligence";
11
+ import type { AnatomySegment } from "./types";
12
+
13
+ export type SessionContextSnapshot = {
14
+ logId: number;
15
+ model: string;
16
+ timestamp: string;
17
+ contextWindow: ContextWindow;
18
+ estimatedInputTokens: number;
19
+ windowUsedTokens: number | null;
20
+ usagePercent: number | null;
21
+ riskLevel: ContextRiskLevel;
22
+ health: ContextHealth;
23
+ diagnostics: string[];
24
+ roleSegments: AnatomySegment[];
25
+ roleTotalTokens: number;
26
+ };
27
+
28
+ export type SessionModelContextSummary = {
29
+ model: string;
30
+ requestCount: number;
31
+ first: SessionContextSnapshot;
32
+ latest: SessionContextSnapshot;
33
+ peak: SessionContextSnapshot;
34
+ trendPercent: number | null;
35
+ };
36
+
37
+ export type SessionContextSummary = {
38
+ models: SessionModelContextSummary[];
39
+ };
40
+
41
+ type SummaryAccumulator = {
42
+ model: string;
43
+ requestCount: number;
44
+ first: SessionContextSnapshot;
45
+ latest: SessionContextSnapshot;
46
+ peak: SessionContextSnapshot;
47
+ };
48
+
49
+ function parseRequestBody(rawBody: string | null): unknown | null {
50
+ if (rawBody === null) return null;
51
+ try {
52
+ const parsed: unknown = JSON.parse(rawBody);
53
+ return parsed;
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+
59
+ function roleUsageSegment(usage: RoleUsage): AnatomySegment {
60
+ return {
61
+ role: usage.role,
62
+ label: usage.label,
63
+ size: usage.tokens,
64
+ characters: usage.tokens * 4,
65
+ text: "",
66
+ path: `role:${usage.role}`,
67
+ };
68
+ }
69
+
70
+ function snapshotFromLog(
71
+ log: CapturedLog,
72
+ providers: readonly ProviderConfig[],
73
+ ): SessionContextSnapshot | null {
74
+ const parsed = parseRequestBody(log.rawRequestBody);
75
+ if (parsed === null) return null;
76
+
77
+ const adapter = getLogFormatAdapter(resolveLogFormat(log));
78
+ const segments = adapter.anatomySegments(parsed);
79
+ if (segments === null) return null;
80
+
81
+ const intelligence = analyzeContextIntelligence({
82
+ segments,
83
+ inputTokens: log.inputTokens ?? null,
84
+ parsed,
85
+ model: log.model,
86
+ providers,
87
+ });
88
+ const model = intelligence.model ?? log.model ?? "Unknown model";
89
+ const roleSegments = intelligence.roleUsages.map(roleUsageSegment);
90
+
91
+ return {
92
+ logId: log.id,
93
+ model,
94
+ timestamp: log.timestamp,
95
+ contextWindow: intelligence.contextWindow,
96
+ estimatedInputTokens: intelligence.estimatedInputTokens,
97
+ windowUsedTokens: intelligence.windowUsedTokens,
98
+ usagePercent: intelligence.usagePercent,
99
+ riskLevel: intelligence.riskLevel,
100
+ health: intelligence.health,
101
+ diagnostics: intelligence.diagnostics.map((diagnostic) => diagnostic.title),
102
+ roleSegments,
103
+ roleTotalTokens: roleSegments.reduce((sum, segment) => sum + segment.size, 0),
104
+ };
105
+ }
106
+
107
+ function isHigherPeak(candidate: SessionContextSnapshot, current: SessionContextSnapshot): boolean {
108
+ if (candidate.usagePercent !== null && current.usagePercent !== null) {
109
+ return candidate.usagePercent > current.usagePercent;
110
+ }
111
+ if (candidate.usagePercent !== null && current.usagePercent === null) return true;
112
+ if (candidate.usagePercent === null && current.usagePercent !== null) return false;
113
+ return candidate.estimatedInputTokens > current.estimatedInputTokens;
114
+ }
115
+
116
+ function riskRank(risk: ContextRiskLevel): number {
117
+ switch (risk) {
118
+ case "danger":
119
+ return 4;
120
+ case "watch":
121
+ return 3;
122
+ case "unknown":
123
+ return 2;
124
+ case "ok":
125
+ return 1;
126
+ }
127
+ }
128
+
129
+ function compareModelSummary(
130
+ left: SessionModelContextSummary,
131
+ right: SessionModelContextSummary,
132
+ ): number {
133
+ const riskDelta = riskRank(right.latest.riskLevel) - riskRank(left.latest.riskLevel);
134
+ if (riskDelta !== 0) return riskDelta;
135
+
136
+ const leftPeak = left.peak.usagePercent ?? -1;
137
+ const rightPeak = right.peak.usagePercent ?? -1;
138
+ const peakDelta = rightPeak - leftPeak;
139
+ if (peakDelta !== 0) return peakDelta;
140
+
141
+ return right.latest.logId - left.latest.logId;
142
+ }
143
+
144
+ function trendPercent(
145
+ first: SessionContextSnapshot,
146
+ latest: SessionContextSnapshot,
147
+ ): number | null {
148
+ if (first.usagePercent === null || latest.usagePercent === null) return null;
149
+ return (latest.usagePercent - first.usagePercent) * 100;
150
+ }
151
+
152
+ export function buildSessionContextSummary(
153
+ logs: readonly CapturedLog[],
154
+ providers: readonly ProviderConfig[] = [],
155
+ ): SessionContextSummary {
156
+ const sortedLogs = [...logs].sort((left, right) => left.id - right.id);
157
+ const byModel = new Map<string, SummaryAccumulator>();
158
+
159
+ for (const log of sortedLogs) {
160
+ const snapshot = snapshotFromLog(log, providers);
161
+ if (snapshot === null) continue;
162
+
163
+ const existing = byModel.get(snapshot.model);
164
+ if (existing === undefined) {
165
+ byModel.set(snapshot.model, {
166
+ model: snapshot.model,
167
+ requestCount: 1,
168
+ first: snapshot,
169
+ latest: snapshot,
170
+ peak: snapshot,
171
+ });
172
+ continue;
173
+ }
174
+
175
+ byModel.set(snapshot.model, {
176
+ model: existing.model,
177
+ requestCount: existing.requestCount + 1,
178
+ first: existing.first,
179
+ latest: snapshot,
180
+ peak: isHigherPeak(snapshot, existing.peak) ? snapshot : existing.peak,
181
+ });
182
+ }
183
+
184
+ const models = [...byModel.values()]
185
+ .map((entry) => ({
186
+ model: entry.model,
187
+ requestCount: entry.requestCount,
188
+ first: entry.first,
189
+ latest: entry.latest,
190
+ peak: entry.peak,
191
+ trendPercent: trendPercent(entry.first, entry.latest),
192
+ }))
193
+ .sort(compareModelSummary);
194
+
195
+ return { models };
196
+ }
@@ -0,0 +1,33 @@
1
+ import { safeGetOwnProperty } from "../../lib/objectUtils";
2
+
3
+ export const LOG_FOCUS_REQUEST_EVENT = "agent-inspector:focus-log";
4
+
5
+ export type LogFocusTab = "anatomy" | "request";
6
+
7
+ export type LogFocusRequest = {
8
+ logId: number;
9
+ tab: LogFocusTab;
10
+ };
11
+
12
+ export function dispatchLogFocusRequest(request: LogFocusRequest): void {
13
+ window.dispatchEvent(new CustomEvent(LOG_FOCUS_REQUEST_EVENT, { detail: request }));
14
+ }
15
+
16
+ export function readLogFocusRequest(event: Event): LogFocusRequest | null {
17
+ if (typeof CustomEvent === "undefined" || !(event instanceof CustomEvent)) return null;
18
+
19
+ const detail: unknown = event.detail;
20
+ const logId = safeGetOwnProperty(detail, "logId");
21
+ if (typeof logId !== "number" || !Number.isInteger(logId) || logId < 0) return null;
22
+
23
+ const tab = safeGetOwnProperty(detail, "tab");
24
+ switch (tab) {
25
+ case "anatomy":
26
+ case "request":
27
+ return { logId, tab };
28
+ case undefined:
29
+ return { logId, tab: "request" };
30
+ default:
31
+ return null;
32
+ }
33
+ }