@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.
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-C9OWdxHM.js → CompareDrawer-CrMyE23D.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-BZuo0px3.js +114 -0
- package/.output/public/assets/{ReplayDialog-CwE6I1Pe.js → ReplayDialog-TQGhDdTa.js} +1 -1
- package/.output/public/assets/RequestAnatomy-D6kQYtfa.js +1 -0
- package/.output/public/assets/ResponseView-C6ybWuB8.js +1 -0
- package/.output/public/assets/{StreamingChunkSequence-CLcLZ7-W.js → StreamingChunkSequence-Bvs59_-I.js} +1 -1
- package/.output/public/assets/_sessionId-BNG_mnaH.js +1 -0
- package/.output/public/assets/index-BUxIwHhC.js +1 -0
- package/.output/public/assets/index-D_nZj9Vt.css +1 -0
- package/.output/public/assets/{main-D1Xanzu7.js → main-DvFSJlOd.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +51 -39
- package/.output/server/{_sessionId-D3IyPpws.mjs → _sessionId-DHYJxPb7.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-BIMThqxy.mjs → CompareDrawer-meimDBeA.mjs} +3 -3
- package/.output/server/_ssr/{ProxyViewerContainer-BiBdKWtj.mjs → ProxyViewerContainer-Co-WKq6u.mjs} +1610 -183
- package/.output/server/_ssr/{ReplayDialog-D_wiDyM2.mjs → ReplayDialog-cclcGyR9.mjs} +4 -4
- package/.output/server/_ssr/RequestAnatomy-to_rezzu.mjs +882 -0
- package/.output/server/_ssr/{ResponseView-Drk5ghmL.mjs → ResponseView-CZZI4IZd.mjs} +3 -3
- package/.output/server/_ssr/{StreamingChunkSequence-F889rmG0.mjs → StreamingChunkSequence-BTF4xSYX.mjs} +2 -2
- package/.output/server/_ssr/{index-CaPniq9z.mjs → index-D2fpJFP5.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-C7S8FRth.mjs → router-RDBAF-Iu.mjs} +85 -22
- package/.output/server/{_tanstack-start-manifest_v-CVTkFOdT.mjs → _tanstack-start-manifest_v-DB9sseoH.mjs} +1 -1
- package/.output/server/index.mjs +58 -58
- package/package.json +1 -1
- package/src/components/ProxyViewer.tsx +454 -52
- package/src/components/providers/ProviderCard.tsx +45 -2
- package/src/components/providers/ProviderForm.tsx +245 -80
- package/src/components/providers/ProvidersPanel.tsx +7 -1
- package/src/components/proxy-viewer/ConversationGroup.tsx +18 -1
- package/src/components/proxy-viewer/LogEntry.tsx +25 -0
- package/src/components/proxy-viewer/TurnGroup.tsx +15 -0
- package/src/components/proxy-viewer/anatomy/RequestAnatomy.tsx +366 -99
- package/src/components/proxy-viewer/anatomy/contextIntelligence.ts +22 -4
- package/src/components/proxy-viewer/anatomy/sessionContextSummary.ts +196 -0
- package/src/components/proxy-viewer/logFocus.ts +33 -0
- package/src/lib/providerModelMetadata.ts +64 -12
- package/src/lib/utils.ts +7 -0
- package/src/proxy/providers.ts +25 -0
- package/src/routes/api/providers.ts +3 -0
- package/.output/public/assets/ProxyViewerContainer-CNzay4u8.js +0 -114
- package/.output/public/assets/RequestAnatomy-DEehC3yz.js +0 -1
- package/.output/public/assets/ResponseView-CE5UsuUq.js +0 -1
- package/.output/public/assets/_sessionId-DE__tPTo.js +0 -1
- package/.output/public/assets/index-BFO4Jmw5.js +0 -1
- package/.output/public/assets/index-CDzZ7l_S.css +0 -1
- 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
|
-
|
|
504
|
+
label,
|
|
128
505
|
logs,
|
|
129
506
|
totalIn,
|
|
130
507
|
totalOut,
|
|
131
508
|
timeDisplayFormat,
|
|
509
|
+
contextSummary,
|
|
510
|
+
showBackLink,
|
|
132
511
|
}: {
|
|
133
|
-
|
|
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
|
|
155
|
-
<
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
<
|
|
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
|
|
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
|
-
{
|
|
782
|
+
{hasSessionContext && sessionContextScope !== null && (
|
|
383
783
|
<SessionContextBar
|
|
384
|
-
|
|
385
|
-
logs={
|
|
386
|
-
totalIn={
|
|
387
|
-
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
|
-
{!
|
|
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={
|
|
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 {
|
|
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
|
-
{
|
|
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
|
})}
|