@tonyclaw/agent-inspector 2.0.11 → 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-C9OWdxHM.js → CompareDrawer-ClM_uVRy.js} +1 -1
  3. package/.output/public/assets/ProxyViewerContainer-CTNNzXSa.js +114 -0
  4. package/.output/public/assets/{ReplayDialog-CwE6I1Pe.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-CLcLZ7-W.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-D1Xanzu7.js → main-FvIQopAy.js} +2 -2
  12. package/.output/server/_libs/lucide-react.mjs +51 -39
  13. package/.output/server/{_sessionId-D3IyPpws.mjs → _sessionId-BDSgtrsI.mjs} +2 -2
  14. package/.output/server/_ssr/{CompareDrawer-BIMThqxy.mjs → CompareDrawer-Cqvv0I3x.mjs} +3 -3
  15. package/.output/server/_ssr/{ProxyViewerContainer-BiBdKWtj.mjs → ProxyViewerContainer-C0YEoG8N.mjs} +1611 -183
  16. package/.output/server/_ssr/{ReplayDialog-D_wiDyM2.mjs → ReplayDialog-CrsynUKI.mjs} +4 -4
  17. package/.output/server/_ssr/RequestAnatomy-CR_JUkcL.mjs +688 -0
  18. package/.output/server/_ssr/{ResponseView-Drk5ghmL.mjs → ResponseView-Dba2Et69.mjs} +3 -3
  19. package/.output/server/_ssr/{StreamingChunkSequence-F889rmG0.mjs → StreamingChunkSequence-BIMHLstI.mjs} +2 -2
  20. package/.output/server/_ssr/{index-CaPniq9z.mjs → index-DveIUTm5.mjs} +2 -2
  21. package/.output/server/_ssr/index.mjs +2 -2
  22. package/.output/server/_ssr/{router-C7S8FRth.mjs → router-D0yXVG0Q.mjs} +85 -22
  23. package/.output/server/{_tanstack-start-manifest_v-CVTkFOdT.mjs → _tanstack-start-manifest_v-pe0xJFuQ.mjs} +1 -1
  24. package/.output/server/index.mjs +62 -62
  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 +99 -79
  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,173 @@ 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 "Window unknown";
161
+ return `Window ${formatContextWindowTokens(tokens)}`;
162
+ }
163
+
164
+ function formatLogReference(logId: number): string {
165
+ return `#${String(logId)}`;
166
+ }
167
+
168
+ function sessionRiskClass(risk: ContextRiskLevel): string {
169
+ switch (risk) {
170
+ case "danger":
171
+ return "text-red-300";
172
+ case "watch":
173
+ return "text-amber-200";
174
+ case "unknown":
175
+ return "text-muted-foreground";
176
+ case "ok":
177
+ return "text-emerald-300";
178
+ }
179
+ }
180
+
181
+ function sessionHealthClass(level: ContextHealthLevel): string {
182
+ switch (level) {
183
+ case "risk":
184
+ return "border-red-400/30 bg-red-500/10 text-red-100";
185
+ case "optimizable":
186
+ return "border-amber-400/25 bg-amber-400/8 text-amber-100";
187
+ case "ok":
188
+ return "border-emerald-400/25 bg-emerald-400/8 text-emerald-100";
189
+ }
190
+ }
191
+
192
+ function sessionHealthLabel(level: ContextHealthLevel): string {
193
+ switch (level) {
194
+ case "risk":
195
+ return "Risk";
196
+ case "optimizable":
197
+ return "Optimizable";
198
+ case "ok":
199
+ return "OK";
200
+ }
201
+ }
202
+
203
+ const LOG_FOCUS_RETRY_LIMIT = 10;
204
+ const LOG_FOCUS_RETRY_DELAY_MS = 80;
205
+
206
+ function highlightLogTarget(target: HTMLElement): void {
207
+ target.setAttribute("data-deep-link-highlight", "true");
208
+ window.setTimeout(() => {
209
+ target.removeAttribute("data-deep-link-highlight");
210
+ }, 1800);
211
+ }
212
+
213
+ function focusLogTarget(logId: number): boolean {
214
+ const target = document.getElementById(`log-${String(logId)}`);
215
+ if (!(target instanceof HTMLElement)) return false;
216
+ target.scrollIntoView({ block: "center", behavior: "smooth" });
217
+ target.focus({ preventScroll: true });
218
+ highlightLogTarget(target);
219
+ return true;
220
+ }
221
+
222
+ function openLogContext(logId: number): void {
223
+ dispatchLogFocusRequest({ logId, tab: "anatomy" });
224
+
225
+ let attempts = 0;
226
+ const tryFocus = (): void => {
227
+ dispatchLogFocusRequest({ logId, tab: "anatomy" });
228
+ if (focusLogTarget(logId)) return;
229
+
230
+ attempts += 1;
231
+ if (attempts < LOG_FOCUS_RETRY_LIMIT) {
232
+ window.setTimeout(tryFocus, LOG_FOCUS_RETRY_DELAY_MS);
233
+ }
234
+ };
235
+
236
+ window.setTimeout(tryFocus, 0);
237
+ }
238
+
54
239
  function CopyableCommand({ command }: { command: string }): JSX.Element {
55
240
  const [copied, setCopied] = useState(false);
56
241
 
@@ -123,18 +308,213 @@ function McpReadyBadge(): JSX.Element {
123
308
  );
124
309
  }
