@tonyclaw/agent-inspector 2.1.13 → 2.1.15
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-BcEd6V-V.js +1 -0
- package/.output/public/assets/ProxyViewerContainer-h851qWNp.js +106 -0
- package/.output/public/assets/ReplayDialog-BNpC0548.js +1 -0
- package/.output/public/assets/{RequestAnatomy-BjlBT-Cy.js → RequestAnatomy-Ds1uRLVB.js} +1 -1
- package/.output/public/assets/ResponseView-7KPVqKl5.js +3 -0
- package/.output/public/assets/{StreamingChunkSequence-D2_SMhlE.js → StreamingChunkSequence-BHQT261s.js} +1 -1
- package/.output/public/assets/_sessionId-DWePGjnS.js +1 -0
- package/.output/public/assets/index-CI1-G8ua.js +1 -0
- package/.output/public/assets/index-DdhFqPsI.css +1 -0
- package/.output/public/assets/index-DjKt8XKe.js +14 -0
- package/.output/public/assets/json-viewer-CkCu-rka.js +1 -0
- package/.output/public/assets/{main-Dtspb4Ui.js → main-DpD1N0S8.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +194 -200
- package/.output/server/_libs/react-markdown.mjs +90 -2
- package/.output/server/{_sessionId-CXDcLuvi.mjs → _sessionId-DF9Sy8cP.mjs} +6 -57
- package/.output/server/_ssr/{CompareDrawer-CMoCAoeq.mjs → CompareDrawer-BoxztaO7.mjs} +50 -74
- package/.output/server/_ssr/{ProxyViewerContainer-BW2vVCBN.mjs → ProxyViewerContainer-CRBkqFlJ.mjs} +1221 -799
- package/.output/server/_ssr/{ReplayDialog-ChXL1t8H.mjs → ReplayDialog-Cc1dyDuK.mjs} +18 -62
- package/.output/server/_ssr/{RequestAnatomy-DtKzIlfU.mjs → RequestAnatomy-CMGSsz5Z.mjs} +6 -57
- package/.output/server/_ssr/{ResponseView-B5I8drzc.mjs → ResponseView-Cp10DM1D.mjs} +139 -61
- package/.output/server/_ssr/{StreamingChunkSequence-DvwjQNcO.mjs → StreamingChunkSequence-B1VGxy3A.mjs} +13 -58
- package/.output/server/_ssr/{index-D_ZHtRfl.mjs → index-47XVPghS.mjs} +6 -57
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/json-viewer-zDE2rrmJ.mjs +478 -0
- package/.output/server/_ssr/{router-DhL9Wp3N.mjs → router-DVeuZFqI.mjs} +680 -174
- package/.output/server/{_tanstack-start-manifest_v-BHgoAmxZ.mjs → _tanstack-start-manifest_v-Bp8JxtPW.mjs} +1 -1
- package/.output/server/index.mjs +72 -58
- package/package.json +1 -1
- package/src/components/ProxyViewer.tsx +28 -2
- package/src/components/ProxyViewerContainer.tsx +25 -17
- package/src/components/clients/ClientLogo.tsx +132 -0
- package/src/components/groups/GroupsDialog.tsx +18 -13
- package/src/components/providers/ImportWizardDialog.tsx +7 -1
- package/src/components/providers/ProviderCard.tsx +10 -1
- package/src/components/providers/ProviderForm.tsx +157 -41
- package/src/components/providers/ProvidersPanel.tsx +5 -1
- package/src/components/proxy-viewer/AgentTraceSummary.tsx +56 -23
- package/src/components/proxy-viewer/AnswerMarkdown.tsx +8 -3
- package/src/components/proxy-viewer/CompareDrawer.tsx +58 -14
- package/src/components/proxy-viewer/ConversationGroup.tsx +11 -15
- package/src/components/proxy-viewer/ConversationHeader.tsx +33 -41
- package/src/components/proxy-viewer/LogEntry.tsx +41 -12
- package/src/components/proxy-viewer/LogEntryHeader.tsx +122 -51
- package/src/components/proxy-viewer/ProviderLogoStack.tsx +60 -0
- package/src/components/proxy-viewer/ReplayDialog.tsx +8 -2
- package/src/components/proxy-viewer/RequestToolsPanel.tsx +19 -13
- package/src/components/proxy-viewer/StreamingChunkSequence.tsx +9 -3
- package/src/components/proxy-viewer/ThreadConnector.tsx +18 -7
- package/src/components/proxy-viewer/TurnGroup.tsx +67 -32
- package/src/components/proxy-viewer/TurnGroupList.tsx +153 -0
- package/src/components/proxy-viewer/anatomy/sessionContextSummary.ts +74 -2
- package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +13 -7
- package/src/components/proxy-viewer/formats/index.tsx +10 -3
- package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +185 -17
- package/src/components/proxy-viewer/log-formats/openai.ts +107 -15
- package/src/components/proxy-viewer/logFocus.ts +15 -2
- package/src/components/proxy-viewer/requestTools.ts +7 -3
- package/src/components/proxy-viewer/viewerState.ts +77 -5
- package/src/components/ui/json-viewer-bulk.ts +41 -6
- package/src/components/ui/json-viewer.tsx +9 -8
- package/src/contracts/index.ts +15 -1
- package/src/contracts/openai.ts +94 -0
- package/src/lib/providerContract.ts +1 -0
- package/src/lib/providerModelMetadata.ts +7 -1
- package/src/lib/sessionInfoContract.ts +1 -0
- package/src/lib/stopReason.ts +59 -15
- package/src/lib/upstreamUrl.ts +46 -0
- package/src/mcp/previewExtractor.ts +73 -3
- package/src/mcp/server.ts +2 -0
- package/src/mcp/toolHandlers.ts +2 -0
- package/src/proxy/constants.ts +2 -0
- package/src/proxy/formats/openai/handler.ts +40 -9
- package/src/proxy/formats/openai/index.ts +7 -0
- package/src/proxy/formats/openai/schemas.ts +15 -1
- package/src/proxy/formats/openai/stream.ts +529 -135
- package/src/proxy/formats/registry.ts +9 -1
- package/src/proxy/providerImporters.ts +45 -5
- package/src/proxy/providers.ts +31 -11
- package/src/proxy/schemas.ts +6 -0
- package/src/proxy/sessionInfo.ts +5 -1
- package/src/proxy/toolSchemaWarnings.ts +25 -3
- package/src/proxy/upstream.ts +18 -37
- package/src/routes/api/providers.$providerId.ts +1 -0
- package/src/routes/api/providers.ts +2 -0
- package/.output/public/assets/CompareDrawer-Cz_1vIpR.js +0 -1
- package/.output/public/assets/ProxyViewerContainer-7QSiluMf.js +0 -117
- package/.output/public/assets/ReplayDialog-sBA1KAYD.js +0 -1
- package/.output/public/assets/ResponseView-PtEKzml9.js +0 -1
- package/.output/public/assets/_sessionId-DZfB4ruK.js +0 -1
- package/.output/public/assets/index-B-QQLbpz.js +0 -1
- package/.output/public/assets/index-CmtfjQPv.css +0 -1
|
@@ -1,15 +1,29 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type CSSProperties,
|
|
3
|
+
type JSX,
|
|
4
|
+
memo,
|
|
5
|
+
useCallback,
|
|
6
|
+
useEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
useRef,
|
|
9
|
+
useState,
|
|
10
|
+
} from "react";
|
|
2
11
|
import { AlertTriangle, ChevronRight, Clock, Zap } from "lucide-react";
|
|
3
12
|
import { isTurnBoundary } from "../../lib/stopReason";
|
|
4
13
|
import type { TimeDisplayFormat } from "../../lib/runtimeConfig";
|
|
5
14
|
import { cn, formatTokens } from "../../lib/utils";
|
|
6
15
|
import type { CapturedLog } from "../../contracts";
|
|
7
16
|
import { getCrabVariant } from "../ui/crab-variants";
|
|
8
|
-
import { ProviderLogo, detectProvider, type Provider } from "../providers/ProviderLogo";
|
|
9
17
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip";
|
|
18
|
+
import { ROW_CHEVRON_SLOT_CLASS } from "./ProviderLogoStack";
|
|
10
19
|
import type { CacheTrendEntry } from "./cacheTrend";
|
|
11
20
|
import { LogEntry } from "./LogEntry";
|
|
12
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
dispatchLogFocusRequest,
|
|
23
|
+
LOG_FOCUS_REQUEST_EVENT,
|
|
24
|
+
readLogFocusRequest,
|
|
25
|
+
type LogFocusRequest,
|
|
26
|
+
} from "./logFocus";
|
|
13
27
|
import { ThreadConnector } from "./ThreadConnector";
|
|
14
28
|
import { ToolTraceEvents } from "./ToolTraceEvents";
|
|
15
29
|
import { extractToolTraceEvents, isTurnCollapsible, type TurnEntry } from "./viewerState";
|
|
@@ -19,7 +33,11 @@ function formatElapsed(ms: number): string {
|
|
|
19
33
|
return `${(ms / 1000).toFixed(1)}s`;
|
|
20
34
|
}
|
|
21
35
|
|
|
22
|
-
type
|
|
36
|
+
export type PendingTurnFocusRequest = LogFocusRequest & {
|
|
37
|
+
nonce: number;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type TurnGroupProps = {
|
|
23
41
|
entries: TurnEntry[];
|
|
24
42
|
viewMode: "simple" | "full";
|
|
25
43
|
strip: boolean;
|
|
@@ -29,6 +47,7 @@ type TurnGroupProps = {
|
|
|
29
47
|
comparisonPredecessors: Map<number, CapturedLog>;
|
|
30
48
|
turnIndex?: number;
|
|
31
49
|
timeDisplayFormat: TimeDisplayFormat;
|
|
50
|
+
pendingFocusRequest?: PendingTurnFocusRequest;
|
|
32
51
|
};
|
|
33
52
|
|
|
34
53
|
export const TurnGroup = memo(function TurnGroup({
|
|
@@ -41,6 +60,7 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
41
60
|
comparisonPredecessors,
|
|
42
61
|
turnIndex = 0,
|
|
43
62
|
timeDisplayFormat,
|
|
63
|
+
pendingFocusRequest,
|
|
44
64
|
}: TurnGroupProps): JSX.Element {
|
|
45
65
|
const lastIdx = entries.length - 1;
|
|
46
66
|
const lastStop = entries[lastIdx]?.stopReason ?? null;
|
|
@@ -75,6 +95,22 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
75
95
|
};
|
|
76
96
|
}, [collapsible, entries]);
|
|
77
97
|
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (pendingFocusRequest === undefined) return undefined;
|
|
100
|
+
if (!entries.some((entry) => entry.log.id === pendingFocusRequest.logId)) return undefined;
|
|
101
|
+
if (collapsible) setCollapsed(false);
|
|
102
|
+
|
|
103
|
+
const timer = window.setTimeout(() => {
|
|
104
|
+
dispatchLogFocusRequest({
|
|
105
|
+
logId: pendingFocusRequest.logId,
|
|
106
|
+
tab: pendingFocusRequest.tab,
|
|
107
|
+
source: "virtualizer",
|
|
108
|
+
});
|
|
109
|
+
}, 0);
|
|
110
|
+
|
|
111
|
+
return () => window.clearTimeout(timer);
|
|
112
|
+
}, [collapsible, entries, pendingFocusRequest]);
|
|
113
|
+
|
|
78
114
|
const toggleCollapse = useCallback(() => {
|
|
79
115
|
if (collapsible) setCollapsed((prev) => !prev);
|
|
80
116
|
}, [collapsible]);
|
|
@@ -106,21 +142,11 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
106
142
|
};
|
|
107
143
|
}, [entries, lastIdx]);
|
|
108
144
|
|
|
109
|
-
// Unique providers across all entries (for collapsed model logos)
|
|
110
|
-
const uniqueProviders = useMemo(() => {
|
|
111
|
-
const seen = new Set<Provider>();
|
|
112
|
-
for (const e of entries) {
|
|
113
|
-
const p = detectProvider(e.log.model);
|
|
114
|
-
if (p !== "unknown") seen.add(p);
|
|
115
|
-
}
|
|
116
|
-
return [...seen];
|
|
117
|
-
}, [entries]);
|
|
118
|
-
|
|
119
145
|
// Crab variant creators for the dual-crab collapsed layout
|
|
120
146
|
const StartCrab = useMemo(() => getCrabVariant(entries[0]?.log.id ?? 0), [entries]);
|
|
121
147
|
const EndCrab = useMemo(() => getCrabVariant(entries[lastIdx]?.log.id ?? 0), [entries, lastIdx]);
|
|
122
148
|
|
|
123
|
-
const bgClass = turnIndex % 2 === 0 ? "bg-
|
|
149
|
+
const bgClass = turnIndex % 2 === 0 ? "bg-[#070a0f]" : "bg-[#090d12]";
|
|
124
150
|
const aggregateIsSlow =
|
|
125
151
|
aggregate.maxElapsed !== null &&
|
|
126
152
|
slowResponseThresholdSeconds > 0 &&
|
|
@@ -154,17 +180,27 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
154
180
|
|
|
155
181
|
const firstLogId = entries[0]?.log.id ?? turnIndex;
|
|
156
182
|
const turnLabel = `Turn ${String(turnIndex + 1)}`;
|
|
183
|
+
const containmentStyle = useMemo<CSSProperties>(
|
|
184
|
+
() => ({
|
|
185
|
+
contentVisibility: "auto",
|
|
186
|
+
containIntrinsicSize: collapsed ? "48px" : "160px",
|
|
187
|
+
}),
|
|
188
|
+
[collapsed],
|
|
189
|
+
);
|
|
157
190
|
|
|
158
191
|
return (
|
|
159
192
|
<div
|
|
160
193
|
ref={containerRef}
|
|
194
|
+
style={containmentStyle}
|
|
161
195
|
tabIndex={collapsed ? undefined : 0}
|
|
162
196
|
role={collapsed ? undefined : "group"}
|
|
163
197
|
aria-label={collapsed ? undefined : turnLabel}
|
|
164
198
|
data-nav-id={collapsed ? undefined : `turn-${String(firstLogId)}`}
|
|
165
199
|
className={cn(
|
|
166
|
-
"border rounded-lg",
|
|
167
|
-
isPending
|
|
200
|
+
"border rounded-lg transition-[border-color,box-shadow]",
|
|
201
|
+
isPending
|
|
202
|
+
? "border-amber-300/15 shadow-[0_0_0_1px_rgba(252,211,77,0.04)]"
|
|
203
|
+
: "border-transparent",
|
|
168
204
|
!collapsed &&
|
|
169
205
|
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:outline-none",
|
|
170
206
|
)}
|
|
@@ -252,13 +288,23 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
252
288
|
{entries.length > 1 && (
|
|
253
289
|
<div
|
|
254
290
|
className={cn(
|
|
255
|
-
"flex-1 min-w-0 mb-0.5 rounded-lg border border-
|
|
291
|
+
"flex-1 min-w-0 mb-0.5 rounded-lg border border-cyan-300/10 py-1.5 px-3 flex items-center gap-3 text-xs shadow-[inset_0_1px_0_rgba(255,255,255,0.04)]",
|
|
256
292
|
bgClass,
|
|
257
293
|
)}
|
|
258
294
|
>
|
|
295
|
+
<span className={ROW_CHEVRON_SLOT_CLASS}>
|
|
296
|
+
<ChevronRight className="size-4 shrink-0" />
|
|
297
|
+
</span>
|
|
298
|
+
|
|
259
299
|
{/* ID range */}
|
|
260
|
-
<span
|
|
261
|
-
|
|
300
|
+
<span
|
|
301
|
+
className="text-blue-400/80 font-mono font-semibold tabular-nums shrink-0"
|
|
302
|
+
title={`Log IDs ${String(entries[0]?.log.id ?? "?")} ~ ${String(
|
|
303
|
+
entries[lastIdx]?.log.id ?? "?",
|
|
304
|
+
)}`}
|
|
305
|
+
>
|
|
306
|
+
#{entries[0]?.sessionLogNumber ?? "?"} ~ #
|
|
307
|
+
{entries[lastIdx]?.sessionLogNumber ?? "?"}
|
|
262
308
|
</span>
|
|
263
309
|
|
|
264
310
|
{/* Request count */}
|
|
@@ -266,15 +312,6 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
266
312
|
{entries.length} request{entries.length > 1 ? "s" : ""}
|
|
267
313
|
</span>
|
|
268
314
|
|
|
269
|
-
{/* Model logos — one per unique provider */}
|
|
270
|
-
{uniqueProviders.length > 0 && (
|
|
271
|
-
<span className="flex items-center gap-0.5 shrink-0">
|
|
272
|
-
{uniqueProviders.map((p) => (
|
|
273
|
-
<ProviderLogo key={p} provider={p} className="size-4" />
|
|
274
|
-
))}
|
|
275
|
-
</span>
|
|
276
|
-
)}
|
|
277
|
-
|
|
278
315
|
{/* Elapsed — slowest single request in the turn (not the sum) */}
|
|
279
316
|
{aggregate.maxElapsed !== null && (
|
|
280
317
|
<TooltipProvider>
|
|
@@ -322,9 +359,6 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
322
359
|
|
|
323
360
|
{/* Spacer */}
|
|
324
361
|
<span className="flex-1 min-w-0" />
|
|
325
|
-
|
|
326
|
-
{/* Expand chevron */}
|
|
327
|
-
<ChevronRight className="size-4 text-muted-foreground shrink-0" />
|
|
328
362
|
</div>
|
|
329
363
|
)}
|
|
330
364
|
</div>
|
|
@@ -358,6 +392,7 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
358
392
|
<div className={cn("flex-1 min-w-0 mb-0.5 rounded-lg", bgClass)}>
|
|
359
393
|
<LogEntry
|
|
360
394
|
log={log}
|
|
395
|
+
displayNumber={entry.sessionLogNumber}
|
|
361
396
|
viewMode={viewMode}
|
|
362
397
|
strip={strip}
|
|
363
398
|
slowResponseThresholdSeconds={slowResponseThresholdSeconds}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { useWindowVirtualizer } from "@tanstack/react-virtual";
|
|
2
|
+
import { type JSX, useEffect, useMemo, useState } from "react";
|
|
3
|
+
import type { CapturedLog } from "../../contracts";
|
|
4
|
+
import type { TimeDisplayFormat } from "../../lib/runtimeConfig";
|
|
5
|
+
import type { CacheTrendEntry } from "./cacheTrend";
|
|
6
|
+
import { LOG_FOCUS_REQUEST_EVENT, readLogFocusRequest } from "./logFocus";
|
|
7
|
+
import { TurnGroup } from "./TurnGroup";
|
|
8
|
+
import type { PendingTurnFocusRequest } from "./TurnGroup";
|
|
9
|
+
import type { TurnGroupData } from "./viewerState";
|
|
10
|
+
|
|
11
|
+
const VIRTUALIZE_TURN_THRESHOLD = 24;
|
|
12
|
+
const ESTIMATED_TURN_HEIGHT = 132;
|
|
13
|
+
const VIRTUAL_OVERSCAN = 8;
|
|
14
|
+
|
|
15
|
+
type TurnGroupListProps = {
|
|
16
|
+
turnGroups: TurnGroupData[];
|
|
17
|
+
viewMode: "simple" | "full";
|
|
18
|
+
strip: boolean;
|
|
19
|
+
slowResponseThresholdSeconds: number;
|
|
20
|
+
cacheTrends?: Map<number, CacheTrendEntry>;
|
|
21
|
+
onCompareWithPrevious: (log: CapturedLog) => void;
|
|
22
|
+
comparisonPredecessors: Map<number, CapturedLog>;
|
|
23
|
+
timeDisplayFormat: TimeDisplayFormat;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type PendingVirtualFocusRequest = PendingTurnFocusRequest & {
|
|
27
|
+
turnIndex: number;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function shouldVirtualizeTurnGroups(turnCount: number): boolean {
|
|
31
|
+
return turnCount > VIRTUALIZE_TURN_THRESHOLD;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function findTurnGroupIndexForLog(
|
|
35
|
+
turnGroups: readonly TurnGroupData[],
|
|
36
|
+
logId: number,
|
|
37
|
+
): number | null {
|
|
38
|
+
for (let index = 0; index < turnGroups.length; index++) {
|
|
39
|
+
const group = turnGroups[index];
|
|
40
|
+
if (group === undefined) continue;
|
|
41
|
+
if (group.entries.some((entry) => entry.log.id === logId)) return index;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function buildLogTurnIndex(turnGroups: readonly TurnGroupData[]): Map<number, number> {
|
|
47
|
+
const result = new Map<number, number>();
|
|
48
|
+
for (let index = 0; index < turnGroups.length; index++) {
|
|
49
|
+
const group = turnGroups[index];
|
|
50
|
+
if (group === undefined) continue;
|
|
51
|
+
for (const entry of group.entries) {
|
|
52
|
+
result.set(entry.log.id, index);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function renderTurnGroup(
|
|
59
|
+
turnGroup: TurnGroupData,
|
|
60
|
+
props: TurnGroupListProps,
|
|
61
|
+
pendingFocusRequest?: PendingTurnFocusRequest,
|
|
62
|
+
): JSX.Element {
|
|
63
|
+
return (
|
|
64
|
+
<TurnGroup
|
|
65
|
+
key={turnGroup.turnIndex}
|
|
66
|
+
entries={turnGroup.entries}
|
|
67
|
+
viewMode={props.viewMode}
|
|
68
|
+
strip={props.strip}
|
|
69
|
+
slowResponseThresholdSeconds={props.slowResponseThresholdSeconds}
|
|
70
|
+
timeDisplayFormat={props.timeDisplayFormat}
|
|
71
|
+
cacheTrends={props.cacheTrends}
|
|
72
|
+
onCompareWithPrevious={props.onCompareWithPrevious}
|
|
73
|
+
comparisonPredecessors={props.comparisonPredecessors}
|
|
74
|
+
turnIndex={turnGroup.turnIndex}
|
|
75
|
+
pendingFocusRequest={pendingFocusRequest}
|
|
76
|
+
/>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function TurnGroupList(props: TurnGroupListProps): JSX.Element {
|
|
81
|
+
if (!shouldVirtualizeTurnGroups(props.turnGroups.length)) {
|
|
82
|
+
return <>{props.turnGroups.map((turnGroup) => renderTurnGroup(turnGroup, props))}</>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return <VirtualizedTurnGroupList {...props} />;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function VirtualizedTurnGroupList(props: TurnGroupListProps): JSX.Element {
|
|
89
|
+
const { turnGroups } = props;
|
|
90
|
+
const [pendingFocusRequest, setPendingFocusRequest] = useState<PendingVirtualFocusRequest | null>(
|
|
91
|
+
null,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const virtualizer = useWindowVirtualizer({
|
|
95
|
+
count: turnGroups.length,
|
|
96
|
+
estimateSize: () => ESTIMATED_TURN_HEIGHT,
|
|
97
|
+
overscan: VIRTUAL_OVERSCAN,
|
|
98
|
+
getItemKey: (index) => turnGroups[index]?.turnIndex ?? index,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const turnIndexByLogId = useMemo(() => buildLogTurnIndex(turnGroups), [turnGroups]);
|
|
102
|
+
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
const handleLogFocusRequest = (event: Event): void => {
|
|
105
|
+
const request = readLogFocusRequest(event);
|
|
106
|
+
if (request === null) return;
|
|
107
|
+
if (request.source === "virtualizer") return;
|
|
108
|
+
|
|
109
|
+
const turnIndex = turnIndexByLogId.get(request.logId);
|
|
110
|
+
if (turnIndex === undefined) return;
|
|
111
|
+
virtualizer.scrollToIndex(turnIndex, { align: "center" });
|
|
112
|
+
setPendingFocusRequest((previous) => ({
|
|
113
|
+
...request,
|
|
114
|
+
turnIndex,
|
|
115
|
+
nonce: previous === null ? 1 : previous.nonce + 1,
|
|
116
|
+
}));
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
window.addEventListener(LOG_FOCUS_REQUEST_EVENT, handleLogFocusRequest);
|
|
120
|
+
return () => {
|
|
121
|
+
window.removeEventListener(LOG_FOCUS_REQUEST_EVENT, handleLogFocusRequest);
|
|
122
|
+
};
|
|
123
|
+
}, [turnIndexByLogId, virtualizer]);
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<div
|
|
127
|
+
className="relative w-full"
|
|
128
|
+
style={{ height: `${virtualizer.getTotalSize()}px` }}
|
|
129
|
+
data-virtualized-turn-list="true"
|
|
130
|
+
>
|
|
131
|
+
{virtualizer.getVirtualItems().map((virtualItem) => {
|
|
132
|
+
const turnGroup = turnGroups[virtualItem.index];
|
|
133
|
+
if (turnGroup === undefined) return null;
|
|
134
|
+
const itemFocusRequest =
|
|
135
|
+
pendingFocusRequest !== null && pendingFocusRequest.turnIndex === virtualItem.index
|
|
136
|
+
? pendingFocusRequest
|
|
137
|
+
: undefined;
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<div
|
|
141
|
+
key={virtualItem.key}
|
|
142
|
+
data-index={virtualItem.index}
|
|
143
|
+
ref={virtualizer.measureElement}
|
|
144
|
+
className="absolute left-0 top-0 w-full"
|
|
145
|
+
style={{ transform: `translateY(${virtualItem.start}px)` }}
|
|
146
|
+
>
|
|
147
|
+
{renderTurnGroup(turnGroup, props, itemFocusRequest)}
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
})}
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
@@ -46,6 +46,32 @@ type SummaryAccumulator = {
|
|
|
46
46
|
peak: SessionContextSnapshot;
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
+
type SnapshotCacheEntry = {
|
|
50
|
+
id: number;
|
|
51
|
+
rawRequestBody: string | null;
|
|
52
|
+
inputTokens: number | null;
|
|
53
|
+
model: string | null;
|
|
54
|
+
path: string;
|
|
55
|
+
apiFormat: CapturedLog["apiFormat"];
|
|
56
|
+
timestamp: string;
|
|
57
|
+
providersSignature: string;
|
|
58
|
+
snapshot: SessionContextSnapshot | null;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const snapshotCache = new WeakMap<CapturedLog, SnapshotCacheEntry>();
|
|
62
|
+
|
|
63
|
+
function providerSignature(providers: readonly ProviderConfig[]): string {
|
|
64
|
+
return providers
|
|
65
|
+
.map((provider) => {
|
|
66
|
+
const metadata =
|
|
67
|
+
provider.modelMetadata
|
|
68
|
+
?.map((entry) => `${entry.model}:${String(entry.contextWindow ?? "")}`)
|
|
69
|
+
.join(",") ?? "";
|
|
70
|
+
return `${provider.id}:${provider.updatedAt}:${provider.models.join(",")}:${metadata}`;
|
|
71
|
+
})
|
|
72
|
+
.join("|");
|
|
73
|
+
}
|
|
74
|
+
|
|
49
75
|
function parseRequestBody(rawBody: string | null): unknown | null {
|
|
50
76
|
if (rawBody === null) return null;
|
|
51
77
|
try {
|
|
@@ -104,6 +130,41 @@ function snapshotFromLog(
|
|
|
104
130
|
};
|
|
105
131
|
}
|
|
106
132
|
|
|
133
|
+
function cachedSnapshotFromLog(
|
|
134
|
+
log: CapturedLog,
|
|
135
|
+
providers: readonly ProviderConfig[],
|
|
136
|
+
providersSignature: string,
|
|
137
|
+
): SessionContextSnapshot | null {
|
|
138
|
+
const cached = snapshotCache.get(log);
|
|
139
|
+
if (
|
|
140
|
+
cached !== undefined &&
|
|
141
|
+
cached.id === log.id &&
|
|
142
|
+
cached.rawRequestBody === log.rawRequestBody &&
|
|
143
|
+
cached.inputTokens === log.inputTokens &&
|
|
144
|
+
cached.model === log.model &&
|
|
145
|
+
cached.path === log.path &&
|
|
146
|
+
cached.apiFormat === log.apiFormat &&
|
|
147
|
+
cached.timestamp === log.timestamp &&
|
|
148
|
+
cached.providersSignature === providersSignature
|
|
149
|
+
) {
|
|
150
|
+
return cached.snapshot;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const snapshot = snapshotFromLog(log, providers);
|
|
154
|
+
snapshotCache.set(log, {
|
|
155
|
+
id: log.id,
|
|
156
|
+
rawRequestBody: log.rawRequestBody,
|
|
157
|
+
inputTokens: log.inputTokens,
|
|
158
|
+
model: log.model,
|
|
159
|
+
path: log.path,
|
|
160
|
+
apiFormat: log.apiFormat,
|
|
161
|
+
timestamp: log.timestamp,
|
|
162
|
+
providersSignature,
|
|
163
|
+
snapshot,
|
|
164
|
+
});
|
|
165
|
+
return snapshot;
|
|
166
|
+
}
|
|
167
|
+
|
|
107
168
|
function isHigherPeak(candidate: SessionContextSnapshot, current: SessionContextSnapshot): boolean {
|
|
108
169
|
if (candidate.usagePercent !== null && current.usagePercent !== null) {
|
|
109
170
|
return candidate.usagePercent > current.usagePercent;
|
|
@@ -149,15 +210,26 @@ function trendPercent(
|
|
|
149
210
|
return (latest.usagePercent - first.usagePercent) * 100;
|
|
150
211
|
}
|
|
151
212
|
|
|
213
|
+
function logsInAscendingIdOrder(logs: readonly CapturedLog[]): readonly CapturedLog[] {
|
|
214
|
+
for (let index = 1; index < logs.length; index++) {
|
|
215
|
+
const previous = logs[index - 1];
|
|
216
|
+
const current = logs[index];
|
|
217
|
+
if (previous === undefined || current === undefined) continue;
|
|
218
|
+
if (previous.id > current.id) return [...logs].sort((left, right) => left.id - right.id);
|
|
219
|
+
}
|
|
220
|
+
return logs;
|
|
221
|
+
}
|
|
222
|
+
|
|
152
223
|
export function buildSessionContextSummary(
|
|
153
224
|
logs: readonly CapturedLog[],
|
|
154
225
|
providers: readonly ProviderConfig[] = [],
|
|
155
226
|
): SessionContextSummary {
|
|
156
|
-
const sortedLogs =
|
|
227
|
+
const sortedLogs = logsInAscendingIdOrder(logs);
|
|
228
|
+
const providersSignature = providerSignature(providers);
|
|
157
229
|
const byModel = new Map<string, SummaryAccumulator>();
|
|
158
230
|
|
|
159
231
|
for (const log of sortedLogs) {
|
|
160
|
-
const snapshot =
|
|
232
|
+
const snapshot = cachedSnapshotFromLog(log, providers, providersSignature);
|
|
161
233
|
if (snapshot === null) continue;
|
|
162
234
|
|
|
163
235
|
const existing = byModel.get(snapshot.model);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { Brain, ChevronDown, ChevronRight, Terminal } from "lucide-react";
|
|
2
|
-
import { type JSX, memo, useState } from "react";
|
|
2
|
+
import { type JSX, memo, Suspense, useState } from "react";
|
|
3
3
|
import type { ResponseContentBlockType } from "../../../../contracts/anthropic";
|
|
4
4
|
import { AnswerMarkdown } from "../../AnswerMarkdown";
|
|
5
5
|
import { Badge } from "../../../ui/badge";
|
|
6
6
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../../../ui/collapsible";
|
|
7
|
-
import { JsonViewer } from "../../../ui/json-viewer";
|
|
8
7
|
import { safeJsonValue } from "../../../ui/json-viewer-bulk";
|
|
9
8
|
import { ScrollArea } from "../../../ui/scroll-area";
|
|
9
|
+
import { LazyJsonViewer } from "../../lazy";
|
|
10
10
|
import { extractThinkingFromContent } from "./thinkingExtract";
|
|
11
11
|
|
|
12
12
|
function assertNever(_value: never): JSX.Element {
|
|
@@ -121,11 +121,17 @@ export const ToolUseBlock = memo(function ToolUseBlock({
|
|
|
121
121
|
)}
|
|
122
122
|
</CollapsibleTrigger>
|
|
123
123
|
<CollapsibleContent>
|
|
124
|
-
|
|
125
|
-
<
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
|
|
124
|
+
{open && (
|
|
125
|
+
<div className="px-3 pb-2">
|
|
126
|
+
<ScrollArea className="max-h-[60vh]">
|
|
127
|
+
<Suspense
|
|
128
|
+
fallback={<div className="text-xs text-muted-foreground">Loading JSON...</div>}
|
|
129
|
+
>
|
|
130
|
+
<LazyJsonViewer data={safeJsonValue(input)} defaultExpandDepth={0} />
|
|
131
|
+
</Suspense>
|
|
132
|
+
</ScrollArea>
|
|
133
|
+
</div>
|
|
134
|
+
)}
|
|
129
135
|
</CollapsibleContent>
|
|
130
136
|
</div>
|
|
131
137
|
</Collapsible>
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { JSX } from "react";
|
|
2
2
|
import type { ApiFormat } from "../../../contracts";
|
|
3
3
|
import type { AnthropicResponse } from "../../../contracts/anthropic";
|
|
4
|
-
import type { OpenAIResponse } from "../../../contracts/openai";
|
|
4
|
+
import type { OpenAIResponse, OpenAIResponsesResponse } from "../../../contracts/openai";
|
|
5
5
|
import { StructuredResponseViewAnthropic } from "./anthropic/ResponseView";
|
|
6
|
-
import { OpenAIResponseView } from "./openai/ResponseView";
|
|
6
|
+
import { OpenAIResponseView, OpenAIResponsesResponseView } from "./openai/ResponseView";
|
|
7
7
|
|
|
8
8
|
export { StructuredResponseViewAnthropic } from "./anthropic/ResponseView";
|
|
9
|
-
export { OpenAIResponseView } from "./openai/ResponseView";
|
|
9
|
+
export { OpenAIResponseView, OpenAIResponsesResponseView } from "./openai/ResponseView";
|
|
10
10
|
export { ResponseContentBlockRenderer } from "./anthropic/ContentBlocks";
|
|
11
11
|
|
|
12
12
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -17,6 +17,10 @@ function isOpenAIResponse(response: unknown): response is OpenAIResponse {
|
|
|
17
17
|
return isRecord(response) && response.object === "chat.completion";
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
function isOpenAIResponsesResponse(response: unknown): response is OpenAIResponsesResponse {
|
|
21
|
+
return isRecord(response) && response.object === "response";
|
|
22
|
+
}
|
|
23
|
+
|
|
20
24
|
function isAnthropicResponse(response: unknown): response is AnthropicResponse {
|
|
21
25
|
return isRecord(response) && response.type === "message" && Array.isArray(response.content);
|
|
22
26
|
}
|
|
@@ -28,6 +32,9 @@ export function formatViewFor(
|
|
|
28
32
|
if (apiFormat === "openai" && isOpenAIResponse(response)) {
|
|
29
33
|
return <OpenAIResponseView response={response} />;
|
|
30
34
|
}
|
|
35
|
+
if (apiFormat === "openai" && isOpenAIResponsesResponse(response)) {
|
|
36
|
+
return <OpenAIResponsesResponseView response={response} />;
|
|
37
|
+
}
|
|
31
38
|
if (apiFormat === "anthropic" && isAnthropicResponse(response)) {
|
|
32
39
|
return <StructuredResponseViewAnthropic response={response} />;
|
|
33
40
|
}
|