@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.
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-BWMipEjS.js → CompareDrawer-ClM_uVRy.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-CTNNzXSa.js +114 -0
- package/.output/public/assets/{ReplayDialog-DaSzZsvM.js → ReplayDialog-7rp7N_KJ.js} +1 -1
- package/.output/public/assets/RequestAnatomy-sL2FFo_N.js +1 -0
- package/.output/public/assets/ResponseView-Cjlf3b6-.js +1 -0
- package/.output/public/assets/{StreamingChunkSequence-B4atnL99.js → StreamingChunkSequence-Ctx3sT3r.js} +1 -1
- package/.output/public/assets/_sessionId-DEWzDQ8-.js +1 -0
- package/.output/public/assets/index-DGIIhZTn.js +1 -0
- package/.output/public/assets/index-D_nZj9Vt.css +1 -0
- package/.output/public/assets/{main-DE6Y4-wV.js → main-FvIQopAy.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +51 -39
- package/.output/server/{_sessionId-61HO9rUI.mjs → _sessionId-BDSgtrsI.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-CSPqneYm.mjs → CompareDrawer-Cqvv0I3x.mjs} +3 -3
- package/.output/server/_ssr/{ProxyViewerContainer-DrSMa5O7.mjs → ProxyViewerContainer-C0YEoG8N.mjs} +1611 -183
- package/.output/server/_ssr/{ReplayDialog-Dn3naENv.mjs → ReplayDialog-CrsynUKI.mjs} +4 -4
- package/.output/server/_ssr/RequestAnatomy-CR_JUkcL.mjs +688 -0
- package/.output/server/_ssr/{ResponseView-CwXAWOoE.mjs → ResponseView-Dba2Et69.mjs} +3 -3
- package/.output/server/_ssr/{StreamingChunkSequence-DRSR2FfN.mjs → StreamingChunkSequence-BIMHLstI.mjs} +2 -2
- package/.output/server/_ssr/{index-D4zn5CAg.mjs → index-DveIUTm5.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-CSRaa_tu.mjs → router-D0yXVG0Q.mjs} +85 -22
- package/.output/server/{_tanstack-start-manifest_v-CfvVUYYj.mjs → _tanstack-start-manifest_v-pe0xJFuQ.mjs} +1 -1
- package/.output/server/index.mjs +70 -70
- package/package.json +1 -1
- package/src/components/ProxyViewer.tsx +453 -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 +144 -80
- package/src/components/proxy-viewer/anatomy/contextIntelligence.ts +70 -5
- 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-DIAgurux.js +0 -114
- package/.output/public/assets/RequestAnatomy-BfQmPHCd.js +0 -1
- package/.output/public/assets/ResponseView-CblEWDMx.js +0 -1
- package/.output/public/assets/_sessionId-DUzYnZXV.js +0 -1
- package/.output/public/assets/index-CxhRsekH.css +0 -1
- package/.output/public/assets/index-jagRsQaV.js +0 -1
- package/.output/server/_ssr/RequestAnatomy-D1X3f3AX.mjs +0 -1322
|
@@ -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
|
-
|
|
503
|
+
label,
|
|
128
504
|
logs,
|
|
129
505
|
totalIn,
|
|
130
506
|
totalOut,
|
|
131
507
|
timeDisplayFormat,
|
|
508
|
+
contextSummary,
|
|
509
|
+
showBackLink,
|
|
132
510
|
}: {
|
|
133
|
-
|
|
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
|
|
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}
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
<
|
|
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
|
|
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
|
-
{
|
|
781
|
+
{hasSessionContext && sessionContextScope !== null && (
|
|
383
782
|
<SessionContextBar
|
|
384
|
-
|
|
385
|
-
logs={
|
|
386
|
-
totalIn={
|
|
387
|
-
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
|
-
{!
|
|
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={
|
|
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 {
|
|
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
|
})}
|