125
310
 
311
+ function SessionContextLogButton({ label, logId }: { label: string; logId: number }): JSX.Element {
312
+ return (
313
+ <button
314
+ type="button"
315
+ onClick={() => openLogContext(logId)}
316
+ 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"
317
+ title={`Open Context for ${formatLogReference(logId)}`}
318
+ >
319
+ {label} {formatLogReference(logId)}
320
+ </button>
321
+ );
322
+ }
323
+
324
+ function SessionContextProgressBar({
325
+ latestPercent,
326
+ peakPercent,
327
+ risk,
328
+ onOpenContext,
329
+ }: {
330
+ latestPercent: number | null;
331
+ peakPercent: number | null;
332
+ risk: ContextRiskLevel;
333
+ onOpenContext: () => void;
334
+ }): JSX.Element {
335
+ const latest = progressPercent(latestPercent);
336
+ const peakMarker = peakMarkerPercent(latestPercent, peakPercent);
337
+ const ariaLabel =
338
+ peakPercent === null
339
+ ? `Latest context window usage ${formatPercent(latestPercent)}`
340
+ : `Latest context window usage ${formatPercent(latestPercent)}, peak ${formatPercent(
341
+ peakPercent,
342
+ )}`;
343
+
344
+ return (
345
+ <button
346
+ type="button"
347
+ onClick={onOpenContext}
348
+ className="group block w-full rounded-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1"
349
+ aria-label={`${ariaLabel}. Open latest Context details`}
350
+ title="Open latest Context details"
351
+ >
352
+ <span
353
+ className="relative block h-2.5 overflow-hidden rounded-full border border-border/70 bg-background transition-colors group-hover:border-foreground/30"
354
+ role="meter"
355
+ aria-label={ariaLabel}
356
+ aria-valuemin={0}
357
+ aria-valuemax={100}
358
+ aria-valuenow={Math.round(latest)}
359
+ >
360
+ <span
361
+ className={cn("block h-full rounded-full transition-[width]", sessionRiskBarClass(risk))}
362
+ style={{ width: `${latest}%` }}
363
+ />
364
+ {peakMarker !== null && (
365
+ <span
366
+ 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)]"
367
+ style={{ left: `calc(${peakMarker}% - 0.5px)` }}
368
+ aria-hidden="true"
369
+ />
370
+ )}
371
+ </span>
372
+ </button>
373
+ );
374
+ }
375
+
376
+ function SessionModelContextRow({ summary }: { summary: SessionModelContextSummary }): JSX.Element {
377
+ const trend = summary.trendPercent;
378
+ const TrendIcon =
379
+ trend === null || Math.abs(trend) < 0.1 ? null : trend > 0 ? ArrowUpRight : ArrowDownRight;
380
+ const diagnosticLabel = summary.latest.diagnostics[0] ?? null;
381
+ const latestUsage = formatPercent(summary.latest.usagePercent);
382
+ const peakUsage = formatPercent(summary.peak.usagePercent);
383
+ const openLatestContext = (): void => {
384
+ openLogContext(summary.latest.logId);
385
+ };
386
+
387
+ return (
388
+ <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)]">
389
+ <div className="min-w-0">
390
+ <div className="flex min-w-0 items-center gap-2">
391
+ <button
392
+ type="button"
393
+ onClick={openLatestContext}
394
+ 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"
395
+ title={`Open latest Context for ${summary.model}`}
396
+ >
397
+ {summary.model}
398
+ </button>
399
+ <span
400
+ className={cn(
401
+ "inline-flex h-5 shrink-0 items-center rounded border px-1.5 font-mono text-[10px]",
402
+ sessionHealthClass(summary.latest.health.level),
403
+ )}
404
+ >
405
+ {sessionHealthLabel(summary.latest.health.level)}
406
+ </span>
407
+ </div>
408
+ <div className="mt-1 flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 text-[11px] text-muted-foreground">
409
+ <span>{summary.requestCount} req</span>
410
+ <span>{formatContextWindow(summary)}</span>
411
+ <span className="truncate" title={summary.latest.contextWindow.label}>
412
+ {summary.latest.contextWindow.label}
413
+ </span>
414
+ </div>
415
+ </div>
416
+
417
+ <div className="min-w-0 space-y-2 text-xs">
418
+ <SessionContextProgressBar
419
+ latestPercent={summary.latest.usagePercent}
420
+ peakPercent={summary.peak.usagePercent}
421
+ risk={summary.latest.riskLevel}
422
+ onOpenContext={openLatestContext}
423
+ />
424
+ <div className="grid grid-cols-3 gap-2">
425
+ <div className="min-w-0">
426
+ <div className="text-[10px] uppercase text-muted-foreground">Latest</div>
427
+ <div className={cn("mt-0.5 font-mono", sessionRiskClass(summary.latest.riskLevel))}>
428
+ {latestUsage}
429
+ </div>
430
+ <SessionContextLogButton label="Log" logId={summary.latest.logId} />
431
+ </div>
432
+ <div className="min-w-0">
433
+ <div className="text-[10px] uppercase text-muted-foreground">Peak</div>
434
+ <div className={cn("mt-0.5 font-mono", sessionRiskClass(summary.peak.riskLevel))}>
435
+ {peakUsage}
436
+ </div>
437
+ <SessionContextLogButton label="Log" logId={summary.peak.logId} />
438
+ </div>
439
+ <div className="min-w-0">
440
+ <div className="text-[10px] uppercase text-muted-foreground">Trend</div>
441
+ <div className="mt-0.5 flex items-center gap-1 font-mono text-foreground">
442
+ {TrendIcon !== null && (
443
+ <TrendIcon
444
+ className={cn(
445
+ "size-3",
446
+ trend !== null && trend > 0 ? "text-amber-300" : "text-emerald-300",
447
+ )}
448
+ />
449
+ )}
450
+ {formatTrend(trend)}
451
+ </div>
452
+ <div className="h-5" aria-hidden="true" />
453
+ </div>
454
+ </div>
455
+ </div>
456
+
457
+ <div className="min-w-0 space-y-1.5">
458
+ <SegmentBar
459
+ segments={summary.latest.roleSegments}
460
+ totalTokens={summary.latest.roleTotalTokens}
461
+ showLabels={false}
462
+ />
463
+ <div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 text-[11px] text-muted-foreground">
464
+ <span className="font-mono">
465
+ ~{formatTokens(summary.latest.estimatedInputTokens)} input
466
+ </span>
467
+ {summary.latest.windowUsedTokens !== null && (
468
+ <span className="font-mono">~{formatTokens(summary.latest.windowUsedTokens)} used</span>
469
+ )}
470
+ {diagnosticLabel !== null && <span className="truncate">{diagnosticLabel}</span>}
471
+ </div>
472
+ </div>
473
+ </div>
474
+ );
475
+ }
476
+
477
+ function SessionContextSummaryPanel({
478
+ summary,
479
+ }: {
480
+ summary: SessionContextSummary;
481
+ }): JSX.Element | null {
482
+ if (summary.models.length === 0) return null;
483
+ return (
484
+ <div className="mt-3 border-t border-border/70 pt-2">
485
+ <div className="mb-1 flex items-center justify-between gap-2">
486
+ <div className="text-[11px] font-semibold uppercase tracking-normal text-muted-foreground">
487
+ Session Context
488
+ </div>
489
+ <div className="font-mono text-[10px] text-muted-foreground">
490
+ {summary.models.length} model{summary.models.length === 1 ? "" : "s"}
491
+ </div>
492
+ </div>
493
+ <div className="max-h-52 overflow-y-auto pr-1">
494
+ {summary.models.map((modelSummary) => (
495
+ <SessionModelContextRow key={modelSummary.model} summary={modelSummary} />
496
+ ))}
497
+ </div>
498
+ </div>
499
+ );
500
+ }
501
+
126
502
  function SessionContextBar({
127
- sessionId,
503
+ label,
128
504
  logs,
129
505
  totalIn,
130
506
  totalOut,
131
507
  timeDisplayFormat,
508
+ contextSummary,
509
+ showBackLink,
132
510
  }: {
133
- sessionId: string;
511
+ label: string;
134
512
  logs: CapturedLog[];
135
513
  totalIn: number;
136
514
  totalOut: number;
137
515
  timeDisplayFormat: TimeDisplayFormat;
516
+ contextSummary: SessionContextSummary;
517
+ showBackLink: boolean;
138
518
  }): JSX.Element {
139
519
  const [copied, setCopied] = useState(false);
140
520
  const timeRange = useMemo(
@@ -151,50 +531,55 @@ function SessionContextBar({
151
531
  }, []);
152
532
 
153
533
  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}
534
+ <div className="mb-4 border border-border rounded-md bg-muted/20 px-3 py-2 text-xs">
535
+ <div className="flex items-center gap-3">
536
+ {showBackLink && (
537
+ <a
538
+ href="/"
539
+ className="inline-flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
540
+ aria-label="Back to all sessions"
541
+ title="Back to all sessions"
542
+ >
543
+ <ArrowLeft className="size-3.5" />
544
+ </a>
545
+ )}
546
+ <div className="min-w-0 flex-1">
547
+ <div className="flex min-w-0 items-center gap-2">
548
+ <span className="font-mono font-semibold text-purple-400/90 truncate" title={label}>
549
+ {truncateSessionId(label)}
174
550
  </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
551
+ {userAgent !== null && (
552
+ <span
553
+ className="font-mono text-muted-foreground truncate max-w-[220px]"
554
+ title={userAgent}
555
+ >
556
+ {userAgent}
557
+ </span>
558
+ )}
559
+ </div>
560
+ <div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-muted-foreground">
561
+ <span>
562
+ {logs.length} request{logs.length !== 1 ? "s" : ""}
185
563
  </span>
186
- )}
564
+ {timeRange !== null && <span>{timeRange}</span>}
565
+ {(totalIn > 0 || totalOut > 0) && (
566
+ <span className="font-mono">
567
+ {formatTokens(totalIn)} in / {formatTokens(totalOut)} out
568
+ </span>
569
+ )}
570
+ </div>
187
571
  </div>
