@tonyclaw/agent-inspector 2.0.11 → 2.0.13

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-C9OWdxHM.js → CompareDrawer-CrMyE23D.js} +1 -1
  3. package/.output/public/assets/ProxyViewerContainer-BZuo0px3.js +114 -0
  4. package/.output/public/assets/{ReplayDialog-CwE6I1Pe.js → ReplayDialog-TQGhDdTa.js} +1 -1
  5. package/.output/public/assets/RequestAnatomy-D6kQYtfa.js +1 -0
  6. package/.output/public/assets/ResponseView-C6ybWuB8.js +1 -0
  7. package/.output/public/assets/{StreamingChunkSequence-CLcLZ7-W.js → StreamingChunkSequence-Bvs59_-I.js} +1 -1
  8. package/.output/public/assets/_sessionId-BNG_mnaH.js +1 -0
  9. package/.output/public/assets/index-BUxIwHhC.js +1 -0
  10. package/.output/public/assets/index-D_nZj9Vt.css +1 -0
  11. package/.output/public/assets/{main-D1Xanzu7.js → main-DvFSJlOd.js} +2 -2
  12. package/.output/server/_libs/lucide-react.mjs +51 -39
  13. package/.output/server/{_sessionId-D3IyPpws.mjs → _sessionId-DHYJxPb7.mjs} +2 -2
  14. package/.output/server/_ssr/{CompareDrawer-BIMThqxy.mjs → CompareDrawer-meimDBeA.mjs} +3 -3
  15. package/.output/server/_ssr/{ProxyViewerContainer-BiBdKWtj.mjs → ProxyViewerContainer-Co-WKq6u.mjs} +1610 -183
  16. package/.output/server/_ssr/{ReplayDialog-D_wiDyM2.mjs → ReplayDialog-cclcGyR9.mjs} +4 -4
  17. package/.output/server/_ssr/RequestAnatomy-to_rezzu.mjs +882 -0
  18. package/.output/server/_ssr/{ResponseView-Drk5ghmL.mjs → ResponseView-CZZI4IZd.mjs} +3 -3
  19. package/.output/server/_ssr/{StreamingChunkSequence-F889rmG0.mjs → StreamingChunkSequence-BTF4xSYX.mjs} +2 -2
  20. package/.output/server/_ssr/{index-CaPniq9z.mjs → index-D2fpJFP5.mjs} +2 -2
  21. package/.output/server/_ssr/index.mjs +2 -2
  22. package/.output/server/_ssr/{router-C7S8FRth.mjs → router-RDBAF-Iu.mjs} +85 -22
  23. package/.output/server/{_tanstack-start-manifest_v-CVTkFOdT.mjs → _tanstack-start-manifest_v-DB9sseoH.mjs} +1 -1
  24. package/.output/server/index.mjs +58 -58
  25. package/package.json +1 -1
  26. package/src/components/ProxyViewer.tsx +454 -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 +366 -99
  34. package/src/components/proxy-viewer/anatomy/contextIntelligence.ts +22 -4
  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-CNzay4u8.js +0 -114
  42. package/.output/public/assets/RequestAnatomy-DEehC3yz.js +0 -1
  43. package/.output/public/assets/ResponseView-CE5UsuUq.js +0 -1
  44. package/.output/public/assets/_sessionId-DE__tPTo.js +0 -1
  45. package/.output/public/assets/index-BFO4Jmw5.js +0 -1
  46. package/.output/public/assets/index-CDzZ7l_S.css +0 -1
  47. package/.output/server/_ssr/RequestAnatomy-5UiMTESr.mjs +0 -1383
@@ -1,11 +1,12 @@
1
1
  import { type JSX, useCallback, useEffect, useMemo, useRef, useState, Suspense } from "react";
2
- import { ArrowLeft, Check, Copy, Download, Plus } from "lucide-react";
2
+ import { ArrowDownRight, ArrowLeft, ArrowUpRight, Check, Copy, Download, Plus } from "lucide-react";
3
3
 
4
4
  import type { CapturedLog } from "../proxy/schemas";
5
5
  import { exportLogsAsZip } from "../lib/export-logs";
