@tonyclaw/agent-inspector 2.0.17 → 2.0.19
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/cli.js +149 -40
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-DDg7Cs4N.js → CompareDrawer-D-90PIPw.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-CxRDWacg.js +114 -0
- package/.output/public/assets/ReplayDialog-DzlLYoTO.js +1 -0
- package/.output/public/assets/{RequestAnatomy-DR8OICOJ.js → RequestAnatomy-0hbyf6oa.js} +1 -1
- package/.output/public/assets/ResponseView-CKe0tNqr.js +1 -0
- package/.output/public/assets/StreamingChunkSequence-enhfdeNh.js +1 -0
- package/.output/public/assets/_sessionId-CJ-SusES.js +1 -0
- package/.output/public/assets/index-BbFsmQRC.js +1 -0
- package/.output/public/assets/index-BfBgrkgU.css +1 -0
- package/.output/public/assets/{main-bwZlEXw2.js → main-o9-hg8g1.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +141 -118
- package/.output/server/_libs/tanstack__react-virtual.mjs +13 -2
- package/.output/server/_libs/tanstack__virtual-core.mjs +24 -1
- package/.output/server/{_sessionId-BEuJJhqT.mjs → _sessionId-F8lm6PIb.mjs} +3 -2
- package/.output/server/_ssr/{CompareDrawer-yTO93GMz.mjs → CompareDrawer-Cf1yfwfd.mjs} +4 -3
- package/.output/server/_ssr/{ProxyViewerContainer-C8qCkHGB.mjs → ProxyViewerContainer-Bmu9867L.mjs} +349 -74
- package/.output/server/_ssr/{ReplayDialog-DRF9PG7Z.mjs → ReplayDialog-DxZfLBOe.mjs} +199 -17
- package/.output/server/_ssr/{RequestAnatomy-DBcuV_jV.mjs → RequestAnatomy-CbdERGZd.mjs} +4 -3
- package/.output/server/_ssr/{ResponseView-JJm78HZT.mjs → ResponseView-CvLd6qxI.mjs} +4 -3
- package/.output/server/_ssr/{StreamingChunkSequence-DyGKjBYx.mjs → StreamingChunkSequence-B_lkRB6o.mjs} +4 -3
- package/.output/server/_ssr/{index-T7JG28t6.mjs → index-ejrSItfz.mjs} +3 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-BJob8RN3.mjs → router-SbCnbDc2.mjs} +896 -240
- package/.output/server/{_tanstack-start-manifest_v-CuiFi3XC.mjs → _tanstack-start-manifest_v-yx8ftqWz.mjs} +1 -1
- package/.output/server/index.mjs +60 -60
- package/README.md +26 -109
- package/package.json +2 -1
- package/src/cli/doctor.ts +160 -8
- package/src/cli/templates/skill-onboard.ts +2 -20
- package/src/components/OnboardingBanner.tsx +13 -13
- package/src/components/ProxyViewer.tsx +55 -38
- package/src/components/ProxyViewerContainer.tsx +5 -0
- package/src/components/providers/SettingsDialog.tsx +113 -2
- package/src/components/proxy-viewer/ConversationGroup.tsx +20 -3
- package/src/components/proxy-viewer/ConversationGroupList.tsx +144 -0
- package/src/components/proxy-viewer/ConversationHeader.tsx +0 -2
- package/src/components/proxy-viewer/ReplayDialog.tsx +200 -13
- package/src/components/proxy-viewer/index.ts +1 -1
- package/src/knowledge/openclawClient.ts +0 -8
- package/src/knowledge/types.ts +0 -2
- package/src/lib/export-logs.ts +72 -5
- package/src/lib/providerTestContract.ts +0 -1
- package/src/lib/useProviders.ts +1 -1
- package/src/mcp/server.ts +14 -6
- package/src/mcp/toolHandlers.ts +21 -4
- package/src/proxy/chunkStorage.ts +2 -4
- package/src/proxy/dataDir.ts +37 -15
- package/src/proxy/formats/anthropic/index.ts +0 -1
- package/src/proxy/formats/anthropic/schemas.ts +0 -2
- package/src/proxy/handler.ts +41 -29
- package/src/proxy/logFinalizer.ts +93 -11
- package/src/proxy/logger.ts +35 -10
- package/src/proxy/providers.ts +12 -5
- package/src/proxy/rawStreamCapture.ts +85 -0
- package/src/proxy/schemas.ts +1 -51
- package/src/proxy/sessionRuntime.ts +0 -9
- package/src/proxy/socketTracker.ts +15 -8
- package/src/proxy/store.ts +414 -13
- package/src/routes/api/logs.$id.replay.ts +66 -1
- package/src/routes/api/logs.stream.ts +11 -3
- package/src/routes/api/logs.ts +51 -17
- package/src/routes/api/providers.$providerId.ts +10 -3
- package/src/routes/api/providers.ts +6 -3
- package/.output/public/assets/ProxyViewerContainer-DbbK1y7N.js +0 -114
- package/.output/public/assets/ReplayDialog-C1zWNkoz.js +0 -1
- package/.output/public/assets/ResponseView-Dh9iSj6h.js +0 -1
- package/.output/public/assets/StreamingChunkSequence-B_xElyH3.js +0 -1
- package/.output/public/assets/_sessionId-gv1a_NNu.js +0 -1
- package/.output/public/assets/index-D_nZj9Vt.css +0 -1
- package/.output/public/assets/index-u-LwDaeH.js +0 -1
- package/src/components/ui/json-expansion-button.tsx +0 -56
|
@@ -101,8 +101,6 @@ agent_inspector_data_dir() {
|
|
|
101
101
|
case "$AGENT_INSPECTOR_DATA_DIR" in /*) printf '%s\n' "$AGENT_INSPECTOR_DATA_DIR" ;; *) printf '%s\n' "$base/$AGENT_INSPECTOR_DATA_DIR" ;; esac
|
|
102
102
|
elif [ -n "\${AGENT_INSPECTOR_CONFIG_DIR:-}" ]; then
|
|
103
103
|
case "$AGENT_INSPECTOR_CONFIG_DIR" in /*) printf '%s\n' "$AGENT_INSPECTOR_CONFIG_DIR" ;; *) printf '%s\n' "$base/$AGENT_INSPECTOR_CONFIG_DIR" ;; esac
|
|
104
|
-
elif { [ ! -e "$base/.agent-inspector/providers.json" ] && [ ! -e "$base/.agent-inspector/config.json" ] && [ ! -e "$base/.agent-inspector/logs" ] && [ ! -e "$base/.agent-inspector/chunks" ]; } && { [ -e "$base/.llm-inspector/providers.json" ] || [ -e "$base/.llm-inspector/config.json" ] || [ -e "$base/.llm-inspector/logs" ] || [ -e "$base/.llm-inspector/chunks" ]; }; then
|
|
105
|
-
printf '%s\n' "$base/.llm-inspector"
|
|
106
104
|
else
|
|
107
105
|
printf '%s\n' "$base/.agent-inspector"
|
|
108
106
|
fi
|
|
@@ -149,14 +147,7 @@ function Resolve-AgentInspectorDataDir {
|
|
|
149
147
|
if ([System.IO.Path]::IsPathRooted($env:AGENT_INSPECTOR_CONFIG_DIR)) { return $env:AGENT_INSPECTOR_CONFIG_DIR }
|
|
150
148
|
return (Join-Path $base $env:AGENT_INSPECTOR_CONFIG_DIR)
|
|
151
149
|
}
|
|
152
|
-
|
|
153
|
-
$legacy = Join-Path $base '.llm-inspector'
|
|
154
|
-
$currentHasState = (Test-Path (Join-Path $current 'providers.json')) -or (Test-Path (Join-Path $current 'config.json')) -or (Test-Path (Join-Path $current 'logs')) -or (Test-Path (Join-Path $current 'chunks'))
|
|
155
|
-
$legacyHasState = (Test-Path (Join-Path $legacy 'providers.json')) -or (Test-Path (Join-Path $legacy 'config.json')) -or (Test-Path (Join-Path $legacy 'logs')) -or (Test-Path (Join-Path $legacy 'chunks'))
|
|
156
|
-
if (-not $currentHasState -and $legacyHasState) {
|
|
157
|
-
return $legacy
|
|
158
|
-
}
|
|
159
|
-
return $current
|
|
150
|
+
return (Join-Path $base '.agent-inspector')
|
|
160
151
|
}
|
|
161
152
|
$dataDir = Resolve-AgentInspectorDataDir
|
|
162
153
|
$providers = Join-Path $dataDir 'providers.json'
|
|
@@ -272,8 +263,6 @@ agent_inspector_data_dir() {
|
|
|
272
263
|
case "$AGENT_INSPECTOR_DATA_DIR" in /*) printf '%s\n' "$AGENT_INSPECTOR_DATA_DIR" ;; *) printf '%s\n' "$base/$AGENT_INSPECTOR_DATA_DIR" ;; esac
|
|
273
264
|
elif [ -n "\${AGENT_INSPECTOR_CONFIG_DIR:-}" ]; then
|
|
274
265
|
case "$AGENT_INSPECTOR_CONFIG_DIR" in /*) printf '%s\n' "$AGENT_INSPECTOR_CONFIG_DIR" ;; *) printf '%s\n' "$base/$AGENT_INSPECTOR_CONFIG_DIR" ;; esac
|
|
275
|
-
elif { [ ! -e "$base/.agent-inspector/providers.json" ] && [ ! -e "$base/.agent-inspector/config.json" ] && [ ! -e "$base/.agent-inspector/logs" ] && [ ! -e "$base/.agent-inspector/chunks" ]; } && { [ -e "$base/.llm-inspector/providers.json" ] || [ -e "$base/.llm-inspector/config.json" ] || [ -e "$base/.llm-inspector/logs" ] || [ -e "$base/.llm-inspector/chunks" ]; }; then
|
|
276
|
-
printf '%s\n' "$base/.llm-inspector"
|
|
277
266
|
else
|
|
278
267
|
printf '%s\n' "$base/.agent-inspector"
|
|
279
268
|
fi
|
|
@@ -317,14 +306,7 @@ function Resolve-AgentInspectorDataDir {
|
|
|
317
306
|
if ([System.IO.Path]::IsPathRooted($env:AGENT_INSPECTOR_CONFIG_DIR)) { return $env:AGENT_INSPECTOR_CONFIG_DIR }
|
|
318
307
|
return (Join-Path $base $env:AGENT_INSPECTOR_CONFIG_DIR)
|
|
319
308
|
}
|
|
320
|
-
|
|
321
|
-
$legacy = Join-Path $base '.llm-inspector'
|
|
322
|
-
$currentHasState = (Test-Path (Join-Path $current 'providers.json')) -or (Test-Path (Join-Path $current 'config.json')) -or (Test-Path (Join-Path $current 'logs')) -or (Test-Path (Join-Path $current 'chunks'))
|
|
323
|
-
$legacyHasState = (Test-Path (Join-Path $legacy 'providers.json')) -or (Test-Path (Join-Path $legacy 'config.json')) -or (Test-Path (Join-Path $legacy 'logs')) -or (Test-Path (Join-Path $legacy 'chunks'))
|
|
324
|
-
if (-not $currentHasState -and $legacyHasState) {
|
|
325
|
-
return $legacy
|
|
326
|
-
}
|
|
327
|
-
return $current
|
|
309
|
+
return (Join-Path $base '.agent-inspector')
|
|
328
310
|
}
|
|
329
311
|
$dir = Resolve-AgentInspectorDataDir
|
|
330
312
|
$file = Join-Path $dir 'providers.json'
|
|
@@ -25,41 +25,41 @@ export function OnboardingBanner(): JSX.Element | null {
|
|
|
25
25
|
? "Agent Inspector is ready in Full mode"
|
|
26
26
|
: "Agent Inspector is ready in Simple mode"}
|
|
27
27
|
</div>
|
|
28
|
-
<
|
|
28
|
+
<ol className="space-y-0.5 text-xs leading-relaxed text-muted-foreground">
|
|
29
|
+
<li>
|
|
30
|
+
<strong>1. Start in the right mode</strong>: this process is currently running in{" "}
|
|
31
|
+
<code>{captureMode}</code>. Mode is fixed at startup.
|
|
32
|
+
</li>
|
|
29
33
|
{isFullMode ? (
|
|
30
34
|
<>
|
|
31
35
|
<li>
|
|
32
|
-
<strong>Full
|
|
33
|
-
for
|
|
36
|
+
<strong>2. Full diagnostics</strong>: raw headers, raw response, and SSE chunks are
|
|
37
|
+
captured for deep investigations.
|
|
34
38
|
</li>
|
|
35
39
|
<li>
|
|
36
|
-
<strong>Daily work</strong>: restart with
|
|
37
|
-
to reduce capture overhead.
|
|
40
|
+
<strong>3. Daily work</strong>: restart with{" "}
|
|
41
|
+
<code>agent-inspector --mode simple</code> to reduce capture overhead.
|
|
38
42
|
</li>
|
|
39
43
|
</>
|
|
40
44
|
) : (
|
|
41
45
|
<>
|
|
42
46
|
<li>
|
|
43
|
-
<strong>Simple
|
|
44
|
-
|
|
47
|
+
<strong>2. Simple capture</strong>: requests, responses, tools, timing, tokens,
|
|
48
|
+
replay, and memory candidates are available with lower overhead.
|
|
45
49
|
</li>
|
|
46
50
|
<li>
|
|
47
|
-
<strong>Need diagnostics?</strong> Restart with{" "}
|
|
51
|
+
<strong>3. Need raw diagnostics?</strong> Restart with{" "}
|
|
48
52
|
<code>agent-inspector --mode full</code> for raw headers, raw response, and SSE
|
|
49
53
|
chunks.
|
|
50
54
|
</li>
|
|
51
55
|
</>
|
|
52
56
|
)}
|
|
53
|
-
<li>
|
|
54
|
-
<strong>Replay and Memory</strong>: available in both modes for provider checks and
|
|
55
|
-
reviewable knowledge candidates.
|
|
56
|
-
</li>
|
|
57
57
|
<li>
|
|
58
58
|
<strong>Agent setup</strong>: run <code>agent-inspector onboard --status</code>, then
|
|
59
59
|
use <code>/agent-inspector:onboard</code> in Claude Code or ask Codex to use{" "}
|
|
60
60
|
<code>agent-inspector-onboard</code>.
|
|
61
61
|
</li>
|
|
62
|
-
</
|
|
62
|
+
</ol>
|
|
63
63
|
</div>
|
|
64
64
|
<button
|
|
65
65
|
type="button"
|
|
@@ -2,13 +2,13 @@ import { type JSX, useCallback, useEffect, useMemo, useRef, useState, Suspense }
|
|
|
2
2
|
import { ArrowDownRight, ArrowLeft, ArrowUpRight, Check, Copy, Download, Plus } from "lucide-react";
|
|
3
3
|
|
|
4
4
|
import type { CapturedLog } from "../proxy/schemas";
|
|
5
|
-
import { exportLogsAsZip } from "../lib/export-logs";
|
|
5
|
+
import { exportLogsAsZip, type ExportMode } from "../lib/export-logs";
|
|
6
6
|
import type { CaptureMode, TimeDisplayFormat } from "../lib/runtimeConfig";
|
|
7
7
|
import { formatTimestampRange } from "../lib/timeDisplay";
|
|
8
8
|
import { cn, formatContextWindowTokens, formatTokens } from "../lib/utils";
|
|
9
9
|
import { useProviders } from "../lib/useProviders";
|
|
10
10
|
import packageJson from "../../package.json";
|
|
11
|
-
import {
|
|
11
|
+
import { groupLogsByConversation } from "./proxy-viewer";
|
|
12
12
|
|
|
13
13
|
import { CrabLogo } from "./ui/crab-logo";
|
|
14
14
|
import { crabVariants } from "./ui/crab-variants";
|
|
@@ -19,6 +19,7 @@ import { SettingsDialog } from "./providers/SettingsDialog";
|
|
|
19
19
|
import { computeCacheTrends } from "./proxy-viewer/cacheTrend";
|
|
20
20
|
import { LazyCompareDrawer } from "./proxy-viewer/lazy";
|
|
21
21
|
import { dispatchLogFocusRequest } from "./proxy-viewer/logFocus";
|
|
22
|
+
import { ConversationGroupList } from "./proxy-viewer/ConversationGroupList";
|
|
22
23
|
import { buildValidPredecessors } from "./proxy-viewer/viewerState";
|
|
23
24
|
import { useKeyboardNavigation } from "./proxy-viewer/useKeyboardNavigation";
|
|
24
25
|
import { SegmentBar } from "./proxy-viewer/anatomy/SegmentBar";
|
|
@@ -34,7 +35,7 @@ import {
|
|
|
34
35
|
|
|
35
36
|
function truncateSessionId(id: string): string {
|
|
36
37
|
if (id.length <= 30) return id;
|
|
37
|
-
return id.slice(0, 12)
|
|
38
|
+
return `${id.slice(0, 12)}...${id.slice(-12)}`;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
function computeTokenSummary(logs: CapturedLog[]): { totalIn: number; totalOut: number } {
|
|
@@ -635,7 +636,7 @@ export function ProxyViewer({
|
|
|
635
636
|
}: ProxyViewerProps): JSX.Element {
|
|
636
637
|
const { totalIn, totalOut } = useMemo(() => computeTokenSummary(logs), [logs]);
|
|
637
638
|
const { providers } = useProviders();
|
|
638
|
-
const [exporting, setExporting] = useState(
|
|
639
|
+
const [exporting, setExporting] = useState<ExportMode | null>(null);
|
|
639
640
|
const [comparePair, setComparePair] = useState<[CapturedLog, CapturedLog] | null>(null);
|
|
640
641
|
const [crabEntrancePhase, setCrabEntrancePhase] = useState<"hidden" | "playing" | "done">(
|
|
641
642
|
"hidden",
|
|
@@ -665,14 +666,17 @@ export function ProxyViewer({
|
|
|
665
666
|
document.title = `${truncateSessionId(pinnedSessionId)} - ${requestLabel} - Agent Inspector`;
|
|
666
667
|
}, [logs.length, pinnedSessionId]);
|
|
667
668
|
|
|
668
|
-
const handleExport = useCallback(
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
669
|
+
const handleExport = useCallback(
|
|
670
|
+
async (mode: ExportMode) => {
|
|
671
|
+
setExporting(mode);
|
|
672
|
+
try {
|
|
673
|
+
await exportLogsAsZip(logs, mode);
|
|
674
|
+
} finally {
|
|
675
|
+
setExporting(null);
|
|
676
|
+
}
|
|
677
|
+
},
|
|
678
|
+
[logs],
|
|
679
|
+
);
|
|
676
680
|
// Close the compare drawer when the user changes the session or model
|
|
677
681
|
// filter, since the predecessor relationship may no longer be meaningful.
|
|
678
682
|
useEffect(() => {
|
|
@@ -710,11 +714,11 @@ export function ProxyViewer({
|
|
|
710
714
|
|
|
711
715
|
return (
|
|
712
716
|
<div className="max-w-[1400px] xl:max-w-[1600px] 2xl:max-w-[1800px] mx-auto px-6 pb-6">
|
|
713
|
-
{/* Sticky chrome
|
|
717
|
+
{/* Sticky chrome: brand row, optional pinned-session context, and the
|
|
714
718
|
filter/control bar stay visible while the user scrolls through the
|
|
715
719
|
log list. `z-30` sits above the per-conversation sticky header
|
|
716
720
|
(`z-10`) but below modal-level overlays (dialogs, dropdowns,
|
|
717
|
-
tooltips
|
|
721
|
+
tooltips: `z-50`). Background is fully opaque so logs don't bleed
|
|
718
722
|
through. */}
|
|
719
723
|
<div className="sticky top-0 z-30 bg-background pt-6">
|
|
720
724
|
{/* Brand row */}
|
|
@@ -722,7 +726,7 @@ export function ProxyViewer({
|
|
|
722
726
|
<div />
|
|
723
727
|
<h1 className="flex min-w-0 flex-col items-center gap-2 text-center">
|
|
724
728
|
<span className="flex max-w-[calc(100vw-7rem)] items-end gap-2 whitespace-nowrap">
|
|
725
|
-
{/* Crab family
|
|
729
|
+
{/* Crab family: hover to animate together */}
|
|
726
730
|
<span
|
|
727
731
|
className="flex shrink-0 items-end gap-1 group cursor-default"
|
|
728
732
|
aria-hidden="true"
|
|
@@ -858,7 +862,7 @@ export function ProxyViewer({
|
|
|
858
862
|
<span className="text-muted-foreground text-xs font-mono">
|
|
859
863
|
{logs.length} request{logs.length !== 1 ? "s" : ""}
|
|
860
864
|
{totalIn > 0 || totalOut > 0
|
|
861
|
-
? `
|
|
865
|
+
? ` - ${formatTokens(totalIn)} in / ${formatTokens(totalOut)} out`
|
|
862
866
|
: ""}
|
|
863
867
|
</span>
|
|
864
868
|
)}
|
|
@@ -866,13 +870,13 @@ export function ProxyViewer({
|
|
|
866
870
|
<button
|
|
867
871
|
type="button"
|
|
868
872
|
onClick={() => {
|
|
869
|
-
void handleExport();
|
|
873
|
+
void handleExport("redacted");
|
|
870
874
|
}}
|
|
871
|
-
disabled={exporting}
|
|
875
|
+
disabled={exporting !== null}
|
|
872
876
|
className="h-8 px-3 text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed inline-flex items-center gap-1.5 rounded-md hover:bg-muted"
|
|
873
|
-
title="Export
|
|
877
|
+
title="Export redacted logs as JSON ZIP"
|
|
874
878
|
>
|
|
875
|
-
{exporting ? (
|
|
879
|
+
{exporting === "redacted" ? (
|
|
876
880
|
<span>Exporting...</span>
|
|
877
881
|
) : (
|
|
878
882
|
<>
|
|
@@ -882,6 +886,19 @@ export function ProxyViewer({
|
|
|
882
886
|
)}
|
|
883
887
|
</button>
|
|
884
888
|
)}
|
|
889
|
+
{logs.length > 0 && (
|
|
890
|
+
<button
|
|
891
|
+
type="button"
|
|
892
|
+
onClick={() => {
|
|
893
|
+
void handleExport("raw");
|
|
894
|
+
}}
|
|
895
|
+
disabled={exporting !== null}
|
|
896
|
+
className="h-8 px-2 text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed rounded-md hover:bg-muted"
|
|
897
|
+
title="Export raw logs without redaction"
|
|
898
|
+
>
|
|
899
|
+
{exporting === "raw" ? "Exporting..." : "Raw"}
|
|
900
|
+
</button>
|
|
901
|
+
)}
|
|
885
902
|
<button
|
|
886
903
|
type="button"
|
|
887
904
|
onClick={onClearAll}
|
|
@@ -923,7 +940,10 @@ export function ProxyViewer({
|
|
|
923
940
|
<div className="text-center text-muted-foreground py-16 space-y-4">
|
|
924
941
|
<p className="text-sm">No requests captured yet.</p>
|
|
925
942
|
<p className="text-xs">Route AI coding tools through the proxy:</p>
|
|
926
|
-
<
|
|
943
|
+
<div className="flex flex-col items-center gap-2">
|
|
944
|
+
<CopyableCommand command="ANTHROPIC_BASE_URL=http://localhost:25947/proxy <your-tool>" />
|
|
945
|
+
<CopyableCommand command="OPENAI_BASE_URL=http://localhost:25947/proxy <your-tool>" />
|
|
946
|
+
</div>
|
|
927
947
|
</div>
|
|
928
948
|
)
|
|
929
949
|
) : (
|
|
@@ -933,28 +953,25 @@ export function ProxyViewer({
|
|
|
933
953
|
className="flex flex-col gap-2 focus:outline-none"
|
|
934
954
|
>
|
|
935
955
|
<div ref={logListRef}>
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
timeDisplayFormat={timeDisplayFormat}
|
|
950
|
-
/>
|
|
951
|
-
))}
|
|
956
|
+
<ConversationGroupList
|
|
957
|
+
groups={groups}
|
|
958
|
+
viewMode={viewMode}
|
|
959
|
+
strip={strip}
|
|
960
|
+
slowResponseThresholdSeconds={slowResponseThresholdSeconds}
|
|
961
|
+
cacheTrends={cacheTrends}
|
|
962
|
+
onCompareWithPrevious={handleCompareWithPrevious}
|
|
963
|
+
comparisonPredecessors={comparisonPredecessors}
|
|
964
|
+
onClearGroup={onClearGroup}
|
|
965
|
+
standalone={groups.length === 1}
|
|
966
|
+
hasPinnedSessionContext={hasSessionContext}
|
|
967
|
+
timeDisplayFormat={timeDisplayFormat}
|
|
968
|
+
/>
|
|
952
969
|
</div>
|
|
953
970
|
</div>
|
|
954
971
|
)}
|
|
955
972
|
</div>
|
|
956
973
|
|
|
957
|
-
{/* Compare drawer
|
|
974
|
+
{/* Compare drawer: sibling of the log list, not a route change. */}
|
|
958
975
|
{comparePair !== null && (
|
|
959
976
|
<Suspense fallback={null}>
|
|
960
977
|
<LazyCompareDrawer left={comparePair[0]} right={comparePair[1]} onClose={closeCompare} />
|
|
@@ -4,6 +4,7 @@ import { CapturedLogSchema, type CapturedLog } from "../proxy/schemas";
|
|
|
4
4
|
import { useStripConfig } from "../lib/useStripConfig";
|
|
5
5
|
import { OnboardingBanner } from "./OnboardingBanner";
|
|
6
6
|
import { ProxyViewer } from "./ProxyViewer";
|
|
7
|
+
import { dispatchLogFocusRequest } from "./proxy-viewer/logFocus";
|
|
7
8
|
|
|
8
9
|
type SSEUpdate =
|
|
9
10
|
| {
|
|
@@ -222,6 +223,10 @@ export function ProxyViewerContainer({
|
|
|
222
223
|
if (!hash.startsWith("#log-")) return;
|
|
223
224
|
if (handledHashRef.current === hash) return;
|
|
224
225
|
const targetId = hash.slice(1);
|
|
226
|
+
const logId = Number(targetId.replace(/^log-/, ""));
|
|
227
|
+
if (Number.isInteger(logId) && logId > 0) {
|
|
228
|
+
dispatchLogFocusRequest({ logId, tab: "request" });
|
|
229
|
+
}
|
|
225
230
|
let cancelled = false;
|
|
226
231
|
let attempts = 0;
|
|
227
232
|
let highlightedTarget: HTMLElement | null = null;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { type JSX, useState, useCallback, useMemo } from "react";
|
|
1
|
+
import { type JSX, useState, useCallback, useEffect, useMemo } from "react";
|
|
2
2
|
import { Check, Copy, Settings, Terminal } from "lucide-react";
|
|
3
|
+
import { z } from "zod";
|
|
3
4
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "../ui/dialog";
|
|
4
5
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "../ui/tabs";
|
|
5
6
|
import { Button } from "../ui/button";
|
|
@@ -72,6 +73,7 @@ export function SettingsDialog(): JSX.Element {
|
|
|
72
73
|
<TabsList>
|
|
73
74
|
<TabsTrigger value="providers">Providers</TabsTrigger>
|
|
74
75
|
<TabsTrigger value="proxy">Proxy</TabsTrigger>
|
|
76
|
+
<TabsTrigger value="storage">Storage</TabsTrigger>
|
|
75
77
|
<TabsTrigger value="onboarding">Onboarding</TabsTrigger>
|
|
76
78
|
</TabsList>
|
|
77
79
|
|
|
@@ -94,6 +96,9 @@ export function SettingsDialog(): JSX.Element {
|
|
|
94
96
|
<TabsContent value="proxy">
|
|
95
97
|
<ProxySettingsTab />
|
|
96
98
|
</TabsContent>
|
|
99
|
+
<TabsContent value="storage">
|
|
100
|
+
<StorageSettingsTab />
|
|
101
|
+
</TabsContent>
|
|
97
102
|
<TabsContent value="onboarding">
|
|
98
103
|
<OnboardingSettingsTab />
|
|
99
104
|
</TabsContent>
|
|
@@ -104,6 +109,109 @@ export function SettingsDialog(): JSX.Element {
|
|
|
104
109
|
);
|
|
105
110
|
}
|
|
106
111
|
|
|
112
|
+
type LogStorageStats = {
|
|
113
|
+
memoryCount: number;
|
|
114
|
+
logDir: string;
|
|
115
|
+
logFileCount: number;
|
|
116
|
+
logBytes: number;
|
|
117
|
+
chunkDir: string;
|
|
118
|
+
chunkFileCount: number;
|
|
119
|
+
chunkBytes: number;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const LogStorageStatsSchema = z.object({
|
|
123
|
+
memoryCount: z.number(),
|
|
124
|
+
logDir: z.string(),
|
|
125
|
+
logFileCount: z.number(),
|
|
126
|
+
logBytes: z.number(),
|
|
127
|
+
chunkDir: z.string(),
|
|
128
|
+
chunkFileCount: z.number(),
|
|
129
|
+
chunkBytes: z.number(),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
function formatBytes(bytes: number): string {
|
|
133
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
134
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`;
|
|
135
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MiB`;
|
|
136
|
+
return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GiB`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function StorageSettingsTab(): JSX.Element {
|
|
140
|
+
const [stats, setStats] = useState<LogStorageStats | null>(null);
|
|
141
|
+
const [error, setError] = useState<string | null>(null);
|
|
142
|
+
const [loading, setLoading] = useState(false);
|
|
143
|
+
|
|
144
|
+
const refresh = useCallback(async () => {
|
|
145
|
+
setLoading(true);
|
|
146
|
+
setError(null);
|
|
147
|
+
try {
|
|
148
|
+
const response = await fetch("/api/logs?stats=1");
|
|
149
|
+
if (!response.ok) {
|
|
150
|
+
setError(`Failed to load storage stats: HTTP ${String(response.status)}`);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const data: unknown = await response.json();
|
|
154
|
+
const parsed = LogStorageStatsSchema.safeParse(data);
|
|
155
|
+
if (!parsed.success) {
|
|
156
|
+
setError("Storage stats response was not valid.");
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
setStats(parsed.data);
|
|
160
|
+
} catch (err) {
|
|
161
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
162
|
+
} finally {
|
|
163
|
+
setLoading(false);
|
|
164
|
+
}
|
|
165
|
+
}, []);
|
|
166
|
+
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
void refresh();
|
|
169
|
+
}, [refresh]);
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<div className="space-y-4">
|
|
173
|
+
<div className="flex items-center justify-between gap-3">
|
|
174
|
+
<div>
|
|
175
|
+
<h3 className="text-sm font-semibold">Log storage</h3>
|
|
176
|
+
<p className="text-xs text-muted-foreground">
|
|
177
|
+
Inspect local log and streaming chunk footprint before exporting or clearing sessions.
|
|
178
|
+
</p>
|
|
179
|
+
</div>
|
|
180
|
+
<Button type="button" variant="outline" size="sm" onClick={() => void refresh()}>
|
|
181
|
+
{loading ? "Refreshing..." : "Refresh"}
|
|
182
|
+
</Button>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
{stats !== null && (
|
|
186
|
+
<div className="grid gap-2 text-xs">
|
|
187
|
+
<div className="rounded-md border border-border bg-muted/20 px-3 py-2">
|
|
188
|
+
<div className="font-medium">In-memory logs</div>
|
|
189
|
+
<div className="mt-1 font-mono text-muted-foreground">
|
|
190
|
+
{stats.memoryCount.toLocaleString()} loaded
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
<div className="rounded-md border border-border bg-muted/20 px-3 py-2">
|
|
194
|
+
<div className="font-medium">Log files</div>
|
|
195
|
+
<div className="mt-1 break-all font-mono text-muted-foreground">{stats.logDir}</div>
|
|
196
|
+
<div className="mt-1 font-mono text-muted-foreground">
|
|
197
|
+
{stats.logFileCount.toLocaleString()} files / {formatBytes(stats.logBytes)}
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
<div className="rounded-md border border-border bg-muted/20 px-3 py-2">
|
|
201
|
+
<div className="font-medium">Streaming chunks</div>
|
|
202
|
+
<div className="mt-1 break-all font-mono text-muted-foreground">{stats.chunkDir}</div>
|
|
203
|
+
<div className="mt-1 font-mono text-muted-foreground">
|
|
204
|
+
{stats.chunkFileCount.toLocaleString()} files / {formatBytes(stats.chunkBytes)}
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
)}
|
|
209
|
+
|
|
210
|
+
{error !== null && <p className="text-xs text-destructive">{error}</p>}
|
|
211
|
+
</div>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
107
215
|
function CopyableSetupValue({
|
|
108
216
|
id,
|
|
109
217
|
label,
|
|
@@ -278,6 +386,9 @@ function ProxySettingsTab(): JSX.Element {
|
|
|
278
386
|
<p className="text-xs text-muted-foreground">
|
|
279
387
|
This process is running in <code>{captureMode}</code> mode. Restart with{" "}
|
|
280
388
|
<code>agent-inspector --mode {alternateCaptureMode}</code> to change it.
|
|
389
|
+
{captureMode === "simple"
|
|
390
|
+
? " Simple mode keeps replay, parsed requests, responses, timing, tools, and token usage while skipping raw headers, raw response retention, and detailed SSE chunk retention."
|
|
391
|
+
: " Full mode retains raw headers, raw responses, and SSE chunks for diagnostics; use simple mode for everyday lower-overhead capture."}
|
|
281
392
|
</p>
|
|
282
393
|
</div>
|
|
283
394
|
|
|
@@ -305,7 +416,7 @@ function ProxySettingsTab(): JSX.Element {
|
|
|
305
416
|
className="size-4 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50"
|
|
306
417
|
/>
|
|
307
418
|
<span className="text-sm">
|
|
308
|
-
{isLoading ? "Loading
|
|
419
|
+
{isLoading ? "Loading..." : strip ? "Stripping enabled" : "Stripping disabled"}
|
|
309
420
|
</span>
|
|
310
421
|
</label>
|
|
311
422
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, memo, useEffect, useMemo } from "react";
|
|
1
|
+
import { useState, memo, useCallback, useEffect, useMemo } from "react";
|
|
2
2
|
import type { JSX } from "react";
|
|
3
3
|
import type { TimeDisplayFormat } from "../../lib/runtimeConfig";
|
|
4
4
|
import type { CapturedLog } from "../../proxy/schemas";
|
|
@@ -37,6 +37,10 @@ export type ConversationGroupProps = {
|
|
|
37
37
|
timeDisplayFormat: TimeDisplayFormat;
|
|
38
38
|
/** Clear all logs that belong to this group. */
|
|
39
39
|
onClearGroup: (ids: number[]) => void;
|
|
40
|
+
/** Optional controlled expansion state used by virtualized lists. */
|
|
41
|
+
expanded?: boolean;
|
|
42
|
+
/** Optional controlled expansion setter used by virtualized lists. */
|
|
43
|
+
onExpandedChange?: (expanded: boolean) => void;
|
|
40
44
|
};
|
|
41
45
|
|
|
42
46
|
function computeStats(logs: CapturedLog[]): {
|
|
@@ -64,8 +68,21 @@ export const ConversationGroup = memo(function ({
|
|
|
64
68
|
standalone = false,
|
|
65
69
|
hasPinnedSessionContext = false,
|
|
66
70
|
timeDisplayFormat,
|
|
71
|
+
expanded: controlledExpanded,
|
|
72
|
+
onExpandedChange,
|
|
67
73
|
}: ConversationGroupProps): JSX.Element {
|
|
68
|
-
const [
|
|
74
|
+
const [internalExpanded, setInternalExpanded] = useState(false);
|
|
75
|
+
const expanded = controlledExpanded ?? internalExpanded;
|
|
76
|
+
const setExpanded = useCallback(
|
|
77
|
+
(nextExpanded: boolean) => {
|
|
78
|
+
if (onExpandedChange !== undefined) {
|
|
79
|
+
onExpandedChange(nextExpanded);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
setInternalExpanded(nextExpanded);
|
|
83
|
+
},
|
|
84
|
+
[onExpandedChange],
|
|
85
|
+
);
|
|
69
86
|
const stats = useMemo(() => computeStats(group.logs), [group.logs]);
|
|
70
87
|
const startTime = group.logs[0]?.timestamp ?? new Date().toISOString();
|
|
71
88
|
const endTime = group.logs[group.logs.length - 1]?.timestamp ?? new Date().toISOString();
|
|
@@ -89,7 +106,7 @@ export const ConversationGroup = memo(function ({
|
|
|
89
106
|
return () => {
|
|
90
107
|
window.removeEventListener(LOG_FOCUS_REQUEST_EVENT, handleLogFocusRequest);
|
|
91
108
|
};
|
|
92
|
-
}, [group.logs, standalone]);
|
|
109
|
+
}, [group.logs, setExpanded, standalone]);
|
|
93
110
|
|
|
94
111
|
return (
|
|
95
112
|
<div className="mb-2">
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { type JSX, useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { useWindowVirtualizer } from "@tanstack/react-virtual";
|
|
3
|
+
import type { CapturedLog } from "../../proxy/schemas";
|
|
4
|
+
import { ConversationGroup, type ConversationGroupProps } from "./ConversationGroup";
|
|
5
|
+
import type { ConversationGroupData } from "./ConversationHeader";
|
|
6
|
+
import { LOG_FOCUS_REQUEST_EVENT, readLogFocusRequest } from "./logFocus";
|
|
7
|
+
|
|
8
|
+
const VIRTUALIZE_GROUP_THRESHOLD = 30;
|
|
9
|
+
const ESTIMATED_GROUP_HEIGHT = 112;
|
|
10
|
+
const VIRTUAL_OVERSCAN = 6;
|
|
11
|
+
|
|
12
|
+
type ConversationGroupListProps = Omit<
|
|
13
|
+
ConversationGroupProps,
|
|
14
|
+
"group" | "standalone" | "expanded" | "onExpandedChange"
|
|
15
|
+
> & {
|
|
16
|
+
groups: ConversationGroupData[];
|
|
17
|
+
standalone: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function shouldVirtualizeConversationGroups(groupCount: number): boolean {
|
|
21
|
+
return groupCount > VIRTUALIZE_GROUP_THRESHOLD;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function findConversationGroupIndexForLog(
|
|
25
|
+
groups: readonly ConversationGroupData[],
|
|
26
|
+
logId: number,
|
|
27
|
+
): number | null {
|
|
28
|
+
for (let index = 0; index < groups.length; index++) {
|
|
29
|
+
const group = groups[index];
|
|
30
|
+
if (group !== undefined && group.logs.some((log: CapturedLog) => log.id === logId)) {
|
|
31
|
+
return index;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function ConversationGroupList({
|
|
38
|
+
groups,
|
|
39
|
+
standalone,
|
|
40
|
+
...groupProps
|
|
41
|
+
}: ConversationGroupListProps): JSX.Element {
|
|
42
|
+
if (!shouldVirtualizeConversationGroups(groups.length)) {
|
|
43
|
+
return (
|
|
44
|
+
<>
|
|
45
|
+
{groups.map((group) => (
|
|
46
|
+
<ConversationGroup key={group.id} group={group} standalone={standalone} {...groupProps} />
|
|
47
|
+
))}
|
|
48
|
+
</>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<VirtualizedConversationGroupList groups={groups} standalone={standalone} {...groupProps} />
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function VirtualizedConversationGroupList({
|
|
58
|
+
groups,
|
|
59
|
+
standalone,
|
|
60
|
+
...groupProps
|
|
61
|
+
}: ConversationGroupListProps): JSX.Element {
|
|
62
|
+
const [expandedByGroupId, setExpandedByGroupId] = useState<ReadonlyMap<string, boolean>>(
|
|
63
|
+
() => new Map(),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const virtualizer = useWindowVirtualizer({
|
|
67
|
+
count: groups.length,
|
|
68
|
+
estimateSize: () => ESTIMATED_GROUP_HEIGHT,
|
|
69
|
+
overscan: VIRTUAL_OVERSCAN,
|
|
70
|
+
getItemKey: (index) => groups[index]?.id ?? index,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const setGroupExpanded = useCallback((groupId: string, expanded: boolean) => {
|
|
74
|
+
setExpandedByGroupId((previous) => {
|
|
75
|
+
const next = new Map(previous);
|
|
76
|
+
if (expanded) {
|
|
77
|
+
next.set(groupId, true);
|
|
78
|
+
} else {
|
|
79
|
+
next.delete(groupId);
|
|
80
|
+
}
|
|
81
|
+
return next;
|
|
82
|
+
});
|
|
83
|
+
}, []);
|
|
84
|
+
|
|
85
|
+
const indexByLogId = useMemo(() => {
|
|
86
|
+
const result = new Map<number, number>();
|
|
87
|
+
for (let index = 0; index < groups.length; index++) {
|
|
88
|
+
const group = groups[index];
|
|
89
|
+
if (group === undefined) continue;
|
|
90
|
+
for (const log of group.logs) {
|
|
91
|
+
result.set(log.id, index);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}, [groups]);
|
|
96
|
+
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
const handleLogFocusRequest = (event: Event): void => {
|
|
99
|
+
const request = readLogFocusRequest(event);
|
|
100
|
+
if (request === null) return;
|
|
101
|
+
const index = indexByLogId.get(request.logId);
|
|
102
|
+
if (index === undefined) return;
|
|
103
|
+
const group = groups[index];
|
|
104
|
+
if (group === undefined) return;
|
|
105
|
+
setGroupExpanded(group.id, true);
|
|
106
|
+
virtualizer.scrollToIndex(index, { align: "center" });
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
window.addEventListener(LOG_FOCUS_REQUEST_EVENT, handleLogFocusRequest);
|
|
110
|
+
return () => {
|
|
111
|
+
window.removeEventListener(LOG_FOCUS_REQUEST_EVENT, handleLogFocusRequest);
|
|
112
|
+
};
|
|
113
|
+
}, [groups, indexByLogId, setGroupExpanded, virtualizer]);
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<div
|
|
117
|
+
className="relative w-full"
|
|
118
|
+
style={{ height: `${virtualizer.getTotalSize()}px` }}
|
|
119
|
+
data-virtualized-conversation-list="true"
|
|
120
|
+
>
|
|
121
|
+
{virtualizer.getVirtualItems().map((virtualItem) => {
|
|
122
|
+
const group = groups[virtualItem.index];
|
|
123
|
+
if (group === undefined) return null;
|
|
124
|
+
return (
|
|
125
|
+
<div
|
|
126
|
+
key={virtualItem.key}
|
|
127
|
+
data-index={virtualItem.index}
|
|
128
|
+
ref={virtualizer.measureElement}
|
|
129
|
+
className="absolute left-0 top-0 w-full"
|
|
130
|
+
style={{ transform: `translateY(${virtualItem.start}px)` }}
|
|
131
|
+
>
|
|
132
|
+
<ConversationGroup
|
|
133
|
+
group={group}
|
|
134
|
+
standalone={standalone}
|
|
135
|
+
expanded={expandedByGroupId.get(group.id) ?? false}
|
|
136
|
+
onExpandedChange={(expanded) => setGroupExpanded(group.id, expanded)}
|
|
137
|
+
{...groupProps}
|
|
138
|
+
/>
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
})}
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
}
|