572
+ <button
573
+ type="button"
574
+ onClick={handleCopyLink}
575
+ className="inline-flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
576
+ aria-label={copied ? "Copied session link" : "Copy session link"}
577
+ title={copied ? "Copied session link" : "Copy session link"}
578
+ >
579
+ {copied ? <Check className="size-3.5" /> : <Copy className="size-3.5" />}
580
+ </button>
188
581
  </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>
582
+ <SessionContextSummaryPanel summary={contextSummary} />
198
583
  </div>
199
584
  );
200
585
  }
@@ -246,6 +631,7 @@ export function ProxyViewer({
246
631
  pinnedSessionId,
247
632
  }: ProxyViewerProps): JSX.Element {
248
633
  const { totalIn, totalOut } = useMemo(() => computeTokenSummary(logs), [logs]);
634
+ const { providers } = useProviders();
249
635
  const [exporting, setExporting] = useState(false);
250
636
  const [comparePair, setComparePair] = useState<[CapturedLog, CapturedLog] | null>(null);
251
637
  const [crabEntrancePhase, setCrabEntrancePhase] = useState<"hidden" | "playing" | "done">(
@@ -295,7 +681,20 @@ export function ProxyViewer({
295
681
  }, []);
296
682
 
297
683
  const groups = useMemo(() => groupLogsByConversation(logs), [logs]);
298
- const hasPinnedSessionContext = pinnedSessionId !== undefined;
684
+ const sessionContextScope = useMemo(
685
+ () => resolveSessionContextScope({ pinnedSessionId, selectedSession, sessions, logs }),
686
+ [logs, pinnedSessionId, selectedSession, sessions],
687
+ );
688
+ const sessionContextLogs = sessionContextScope?.logs ?? [];
689
+ const hasSessionContext = sessionContextScope !== null && sessionContextLogs.length > 0;
690
+ const { totalIn: sessionContextTotalIn, totalOut: sessionContextTotalOut } = useMemo(
691
+ () => computeTokenSummary(sessionContextLogs),
692
+ [sessionContextLogs],
693
+ );
694
+ const sessionContextSummary = useMemo(
695
+ () => buildSessionContextSummary(sessionContextLogs, providers),
696
+ [sessionContextLogs, providers],
697
+ );
299
698
  const cacheTrends = useMemo(() => computeCacheTrends(groups), [groups]);
300
699
  const comparisonPredecessors = useMemo(() => buildValidPredecessors(groups), [groups]);
301
700
  const handleCompareWithPrevious = useCallback(
@@ -379,13 +778,15 @@ export function ProxyViewer({
379
778
  </div>
380
779
  </div>
381
780
 
382
- {pinnedSessionId !== undefined && (
781
+ {hasSessionContext && sessionContextScope !== null && (
383
782
  <SessionContextBar
384
- sessionId={pinnedSessionId}
385
- logs={logs}
386
- totalIn={totalIn}
387
- totalOut={totalOut}
783
+ label={sessionContextScope.label}
784
+ logs={sessionContextLogs}
785
+ totalIn={sessionContextTotalIn}
786
+ totalOut={sessionContextTotalOut}
388
787
  timeDisplayFormat={timeDisplayFormat}
788
+ contextSummary={sessionContextSummary}
789
+ showBackLink={sessionContextScope.showBackLink}
389
790
  />
390
791
  )}
391
792
 
@@ -444,7 +845,7 @@ export function ProxyViewer({
444
845
  </button>
445
846
  </div>
446
847
  <div className="flex-1" />
447
- {!hasPinnedSessionContext && (
848
+ {!hasSessionContext && (
448
849
  <span className="text-muted-foreground text-xs font-mono">
449
850
  {logs.length} request{logs.length !== 1 ? "s" : ""}
450
851
  {totalIn > 0 || totalOut > 0
@@ -535,7 +936,7 @@ export function ProxyViewer({
535
936
  comparisonPredecessors={comparisonPredecessors}
536
937
  onClearGroup={onClearGroup}
537
938
  standalone={groups.length === 1}
538
- hasPinnedSessionContext={hasPinnedSessionContext}
939
+ hasPinnedSessionContext={hasSessionContext}
539
940
  timeDisplayFormat={timeDisplayFormat}
540
941
  />
541
942
  ))}
@@ -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
  })}