6
6
  import type { TimeDisplayFormat } from "../lib/runtimeConfig";
7
7
  import { formatTimestampRange } from "../lib/timeDisplay";
8
- import { formatTokens } from "../lib/utils";
8
+ import { cn, formatContextWindowTokens, formatTokens } from "../lib/utils";
9
+ import { useProviders } from "../lib/useProviders";
9
10
  import packageJson from "../../package.json";
10
11
  import { ConversationGroup, groupLogsByConversation } from "./proxy-viewer";
11
12
 
@@ -17,8 +18,19 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/t
17
18
  import { SettingsDialog } from "./providers/SettingsDialog";
18
19
  import { computeCacheTrends } from "./proxy-viewer/cacheTrend";
19
20
  import { LazyCompareDrawer } from "./proxy-viewer/lazy";
21
+ import { dispatchLogFocusRequest } from "./proxy-viewer/logFocus";
20
22
  import { buildValidPredecessors } from "./proxy-viewer/viewerState";
21
23
  import { useKeyboardNavigation } from "./proxy-viewer/useKeyboardNavigation";
24
+ import { SegmentBar } from "./proxy-viewer/anatomy/SegmentBar";
25
+ import type {
26
+ ContextHealthLevel,
27
+ ContextRiskLevel,
28
+ } from "./proxy-viewer/anatomy/contextIntelligence";
29
+ import {
30
+ buildSessionContextSummary,
31
+ type SessionContextSummary,
32
+ type SessionModelContextSummary,
33
+ } from "./proxy-viewer/anatomy/sessionContextSummary";
22
34
 
23
35
  function truncateSessionId(id: string): string {
24
36
  if (id.length <= 30) return id;
@@ -35,6 +47,12 @@ function computeTokenSummary(logs: CapturedLog[]): { totalIn: number; totalOut:
35
47
  return { totalIn, totalOut };
36
48
  }
37
49
 
50
+ export type SessionContextScope = {
51
+ label: string;
52
+ logs: CapturedLog[];
53
+ showBackLink: boolean;
54
+ };
55
+
38
56
  function formatTimeRange(logs: CapturedLog[], timeDisplayFormat: TimeDisplayFormat): string | null {
39
57
  const first = logs[0];
40
58
  const last = logs[logs.length - 1];
@@ -51,6 +69,177 @@ function getFirstUserAgent(logs: CapturedLog[]): string | null {
51
69
  return null;
52
70
  }
53
71
 
72
+ export function resolveSessionContextScope({
73
+ pinnedSessionId,
74
+ selectedSession,
75
+ sessions,
76
+ logs,
77
+ }: {
78
+ pinnedSessionId: string | undefined;
79
+ selectedSession: string;
80
+ sessions: readonly string[];
81
+ logs: readonly CapturedLog[];
82
+ }): SessionContextScope | null {
83
+ if (pinnedSessionId !== undefined) {
84
+ const scopedLogs = logs.filter((log) => log.sessionId === pinnedSessionId);
85
+ if (scopedLogs.length === 0) return null;
86
+ return { label: pinnedSessionId, logs: scopedLogs, showBackLink: true };
87
+ }
88
+
89
+ if (selectedSession !== "__all__") {
90
+ const scopedLogs = logs.filter((log) => log.sessionId === selectedSession);
91
+ if (scopedLogs.length === 0) return null;
92
+ return { label: selectedSession, logs: scopedLogs, showBackLink: true };
93
+ }
94
+
95
+ const singleSessionId = sessions.length === 1 ? (sessions[0] ?? null) : null;
96
+ if (singleSessionId !== null) {
97
+ const allVisibleLogsBelongToSession = logs.every((log) => log.sessionId === singleSessionId);
98
+ if (allVisibleLogsBelongToSession) {
99
+ return { label: singleSessionId, logs: [...logs], showBackLink: false };
100
+ }
101
+ }
102
+
103
+ const allVisibleLogsAreSessionless =
104
+ sessions.length === 0 && logs.length > 0 && logs.every((log) => log.sessionId === null);
105
+ if (allVisibleLogsAreSessionless) {
106
+ return { label: "Visible requests", logs: [...logs], showBackLink: false };
107
+ }
108
+
109
+ return null;
110
+ }
111
+
112
+ function formatPercent(value: number | null): string {
113
+ if (value === null) return "Unknown";
114
+ const percent = value * 100;
115
+ if (percent >= 10) return `${percent.toFixed(0)}%`;
116
+ if (percent >= 1) return `${percent.toFixed(1)}%`;
117
+ if (percent > 0) return "<1%";
118
+ return "0%";
119
+ }
120
+
121
+ function formatTrend(value: number | null): string {
122
+ if (value === null) return "n/a";
123
+ if (Math.abs(value) < 0.1) return "0%";
124
+ const sign = value > 0 ? "+" : "";
125
+ return `${sign}${value.toFixed(1)}%`;
126
+ }
127
+
128
+ function progressPercent(value: number | null): number {
129
+ if (value === null || !Number.isFinite(value)) return 0;
130
+ return Math.min(Math.max(value * 100, 0), 100);
131
+ }
132
+
133
+ function peakMarkerPercent(
134
+ latestPercent: number | null,
135
+ peakPercent: number | null,
136
+ ): number | null {
137
+ if (peakPercent === null) return null;
138
+ const latest = progressPercent(latestPercent);
139
+ const peak = progressPercent(peakPercent);
140
+ if (peak <= 0) return null;
141
+ if (Math.abs(peak - latest) < 0.5) return null;
142
+ return peak;
143
+ }
144
+
145
+ function sessionRiskBarClass(risk: ContextRiskLevel): string {
146
+ switch (risk) {
147
+ case "danger":
148
+ return "bg-red-400";
149
+ case "watch":
150
+ return "bg-amber-300";
151
+ case "unknown":
152
+ return "bg-muted-foreground/50";
153
+ case "ok":
154
+ return "bg-emerald-400";
155
+ }
156
+ }
157
+
158
+ function formatContextWindow(summary: SessionModelContextSummary): string {
159
+ const tokens = summary.latest.contextWindow.tokens;
160
+ if (tokens === null) return "Context window unknown";
161
+ return `Context window ${formatContextWindowTokens(tokens)}`;
162
+ }
163
+
164
+ function formatRequestCount(count: number): string {
165
+ return `${count} ${count === 1 ? "request" : "requests"}`;
166
+ }
167
+
168
+ function formatLogReference(logId: number): string {
169
+ return `#${String(logId)}`;
170
+ }
171
+
172
+ function sessionRiskClass(risk: ContextRiskLevel): string {
173
+ switch (risk) {
174
+ case "danger":
175
+ return "text-red-300";
176
+ case "watch":
177
+ return "text-amber-200";
178
+ case "unknown":
179
+ return "text-muted-foreground";
180
+ case "ok":
181
+ return "text-emerald-300";
182
+ }
183
+ }
184
+
185
+ function sessionHealthClass(level: ContextHealthLevel): string {
186
+ switch (level) {
187
+ case "risk":
188
+ return "border-red-400/30 bg-red-500/10 text-red-100";
189
+ case "optimizable":
190
+ return "border-amber-400/25 bg-amber-400/8 text-amber-100";
191
+ case "ok":
192
+ return "border-emerald-400/25 bg-emerald-400/8 text-emerald-100";
193
+ }
194
+ }
195
+
196
+ function sessionHealthLabel(level: ContextHealthLevel): string {
197
+ switch (level) {
198
+ case "risk":
199
+ return "Risk";
200
+ case "optimizable":
201
+ return "Optimizable";
202
+ case "ok":
203
+ return "OK";
204
+ }
205
+ }
206
+
207
+ const LOG_FOCUS_RETRY_LIMIT = 10;
208
+ const LOG_FOCUS_RETRY_DELAY_MS = 80;
209
+
210
+ function highlightLogTarget(target: HTMLElement): void {
211
+ target.setAttribute("data-deep-link-highlight", "true");
212
+ window.setTimeout(() => {
213
+ target.removeAttribute("data-deep-link-highlight");
214
+ }, 1800);
215
+ }
216
+
217
+ function focusLogTarget(logId: number): boolean {
218
+ const target = document.getElementById(`log-${String(logId)}`);
219
+ if (!(target instanceof HTMLElement)) return false;
220
+ target.scrollIntoView({ block: "center", behavior: "smooth" });
221
+ target.focus({ preventScroll: true });
222
+ highlightLogTarget(target);
223
+ return true;
224
+ }
225
+
226
+ function openLogContext(logId: number): void {
227
+ dispatchLogFocusRequest({ logId, tab: "anatomy" });
228
+
229
+ let attempts = 0;
230
+ const tryFocus = (): void => {
231
+ dispatchLogFocusRequest({ logId, tab: "anatomy" });
232
+ if (focusLogTarget(logId)) return;
233
+
234
+ attempts += 1;
235
+ if (attempts < LOG_FOCUS_RETRY_LIMIT) {
236
+ window.setTimeout(tryFocus, LOG_FOCUS_RETRY_DELAY_MS);
237
+ }
238
+ };
239
+
240
+ window.setTimeout(tryFocus, 0);
241
+ }
242
+
54
243
  function CopyableCommand({ command }: { command: string }): JSX.Element {
55
244
  const [copied, setCopied] = useState(false);
56
245
 
@@ -123,18 +312,210 @@ function McpReadyBadge(): JSX.Element {
123
312
  );
124
313
  }
125
314
 
315
+ function SessionContextLogButton({ label, logId }: { label: string; logId: number }): JSX.Element {
316
+ return (
317
+ <button
318
+ type="button"
319
+ onClick={() => openLogContext(logId)}
320
+ 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"
321
+ title={`Open Context for ${formatLogReference(logId)}`}
322
+ >
323
+ {label} {formatLogReference(logId)}
324
+ </button>
325
+ );
326
+ }
327
+
328
+ function SessionContextProgressBar({
329
+ latestPercent,
330
+ peakPercent,
331
+ risk,
332
+ onOpenContext,
333
+ }: {
334
+ latestPercent: number | null;
335
+ peakPercent: number | null;
336
+ risk: ContextRiskLevel;
337
+ onOpenContext: () => void;
338
+ }): JSX.Element {
339
+ const latest = progressPercent(latestPercent);
340
+ const peakMarker = peakMarkerPercent(latestPercent, peakPercent);
341
+ const ariaLabel =
342
+ peakPercent === null
343
+ ? `Latest context window usage ${formatPercent(latestPercent)}`
344
+ : `Latest context window usage ${formatPercent(latestPercent)}, peak ${formatPercent(
345
+ peakPercent,
346
+ )}`;
347
+
348
+ return (
349
+ <button
350
+ type="button"
351
+ onClick={onOpenContext}
352
+ className="group block w-full rounded-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1"
353
+ aria-label={`${ariaLabel}. Open latest Context details`}
354
+ title="Open latest Context details"
355
+ >
356
+ <span
357
+ className="relative block h-2.5 overflow-hidden rounded-full border border-border/70 bg-background transition-colors group-hover:border-foreground/30"
358
+ role="meter"
359
+ aria-label={ariaLabel}
360
+ aria-valuemin={0}
361
+ aria-valuemax={100}
362
+ aria-valuenow={Math.round(latest)}
363
+ >
364
+ <span
365
+ className={cn("block h-full rounded-full transition-[width]", sessionRiskBarClass(risk))}
366
+ style={{ width: `${latest}%` }}
367
+ />
368
+ {peakMarker !== null && (
369
+ <span
370
+ className="absolute top-0 h-full w-0.5 rounded-full bg-amber-100/75 shadow-[0_0_0_1px_rgba(0,0,0,0.25)]"
371
+ style={{ left: `calc(${peakMarker}% - 0.5px)` }}
372
+ aria-hidden="true"
373
+ />
374
+ )}
375
+ </span>
376
+ </button>
377
+ );
378
+ }
379
+
380
+ function SessionModelContextRow({ summary }: { summary: SessionModelContextSummary }): JSX.Element {
381
+ const trend = summary.trendPercent;
382
+ const TrendIcon =
383
+ trend === null || Math.abs(trend) < 0.1 ? null : trend > 0 ? ArrowUpRight : ArrowDownRight;
384
+ const diagnosticLabel = summary.latest.diagnostics[0] ?? null;
385
+ const latestUsage = formatPercent(summary.latest.usagePercent);
386
+ const peakUsage = formatPercent(summary.peak.usagePercent);
387
+ const openLatestContext = (): void => {
388
+ openLogContext(summary.latest.logId);
389
+ };
390
+
391
+ return (
392
+ <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)]">
393
+ <div className="min-w-0">
394
+ <div className="flex min-w-0 items-center gap-2">
395
+ <button
396
+ type="button"
397
+ onClick={openLatestContext}
398
+ className="min-w-0 truncate rounded-sm text-left font-mono font-semibold text-foreground transition-colors hover:text-cyan-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1"
399
+ title={`Open latest Context for ${summary.model}`}
400
+ >
401
+ {summary.model}
402
+ </button>
403
+ <span
404
+ className={cn(
405
+ "inline-flex h-5 shrink-0 items-center rounded border px-1.5 font-mono text-[10px]",
406
+ sessionHealthClass(summary.latest.health.level),
407
+ )}
408
+ >
409
+ {sessionHealthLabel(summary.latest.health.level)}
410
+ </span>
411
+ </div>
412
+ <div className="mt-1 flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 text-[11px] text-muted-foreground">
413
+ <span>{formatRequestCount(summary.requestCount)}</span>
414
+ <span>{formatContextWindow(summary)}</span>
415
+ </div>
416
+ </div>
417
+
418
+ <div className="min-w-0 space-y-2 text-xs">
419
+ <SessionContextProgressBar
420
+ latestPercent={summary.latest.usagePercent}
421
+ peakPercent={summary.peak.usagePercent}
422
+ risk={summary.latest.riskLevel}
423
+ onOpenContext={openLatestContext}
424
+ />
425
+ <div className="grid grid-cols-3 gap-2">
426
+ <div className="min-w-0">
427
+ <div className="text-[10px] uppercase text-muted-foreground">Latest</div>
428
+ <div className={cn("mt-0.5 font-mono", sessionRiskClass(summary.latest.riskLevel))}>
429
+ {latestUsage}
430
+ </div>
431
+ <SessionContextLogButton label="Log" logId={summary.latest.logId} />
432
+ </div>
433
+ <div className="min-w-0">
434
+ <div className="text-[10px] uppercase text-muted-foreground">Peak</div>
435
+ <div className={cn("mt-0.5 font-mono", sessionRiskClass(summary.peak.riskLevel))}>
436
+ {peakUsage}
437
+ </div>
438
+ <SessionContextLogButton label="Log" logId={summary.peak.logId} />
439
+ </div>
440
+ <div className="min-w-0">
441
+ <div className="text-[10px] uppercase text-muted-foreground">Trend</div>
442
+ <div className="mt-0.5 flex items-center gap-1 font-mono text-foreground">
443
+ {TrendIcon !== null && (
444
+ <TrendIcon
445
+ className={cn(
446
+ "size-3",
447
+ trend !== null && trend > 0 ? "text-amber-300" : "text-emerald-300",
448
+ )}
449
+ />
450
+ )}
451
+ {formatTrend(trend)}
452
+ </div>
453
+ <div className="h-5" aria-hidden="true" />
454
+ </div>
455
+ </div>
456
+ </div>
457
+
458
+ <div className="min-w-0 space-y-1.5">
459
+ <SegmentBar
460
+ segments={summary.latest.roleSegments}
461
+ totalTokens={summary.latest.roleTotalTokens}
462
+ showLabels={false}
463
+ />
464
+ <div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 text-[11px] text-muted-foreground">
465
+ <span className="font-mono">
466
+ ~{formatTokens(summary.latest.estimatedInputTokens)} input
467
+ </span>
468
+ {summary.latest.windowUsedTokens !== null && (
469
+ <span className="font-mono">~{formatTokens(summary.latest.windowUsedTokens)} used</span>
470
+ )}
471
+ {diagnosticLabel !== null && <span className="truncate">{diagnosticLabel}</span>}
472
+ </div>
473
+ </div>
474
+ </div>
475
+ );
476
+ }
477
+
478
+ function SessionContextSummaryPanel({
479
+ summary,
480
+ }: {
481
+ summary: SessionContextSummary;
482
+ }): JSX.Element | null {
483
+ if (summary.models.length === 0) return null;
484
+ return (
485
+ <div className="mt-3 border-t border-border/70 pt-2">
486
+ <div className="mb-1 flex items-center justify-between gap-2">
487
+ <div className="text-[11px] font-semibold uppercase tracking-normal text-muted-foreground">
488
+ Session Context
489
+ </div>
490
+ <div className="font-mono text-[10px] text-muted-foreground">
491
+ {summary.models.length} model{summary.models.length === 1 ? "" : "s"}
492
+ </div>
493
+ </div>
494
+ <div className="max-h-52 overflow-y-auto pr-1">
495
+ {summary.models.map((modelSummary) => (
496
+ <SessionModelContextRow key={modelSummary.model} summary={modelSummary} />
497
+ ))}
498
+ </div>
499
+ </div>
500
+ );
501
+ }
502
+
126
503
  function SessionContextBar({
127
- sessionId,
504
+ label,
128
505
  logs,
129
506
  totalIn,
130
507
  totalOut,
131
508
  timeDisplayFormat,
509
+ contextSummary,
510
+ showBackLink,
132
511
  }: {
133
- sessionId: string;
512
+ label: string;
134
513
  logs: CapturedLog[];
135
514
  totalIn: number;
136
515
  totalOut: number;
137
516
  timeDisplayFormat: TimeDisplayFormat;
517
+ contextSummary: SessionContextSummary;
518
+ showBackLink: boolean;
138
519
  }): JSX.Element {
139
520
  const [copied, setCopied] = useState(false);
140
521
  const timeRange = useMemo(
@@ -151,50 +532,55 @@ function SessionContextBar({
151
532
  }, []);
152
533
 
153
534
  return (
154
- <div className="mb-4 flex items-center gap-3 border border-border rounded-md bg-muted/20 px-3 py-2 text-xs">
155
- <a
156
- href="/"
157
- className="inline-flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
158
- aria-label="Back to all sessions"
159
- title="Back to all sessions"
160
- >
161
- <ArrowLeft className="size-3.5" />
162
- </a>
163
- <div className="min-w-0 flex-1">
164
- <div className="flex min-w-0 items-center gap-2">
165
- <span className="font-mono font-semibold text-purple-400/90 truncate" title={sessionId}>
166
- {truncateSessionId(sessionId)}
167
- </span>
168
- {userAgent !== null && (
169
- <span
170
- className="font-mono text-muted-foreground truncate max-w-[220px]"
171
- title={userAgent}
172
- >
173
- {userAgent}
535
+ <div className="mb-4 border border-border rounded-md bg-muted/20 px-3 py-2 text-xs">
536
+ <div className="flex items-center gap-3">
537
+ {showBackLink && (
538
+ <a
539
+ href="/"
540
+ className="inline-flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
541
+ aria-label="Back to all sessions"
542
+ title="Back to all sessions"
543
+ >
544
+ <ArrowLeft className="size-3.5" />
545
+ </a>
546
+ )}
547
+ <div className="min-w-0 flex-1">
548
+ <div className="flex min-w-0 items-center gap-2">
549
+ <span className="font-mono font-semibold text-purple-400/90 truncate" title={label}>
550
+ {truncateSessionId(label)}
174
551
  </span>
175
- )}
176
- </div>
177
- <div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-muted-foreground">
178
- <span>
179
- {logs.length} request{logs.length !== 1 ? "s" : ""}
180
- </span>
181
- {timeRange !== null && <span>{timeRange}</span>}
182
- {(totalIn > 0 || totalOut > 0) && (
183
- <span className="font-mono">
184
- {formatTokens(totalIn)} in / {formatTokens(totalOut)} out
552
+ {userAgent !== null && (
553
+ <span
554
+ className="font-mono text-muted-foreground truncate max-w-[220px]"
555
+ title={userAgent}
556
+ >
557
+ {userAgent}
558
+ </span>
559
+ )}
560
+ </div>
561
+ <div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-muted-foreground">
562
+ <span>
563
+ {logs.length} request{logs.length !== 1 ? "s" : ""}
185
564
  </span>
186
- )}
565
+ {timeRange !== null && <span>{timeRange}</span>}
566
+ {(totalIn > 0 || totalOut > 0) && (
567
+ <span className="font-mono">
568
+ {formatTokens(totalIn)} in / {formatTokens(totalOut)} out
569
+ </span>
570
+ )}
571
+ </div>
187
572
  </div>
573
+ <button
574
+ type="button"
575
+ onClick={handleCopyLink}
576
+ className="inline-flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
577
+ aria-label={copied ? "Copied session link" : "Copy session link"}
578
+ title={copied ? "Copied session link" : "Copy session link"}
579
+ >
580
+ {copied ? <Check className="size-3.5" /> : <Copy className="size-3.5" />}
581
+ </button>
188
582
  </div>
189
- <button
190
- type="button"
191
- onClick={handleCopyLink}
192
- className="inline-flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
193
- aria-label={copied ? "Copied session link" : "Copy session link"}
194
- title={copied ? "Copied session link" : "Copy session link"}
195
- >
196
- {copied ? <Check className="size-3.5" /> : <Copy className="size-3.5" />}
197
- </button>
583
+ <SessionContextSummaryPanel summary={contextSummary} />
198
584
  </div>
199
585
  );
200
586
  }
@@ -246,6 +632,7 @@ export function ProxyViewer({
246
632
  pinnedSessionId,
247
633
  }: ProxyViewerProps): JSX.Element {
248
634
  const { totalIn, totalOut } = useMemo(() => computeTokenSummary(logs), [logs]);
635
+ const { providers } = useProviders();
249
636
  const [exporting, setExporting] = useState(false);
250
637
  const [comparePair, setComparePair] = useState<[CapturedLog, CapturedLog] | null>(null);
251
638
  const [crabEntrancePhase, setCrabEntrancePhase] = useState<"hidden" | "playing" | "done">(
@@ -295,7 +682,20 @@ export function ProxyViewer({
295
682
  }, []);
296
683
 
297
684
  const groups = useMemo(() => groupLogsByConversation(logs), [logs]);
298
- const hasPinnedSessionContext = pinnedSessionId !== undefined;
685
+ const sessionContextScope = useMemo(
686
+ () => resolveSessionContextScope({ pinnedSessionId, selectedSession, sessions, logs }),
687
+ [logs, pinnedSessionId, selectedSession, sessions],
688
+ );
689
+ const sessionContextLogs = sessionContextScope?.logs ?? [];
690
+ const hasSessionContext = sessionContextScope !== null && sessionContextLogs.length > 0;
691
+ const { totalIn: sessionContextTotalIn, totalOut: sessionContextTotalOut } = useMemo(
692
+ () => computeTokenSummary(sessionContextLogs),
693
+ [sessionContextLogs],
694
+ );
695
+ const sessionContextSummary = useMemo(
696
+ () => buildSessionContextSummary(sessionContextLogs, providers),
697
+ [sessionContextLogs, providers],
698
+ );
299
699
  const cacheTrends = useMemo(() => computeCacheTrends(groups), [groups]);
300
700
  const comparisonPredecessors = useMemo(() => buildValidPredecessors(groups), [groups]);
301
701
  const handleCompareWithPrevious = useCallback(
@@ -379,13 +779,15 @@ export function ProxyViewer({
379
779
  </div>
380
780
  </div>
381
781
 
382
- {pinnedSessionId !== undefined && (
782
+ {hasSessionContext && sessionContextScope !== null && (
383
783
  <SessionContextBar
384
- sessionId={pinnedSessionId}
385
- logs={logs}
386
- totalIn={totalIn}
387
- totalOut={totalOut}
784
+ label={sessionContextScope.label}
785
+ logs={sessionContextLogs}
786
+ totalIn={sessionContextTotalIn}
787
+ totalOut={sessionContextTotalOut}
388
788
  timeDisplayFormat={timeDisplayFormat}
789
+ contextSummary={sessionContextSummary}
790
+ showBackLink={sessionContextScope.showBackLink}
389
791
  />
390
792
  )}
391
793
 
@@ -444,7 +846,7 @@ export function ProxyViewer({
444
846
  </button>
445
847
  </div>
446
848
  <div className="flex-1" />
447
- {!hasPinnedSessionContext && (
849
+ {!hasSessionContext && (
448
850
  <span className="text-muted-foreground text-xs font-mono">
449
851
  {logs.length} request{logs.length !== 1 ? "s" : ""}
450
852
  {totalIn > 0 || totalOut > 0
@@ -535,7 +937,7 @@ export function ProxyViewer({
535
937
  comparisonPredecessors={comparisonPredecessors}
536
938
  onClearGroup={onClearGroup}
537
939
  standalone={groups.length === 1}
538
- hasPinnedSessionContext={hasPinnedSessionContext}
940
+ hasPinnedSessionContext={hasSessionContext}
539
941
  timeDisplayFormat={timeDisplayFormat}
540
942
  />
541
943
  ))}
@@ -27,7 +27,7 @@ import {
27
27
  findProviderModelMetadata,
28
28
  providerHasContextMetadata,
29
29
  } from "../../lib/providerModelMetadata";
30
- import { formatTokens } from "../../lib/utils";
30
+ import { formatContextWindowTokens } from "../../lib/utils";
31
31
  import type {
32
32
  ProviderTestErrorType as ErrorType,
33
33
  ProviderTestResult as TestResult,
@@ -215,6 +215,30 @@ function formatTimeAgo(isoString: string): string {
215
215
  return `${diffDay}d ago`;
216
216
  }
217
217
 
218
+ type ModelSourceLink = {
219
+ label: string;
220
+ title: string;
221
+ url: string;
222
+ };
223
+
224
+ function getModelSourceLink(sourceUrl: string | undefined): ModelSourceLink | null {
225
+ if (sourceUrl === undefined || sourceUrl.trim() === "") return null;
226
+ try {
227
+ const parsed = new URL(sourceUrl);
228
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return null;
229
+
230
+ const hostname = parsed.hostname.toLowerCase();
231
+ const isHuggingFace = hostname === "huggingface.co" || hostname.endsWith(".huggingface.co");
232
+ return {
233
+ label: isHuggingFace ? "HF" : "Source",
234
+ title: isHuggingFace ? "Open Hugging Face model source" : "Open model metadata source",
235
+ url: sourceUrl,
236
+ };
237
+ } catch {
238
+ return null;
239
+ }
240
+ }
241
+
218
242
  export function ProviderCard({
219
243
  provider,
220
244
  testResults,
@@ -298,6 +322,7 @@ export function ProviderCard({
298
322
  <div className="flex flex-wrap gap-1">
299
323
  {provider.models.map((m) => {
300
324
  const metadata = findProviderModelMetadata(provider, m);
325
+ const sourceLink = getModelSourceLink(metadata?.sourceUrl);
301
326
  return (
302
327
  <span
303
328
  key={m}
@@ -306,9 +331,27 @@ export function ProviderCard({
306
331
  <span>{m}</span>
307
332
  {metadata !== null && metadata.contextWindow !== undefined && (
308
333
  <span className="font-mono text-foreground/80">
309
- {formatTokens(metadata.contextWindow)}
334
+ {formatContextWindowTokens(metadata.contextWindow)}
310
335
  </span>
311
336
  )}
337
+ {sourceLink !== null && (
338
+ <TooltipProvider>
339
+ <Tooltip>
340
+ <TooltipTrigger asChild>
341
+ <a
342
+ href={sourceLink.url}
343
+ target="_blank"
344
+ rel="noopener noreferrer"
345
+ className="inline-flex items-center gap-0.5 rounded border border-border/70 bg-background/70 px-1 font-mono text-[10px] leading-4 text-muted-foreground transition-colors hover:text-foreground"
346
+ aria-label={sourceLink.title}
347
+ >
348
+ {sourceLink.label}
349
+ </a>
350
+ </TooltipTrigger>
351
+ <TooltipContent>{sourceLink.title}</TooltipContent>
352
+ </Tooltip>
353
+ </TooltipProvider>
354
+ )}
312
355
  </span>
313
356
  );
314
357
  })}