@tonyclaw/agent-inspector 3.0.8 → 3.0.10
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-Bth_Ywyt.js → CompareDrawer-BxLP7jmX.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-D_2ez29e.js +106 -0
- package/.output/public/assets/{ReplayDialog-C2YYAuTe.js → ReplayDialog-Btm-wONI.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-8k9dAX2j.js → RequestAnatomy-C9eZJN9E.js} +1 -1
- package/.output/public/assets/{ResponseView-DEL6JcXm.js → ResponseView-9R2MW1kp.js} +2 -2
- package/.output/public/assets/{StreamingChunkSequence-DngmoR9j.js → StreamingChunkSequence-zxqGpFfv.js} +1 -1
- package/.output/public/assets/{_sessionId-4vk9RMZ5.js → _sessionId-C0tPTQlC.js} +1 -1
- package/.output/public/assets/agent-inspector-58K1_MsC.ico +0 -0
- package/.output/public/assets/index-BIgsQvaF.css +1 -0
- package/.output/public/assets/index-DgVWrzsQ.js +1 -0
- package/.output/public/assets/{index-DINLvnm9.js → index-WiDkyjq8.js} +1 -1
- package/.output/public/assets/{json-viewer-CH5dvS2Y.js → json-viewer-BAmkfRDE.js} +1 -1
- package/.output/public/assets/{main-GWkZn4xy.js → main-2feHoEI6.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +2 -2
- package/.output/server/_libs/radix-ui__react-dialog.mjs +2 -2
- package/.output/server/{_sessionId-EgsjdkN7.mjs → _sessionId-BkoyescL.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-CaM8FYuD.mjs → CompareDrawer-irfFdkJ8.mjs} +3 -3
- package/.output/server/_ssr/{ProxyViewerContainer-EJIogcGd.mjs → ProxyViewerContainer-CcJr9STz.mjs} +574 -324
- package/.output/server/_ssr/{ReplayDialog-BPpJ2-q8.mjs → ReplayDialog-CJnmXQGT.mjs} +3 -3
- package/.output/server/_ssr/{RequestAnatomy-DeQAWnZx.mjs → RequestAnatomy-CupP0Wdb.mjs} +2 -2
- package/.output/server/_ssr/{ResponseView-Dshm8iQ7.mjs → ResponseView-Bp-0t5cd.mjs} +2 -2
- package/.output/server/_ssr/{StreamingChunkSequence-32yIU2hn.mjs → StreamingChunkSequence-BXSn4kMH.mjs} +2 -2
- package/.output/server/_ssr/{index-XRcx9blU.mjs → index-KVltJBV8.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-C0aX5rqC.mjs → json-viewer-EYQ5eG2Q.mjs} +2 -2
- package/.output/server/_ssr/{router-BI-Mvxz9.mjs → router-_cAQddXC.mjs} +39 -36
- package/.output/server/{_tanstack-start-manifest_v-jKScklL_.mjs → _tanstack-start-manifest_v-BfiF5iov.mjs} +1 -1
- package/.output/server/index.mjs +72 -65
- package/package.json +1 -1
- package/src/components/ProxyViewer.tsx +194 -29
- package/src/components/ProxyViewerContainer.tsx +145 -3
- package/src/components/clients/ClientLogo.tsx +16 -1
- package/src/components/providers/ProviderCard.tsx +83 -91
- package/src/components/providers/ProvidersPanel.tsx +7 -7
- package/src/components/providers/SettingsDialog.tsx +17 -22
- package/src/components/proxy-viewer/TurnGroup.tsx +0 -16
- package/src/components/proxy-viewer/log-formats/anthropic.ts +5 -5
- package/src/components/proxy-viewer/log-formats/openai.ts +7 -10
- package/src/components/proxy-viewer/requestTools.ts +5 -2
- package/src/routes/api/logs.ts +14 -5
- package/src/routes/api/providers.$providerId.test.log.ts +28 -24
- package/.output/public/assets/ProxyViewerContainer-C73EU9Jq.js +0 -106
- package/.output/public/assets/index-DBEB1rHe.css +0 -1
- package/.output/public/assets/index-LPIGiUMP.js +0 -1
|
@@ -52,6 +52,8 @@ import { groupLogsByConversation, type ConversationGroupData } from "./proxy-vie
|
|
|
52
52
|
import { CrabLogo } from "./ui/crab-logo";
|
|
53
53
|
import { crabVariants } from "./ui/crab-variants";
|
|
54
54
|
import { McpLogo } from "./ui/mcp-logo";
|
|
55
|
+
import { Button } from "./ui/button";
|
|
56
|
+
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "./ui/dialog";
|
|
55
57
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
|
|
56
58
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
|
|
57
59
|
import type { TransientToastInput } from "./ui/transient-toast";
|
|
@@ -149,6 +151,7 @@ export type SessionMembershipEvidence = {
|
|
|
149
151
|
|
|
150
152
|
export type LogPaginationControls = {
|
|
151
153
|
isLoading: boolean;
|
|
154
|
+
loadProgress?: SessionLoadProgress;
|
|
152
155
|
total: number | null;
|
|
153
156
|
pageSize: number;
|
|
154
157
|
hasOlder: boolean;
|
|
@@ -161,6 +164,14 @@ export type LogPaginationControls = {
|
|
|
161
164
|
onNewest: () => void;
|
|
162
165
|
};
|
|
163
166
|
|
|
167
|
+
export type SessionLoadProgress = {
|
|
168
|
+
label: string;
|
|
169
|
+
elapsedMs: number;
|
|
170
|
+
timeoutMs: number;
|
|
171
|
+
loadedLogs: number;
|
|
172
|
+
loadedPages: number;
|
|
173
|
+
};
|
|
174
|
+
|
|
164
175
|
function formatTimeRange(logs: CapturedLog[], timeDisplayFormat: TimeDisplayFormat): string | null {
|
|
165
176
|
const first = logs[0];
|
|
166
177
|
const last = logs[logs.length - 1];
|
|
@@ -1003,6 +1014,44 @@ function LogPaginationButton({
|
|
|
1003
1014
|
);
|
|
1004
1015
|
}
|
|
1005
1016
|
|
|
1017
|
+
function formatDurationSeconds(ms: number): string {
|
|
1018
|
+
return `${Math.max(0, Math.ceil(ms / 1000)).toString()}s`;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
function SessionLoadProgressBar({ progress }: { progress: SessionLoadProgress }): JSX.Element {
|
|
1022
|
+
const elapsedRatio = Math.min(1, progress.elapsedMs / progress.timeoutMs);
|
|
1023
|
+
const percent = Math.max(4, Math.round(elapsedRatio * 100));
|
|
1024
|
+
const remainingMs = Math.max(0, progress.timeoutMs - progress.elapsedMs);
|
|
1025
|
+
const nearTimeout = elapsedRatio >= 0.75;
|
|
1026
|
+
const details = [
|
|
1027
|
+
progress.loadedLogs > 0 ? `${progress.loadedLogs.toString()} logs` : null,
|
|
1028
|
+
progress.loadedPages > 0
|
|
1029
|
+
? `${progress.loadedPages.toString()} page${progress.loadedPages === 1 ? "" : "s"}`
|
|
1030
|
+
: null,
|
|
1031
|
+
`${formatDurationSeconds(remainingMs)} before timeout`,
|
|
1032
|
+
].filter((item): item is string => item !== null);
|
|
1033
|
+
|
|
1034
|
+
return (
|
|
1035
|
+
<div className="min-w-0 space-y-1 px-1 pb-1">
|
|
1036
|
+
<div className="flex min-w-0 items-center justify-between gap-3 font-mono text-[10px] text-muted-foreground">
|
|
1037
|
+
<span className="min-w-0 truncate">{progress.label}</span>
|
|
1038
|
+
<span className={cn("shrink-0 tabular-nums", nearTimeout && "text-amber-300")}>
|
|
1039
|
+
{details.join(" / ")}
|
|
1040
|
+
</span>
|
|
1041
|
+
</div>
|
|
1042
|
+
<div className="h-1.5 overflow-hidden rounded-full bg-white/[0.06]">
|
|
1043
|
+
<div
|
|
1044
|
+
className={cn(
|
|
1045
|
+
"h-full rounded-full transition-all duration-300",
|
|
1046
|
+
nearTimeout ? "bg-amber-300/80" : "bg-cyan-300/70",
|
|
1047
|
+
)}
|
|
1048
|
+
style={{ width: `${percent.toString()}%` }}
|
|
1049
|
+
/>
|
|
1050
|
+
</div>
|
|
1051
|
+
</div>
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1006
1055
|
function LogPaginationBar({
|
|
1007
1056
|
logs,
|
|
1008
1057
|
pagination,
|
|
@@ -1027,6 +1076,9 @@ function LogPaginationBar({
|
|
|
1027
1076
|
<div className="w-full min-w-0 px-1 font-mono text-[11px] text-muted-foreground sm:flex-1">
|
|
1028
1077
|
{formatPaginationStatus({ logs, pagination })}
|
|
1029
1078
|
{pagination.isLoading && logs.length > 0 && <span className="ml-2">Loading...</span>}
|
|
1079
|
+
{pagination.loadProgress !== undefined && (
|
|
1080
|
+
<SessionLoadProgressBar progress={pagination.loadProgress} />
|
|
1081
|
+
)}
|
|
1030
1082
|
</div>
|
|
1031
1083
|
<div className="flex w-full items-center gap-1 overflow-x-auto sm:w-auto">
|
|
1032
1084
|
<LogPaginationButton
|
|
@@ -1167,26 +1219,6 @@ function CopyableCommand({ command }: { command: string }): JSX.Element {
|
|
|
1167
1219
|
);
|
|
1168
1220
|
}
|
|
1169
1221
|
|
|
1170
|
-
function McpReadyBadge(): JSX.Element {
|
|
1171
|
-
return (
|
|
1172
|
-
<TooltipProvider>
|
|
1173
|
-
<Tooltip>
|
|
1174
|
-
<TooltipTrigger asChild>
|
|
1175
|
-
<span className="bg-cyan-300/[0.055] shadow-[inset_0_1px_0_rgba(255,255,255,0.05)] inline-flex h-7 items-center gap-2 rounded-md px-2.5 font-mono text-[11px] font-medium text-cyan-200">
|
|
1176
|
-
<span className="size-1.5 rounded-full bg-cyan-200 shadow-[0_0_8px_rgba(125,211,252,0.75)]" />
|
|
1177
|
-
MCP Ready
|
|
1178
|
-
<span className="hidden text-cyan-200/70 sm:inline">/api/mcp</span>
|
|
1179
|
-
</span>
|
|
1180
|
-
</TooltipTrigger>
|
|
1181
|
-
<TooltipContent sideOffset={8} className="max-w-[320px] text-left leading-relaxed">
|
|
1182
|
-
Coding agents can inspect logs, replay requests, test providers, and debug sessions
|
|
1183
|
-
through MCP at /api/mcp.
|
|
1184
|
-
</TooltipContent>
|
|
1185
|
-
</Tooltip>
|
|
1186
|
-
</TooltipProvider>
|
|
1187
|
-
);
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
1222
|
function SidebarPanel({
|
|
1191
1223
|
label,
|
|
1192
1224
|
children,
|
|
@@ -1249,7 +1281,7 @@ function CrabFamily({
|
|
|
1249
1281
|
entrancePhase: "hidden" | "playing" | "done";
|
|
1250
1282
|
}): JSX.Element {
|
|
1251
1283
|
if (compact) {
|
|
1252
|
-
return <CrabLogo className="size-
|
|
1284
|
+
return <CrabLogo className="size-14 text-amber-500" />;
|
|
1253
1285
|
}
|
|
1254
1286
|
|
|
1255
1287
|
return (
|
|
@@ -1295,7 +1327,7 @@ function BrandHeader({
|
|
|
1295
1327
|
return (
|
|
1296
1328
|
<div className="bg-white/[0.035] shadow-[inset_0_1px_0_rgba(255,255,255,0.045),0_16px_44px_rgba(0,0,0,0.18)] rounded-[8px] p-3">
|
|
1297
1329
|
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
1298
|
-
<div className="flex min-w-fit items-start gap-
|
|
1330
|
+
<div className="flex min-w-fit items-start gap-3">
|
|
1299
1331
|
<CrabFamily compact={true} entrancePhase={crabEntrancePhase} />
|
|
1300
1332
|
<div className="min-w-fit">
|
|
1301
1333
|
<div className="flex min-w-fit flex-wrap items-baseline gap-x-2 gap-y-0.5">
|
|
@@ -1316,9 +1348,6 @@ function BrandHeader({
|
|
|
1316
1348
|
<SettingsDialog />
|
|
1317
1349
|
</div>
|
|
1318
1350
|
</div>
|
|
1319
|
-
<div className="mt-2 hidden sm:block">
|
|
1320
|
-
<McpReadyBadge />
|
|
1321
|
-
</div>
|
|
1322
1351
|
</div>
|
|
1323
1352
|
);
|
|
1324
1353
|
}
|
|
@@ -1341,7 +1370,6 @@ function BrandHeader({
|
|
|
1341
1370
|
<span className="max-w-full text-sm font-semibold text-muted-foreground">
|
|
1342
1371
|
Local First. Evidence First.
|
|
1343
1372
|
</span>
|
|
1344
|
-
<McpReadyBadge />
|
|
1345
1373
|
</h1>
|
|
1346
1374
|
<div className="flex flex-1 justify-end gap-1">
|
|
1347
1375
|
<AlertsDialog />
|
|
@@ -1669,6 +1697,8 @@ export type ProxyViewerProps = {
|
|
|
1669
1697
|
onSessionChange: (session: string) => void;
|
|
1670
1698
|
onModelChange: (model: string) => void;
|
|
1671
1699
|
onClearAll: () => void;
|
|
1700
|
+
/** Delete visible logs from persisted storage when the user explicitly confirms it. */
|
|
1701
|
+
onDeleteCurrentView?: (ids: number[]) => Promise<void>;
|
|
1672
1702
|
/** Reload the selected/pinned session from stored logs without requiring a page refresh. */
|
|
1673
1703
|
onReloadSession?: () => void;
|
|
1674
1704
|
/** Clear only the logs whose ids are passed. Called by the per-group
|
|
@@ -1689,6 +1719,8 @@ export type ProxyViewerProps = {
|
|
|
1689
1719
|
isLoading?: boolean;
|
|
1690
1720
|
/** Cursor controls for history pages. Undefined keeps the live stream UI compact. */
|
|
1691
1721
|
pagination?: LogPaginationControls;
|
|
1722
|
+
/** Active session log loading progress, shown near load/reload controls. */
|
|
1723
|
+
sessionLoadProgress?: SessionLoadProgress;
|
|
1692
1724
|
/** Group/run member metadata attached to the pinned session id. */
|
|
1693
1725
|
sessionMemberships?: SessionMembershipEvidence[];
|
|
1694
1726
|
/** Hide the session filter dropdown. Used on `/session/$id` routes where
|
|
@@ -1708,6 +1740,7 @@ export function ProxyViewer({
|
|
|
1708
1740
|
onSessionChange,
|
|
1709
1741
|
onModelChange,
|
|
1710
1742
|
onClearAll,
|
|
1743
|
+
onDeleteCurrentView,
|
|
1711
1744
|
onReloadSession,
|
|
1712
1745
|
onClearGroup,
|
|
1713
1746
|
onNotify,
|
|
@@ -1719,6 +1752,7 @@ export function ProxyViewer({
|
|
|
1719
1752
|
timeDisplayFormat,
|
|
1720
1753
|
isLoading = false,
|
|
1721
1754
|
pagination,
|
|
1755
|
+
sessionLoadProgress,
|
|
1722
1756
|
sessionMemberships = [],
|
|
1723
1757
|
hideSessionFilter = false,
|
|
1724
1758
|
pinnedSessionId,
|
|
@@ -1730,6 +1764,9 @@ export function ProxyViewer({
|
|
|
1730
1764
|
const [importing, setImporting] = useState(false);
|
|
1731
1765
|
const [importStatus, setImportStatus] = useState<string | null>(null);
|
|
1732
1766
|
const [importError, setImportError] = useState<string | null>(null);
|
|
1767
|
+
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
1768
|
+
const [deleteStoredData, setDeleteStoredData] = useState(false);
|
|
1769
|
+
const [deletingCurrentView, setDeletingCurrentView] = useState(false);
|
|
1733
1770
|
const [comparePair, setComparePair] = useState<[CapturedLog, CapturedLog] | null>(null);
|
|
1734
1771
|
const [crabEntrancePhase, setCrabEntrancePhase] = useState<"hidden" | "playing" | "done">(
|
|
1735
1772
|
"hidden",
|
|
@@ -1860,6 +1897,41 @@ export function ProxyViewer({
|
|
|
1860
1897
|
onClearAll();
|
|
1861
1898
|
}, [cancelTransientTasks, onClearAll]);
|
|
1862
1899
|
|
|
1900
|
+
const handleRequestDeleteCurrentView = useCallback(() => {
|
|
1901
|
+
setDeleteStoredData(false);
|
|
1902
|
+
setDeleteDialogOpen(true);
|
|
1903
|
+
}, []);
|
|
1904
|
+
|
|
1905
|
+
const handleConfirmDeleteCurrentView = useCallback(() => {
|
|
1906
|
+
cancelTransientTasks();
|
|
1907
|
+
setImportStatus(null);
|
|
1908
|
+
setImportError(null);
|
|
1909
|
+
setComparePair(null);
|
|
1910
|
+
|
|
1911
|
+
if (!deleteStoredData || onDeleteCurrentView === undefined) {
|
|
1912
|
+
setDeleteDialogOpen(false);
|
|
1913
|
+
onClearAll();
|
|
1914
|
+
return;
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
setDeletingCurrentView(true);
|
|
1918
|
+
void onDeleteCurrentView(logs.map((log) => log.id))
|
|
1919
|
+
.then(() => {
|
|
1920
|
+
setDeleteDialogOpen(false);
|
|
1921
|
+
})
|
|
1922
|
+
.catch((err: unknown) => {
|
|
1923
|
+
const message = err instanceof Error ? err.message : "Failed to delete stored logs";
|
|
1924
|
+
if (onNotify !== undefined) {
|
|
1925
|
+
onNotify({ message, tone: "error" });
|
|
1926
|
+
} else {
|
|
1927
|
+
setImportError(message);
|
|
1928
|
+
}
|
|
1929
|
+
})
|
|
1930
|
+
.finally(() => {
|
|
1931
|
+
setDeletingCurrentView(false);
|
|
1932
|
+
});
|
|
1933
|
+
}, [cancelTransientTasks, deleteStoredData, logs, onClearAll, onDeleteCurrentView, onNotify]);
|
|
1934
|
+
|
|
1863
1935
|
const handleReloadCurrentSession = useCallback(() => {
|
|
1864
1936
|
if (onReloadSession === undefined) return;
|
|
1865
1937
|
cancelTransientTasks();
|
|
@@ -1989,14 +2061,33 @@ export function ProxyViewer({
|
|
|
1989
2061
|
disabled={!canClearCurrentView}
|
|
1990
2062
|
className={cn(
|
|
1991
2063
|
"bg-white/[0.035] hover:bg-white/[0.065] inline-flex h-8 min-w-0 items-center justify-center gap-1.5 rounded-md px-2 text-xs text-muted-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)] transition-colors hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
|
1992
|
-
onReloadSession === undefined &&
|
|
2064
|
+
onReloadSession === undefined &&
|
|
2065
|
+
onDeleteCurrentView === undefined &&
|
|
2066
|
+
"col-span-2",
|
|
1993
2067
|
)}
|
|
1994
2068
|
title="Clear the current view. Stored logs remain reloadable."
|
|
1995
2069
|
>
|
|
1996
2070
|
<Trash2 className="size-3.5" />
|
|
1997
|
-
<span>Clear
|
|
2071
|
+
<span>Clear</span>
|
|
1998
2072
|
</button>
|
|
2073
|
+
{onDeleteCurrentView !== undefined && (
|
|
2074
|
+
<button
|
|
2075
|
+
type="button"
|
|
2076
|
+
onClick={handleRequestDeleteCurrentView}
|
|
2077
|
+
disabled={!canClearCurrentView || deletingCurrentView}
|
|
2078
|
+
className="bg-red-500/[0.07] hover:bg-red-500/[0.12] inline-flex h-8 min-w-0 items-center justify-center gap-1.5 rounded-md px-2 text-xs text-red-200 shadow-[inset_0_0_0_1px_rgba(248,113,113,0.14)] transition-colors hover:text-red-100 disabled:cursor-not-allowed disabled:opacity-50"
|
|
2079
|
+
title="Choose whether to delete stored data for the current view."
|
|
2080
|
+
>
|
|
2081
|
+
<Trash2 className="size-3.5" />
|
|
2082
|
+
<span>Delete</span>
|
|
2083
|
+
</button>
|
|
2084
|
+
)}
|
|
1999
2085
|
</div>
|
|
2086
|
+
{sessionLoadProgress !== undefined && (
|
|
2087
|
+
<div className="mt-2 rounded-md bg-black/20 px-2 py-2 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.045)]">
|
|
2088
|
+
<SessionLoadProgressBar progress={sessionLoadProgress} />
|
|
2089
|
+
</div>
|
|
2090
|
+
)}
|
|
2000
2091
|
</SidebarPanel>
|
|
2001
2092
|
|
|
2002
2093
|
<SidebarPanel label="Workspace">
|
|
@@ -2192,9 +2283,26 @@ export function ProxyViewer({
|
|
|
2192
2283
|
title="Clear the current view. Stored logs remain reloadable."
|
|
2193
2284
|
>
|
|
2194
2285
|
<Trash2 className="size-3.5" />
|
|
2195
|
-
<span>Clear
|
|
2286
|
+
<span>Clear</span>
|
|
2196
2287
|
</button>
|
|
2288
|
+
{onDeleteCurrentView !== undefined && (
|
|
2289
|
+
<button
|
|
2290
|
+
type="button"
|
|
2291
|
+
onClick={handleRequestDeleteCurrentView}
|
|
2292
|
+
disabled={!canClearCurrentView || deletingCurrentView}
|
|
2293
|
+
className="bg-red-500/[0.07] hover:bg-red-500/[0.12] inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-xs text-red-200 shadow-[inset_0_0_0_1px_rgba(248,113,113,0.14)] transition-colors hover:text-red-100 disabled:cursor-not-allowed disabled:opacity-50"
|
|
2294
|
+
title="Choose whether to delete stored data for the current view."
|
|
2295
|
+
>
|
|
2296
|
+
<Trash2 className="size-3.5" />
|
|
2297
|
+
<span>Delete</span>
|
|
2298
|
+
</button>
|
|
2299
|
+
)}
|
|
2197
2300
|
</div>
|
|
2301
|
+
{pagination === undefined && sessionLoadProgress !== undefined && (
|
|
2302
|
+
<div className="mt-2 rounded-md bg-black/20 px-2 py-2 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.045)]">
|
|
2303
|
+
<SessionLoadProgressBar progress={sessionLoadProgress} />
|
|
2304
|
+
</div>
|
|
2305
|
+
)}
|
|
2198
2306
|
<LogPaginationBar logs={logs} pagination={pagination} embedded={true} />
|
|
2199
2307
|
{onNotify === undefined && exportError !== null && (
|
|
2200
2308
|
<div className="mt-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
|
|
@@ -2225,6 +2333,11 @@ export function ProxyViewer({
|
|
|
2225
2333
|
<p className="inline-block max-w-full rounded bg-muted px-3 py-1 font-mono text-xs break-all">
|
|
2226
2334
|
{truncateSessionId(selectedSession)}
|
|
2227
2335
|
</p>
|
|
2336
|
+
{sessionLoadProgress !== undefined && (
|
|
2337
|
+
<div className="mx-auto max-w-md rounded-md bg-black/20 px-2 py-2 text-left shadow-[inset_0_0_0_1px_rgba(255,255,255,0.045)]">
|
|
2338
|
+
<SessionLoadProgressBar progress={sessionLoadProgress} />
|
|
2339
|
+
</div>
|
|
2340
|
+
)}
|
|
2228
2341
|
{sessionMemberships.length > 0 && (
|
|
2229
2342
|
<div className="text-left">
|
|
2230
2343
|
<SessionMembershipPanel memberships={sessionMemberships} />
|
|
@@ -2315,6 +2428,58 @@ export function ProxyViewer({
|
|
|
2315
2428
|
</main>
|
|
2316
2429
|
</div>
|
|
2317
2430
|
|
|
2431
|
+
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
|
2432
|
+
<DialogContent className="border-red-500/20 bg-zinc-950 text-zinc-100 sm:max-w-md">
|
|
2433
|
+
<DialogHeader>
|
|
2434
|
+
<DialogTitle>Delete current logs?</DialogTitle>
|
|
2435
|
+
<DialogDescription>
|
|
2436
|
+
Choose whether this only clears the UI or also deletes the stored log data for the
|
|
2437
|
+
current view.
|
|
2438
|
+
</DialogDescription>
|
|
2439
|
+
</DialogHeader>
|
|
2440
|
+
<label className="flex items-start gap-3 rounded-md bg-red-500/[0.08] px-3 py-3 text-sm text-red-100 shadow-[inset_0_0_0_1px_rgba(248,113,113,0.16)]">
|
|
2441
|
+
<input
|
|
2442
|
+
type="checkbox"
|
|
2443
|
+
className="mt-0.5 size-4 shrink-0"
|
|
2444
|
+
checked={deleteStoredData}
|
|
2445
|
+
disabled={deletingCurrentView}
|
|
2446
|
+
onChange={(event) => setDeleteStoredData(event.currentTarget.checked)}
|
|
2447
|
+
/>
|
|
2448
|
+
<span className="min-w-0">
|
|
2449
|
+
<span className="block font-medium">Also delete stored data</span>
|
|
2450
|
+
<span className="mt-1 block text-xs text-red-200/80">
|
|
2451
|
+
Removes persisted log rows, streaming chunks, and session archive entries for these{" "}
|
|
2452
|
+
{logs.length.toString()} visible request{logs.length === 1 ? "" : "s"}.
|
|
2453
|
+
</span>
|
|
2454
|
+
</span>
|
|
2455
|
+
</label>
|
|
2456
|
+
<div className="flex justify-end gap-2">
|
|
2457
|
+
<Button
|
|
2458
|
+
type="button"
|
|
2459
|
+
variant="outline"
|
|
2460
|
+
size="sm"
|
|
2461
|
+
disabled={deletingCurrentView}
|
|
2462
|
+
onClick={() => setDeleteDialogOpen(false)}
|
|
2463
|
+
>
|
|
2464
|
+
Cancel
|
|
2465
|
+
</Button>
|
|
2466
|
+
<Button
|
|
2467
|
+
type="button"
|
|
2468
|
+
variant={deleteStoredData ? "destructive" : "secondary"}
|
|
2469
|
+
size="sm"
|
|
2470
|
+
disabled={deletingCurrentView}
|
|
2471
|
+
onClick={handleConfirmDeleteCurrentView}
|
|
2472
|
+
>
|
|
2473
|
+
{deletingCurrentView
|
|
2474
|
+
? "Deleting..."
|
|
2475
|
+
: deleteStoredData
|
|
2476
|
+
? "Delete data"
|
|
2477
|
+
: "Clear only"}
|
|
2478
|
+
</Button>
|
|
2479
|
+
</div>
|
|
2480
|
+
</DialogContent>
|
|
2481
|
+
</Dialog>
|
|
2482
|
+
|
|
2318
2483
|
{/* Compare drawer: sibling of the log list, not a route change. */}
|
|
2319
2484
|
{comparePair !== null && (
|
|
2320
2485
|
<Suspense fallback={null}>
|
|
@@ -9,6 +9,7 @@ import { OnboardingBanner } from "./OnboardingBanner";
|
|
|
9
9
|
import {
|
|
10
10
|
ProxyViewer,
|
|
11
11
|
type LogPaginationControls,
|
|
12
|
+
type SessionLoadProgress,
|
|
12
13
|
type SessionMembershipEvidence,
|
|
13
14
|
} from "./ProxyViewer";
|
|
14
15
|
import { dispatchLogFocusRequest } from "./proxy-viewer/logFocus";
|
|
@@ -49,6 +50,14 @@ type LogCursorPage = z.infer<typeof LogCursorPageSchema>;
|
|
|
49
50
|
|
|
50
51
|
const SessionsResponseSchema = z.array(z.string());
|
|
51
52
|
|
|
53
|
+
const DeleteLogsResponseSchema = z.object({
|
|
54
|
+
success: z.boolean(),
|
|
55
|
+
cleared: z.number().int().nonnegative(),
|
|
56
|
+
logFilesRewritten: z.number().int().nonnegative().optional(),
|
|
57
|
+
chunkFilesDeleted: z.number().int().nonnegative(),
|
|
58
|
+
sessionArchiveLogsDeleted: z.number().int().nonnegative().optional(),
|
|
59
|
+
});
|
|
60
|
+
|
|
52
61
|
type SessionPageRequest =
|
|
53
62
|
| { kind: "newest" }
|
|
54
63
|
| { kind: "oldest" }
|
|
@@ -236,12 +245,17 @@ export function ProxyViewerContainer({
|
|
|
236
245
|
const [streamInitialized, setStreamInitialized] = useState(initialSessionId === undefined);
|
|
237
246
|
const [logPage, setLogPage] = useState<LogCursorPage | null>(null);
|
|
238
247
|
const [sessionPageLoading, setSessionPageLoading] = useState(initialSessionId !== undefined);
|
|
248
|
+
const [sessionLoadProgress, setSessionLoadProgress] = useState<SessionLoadProgress | undefined>(
|
|
249
|
+
undefined,
|
|
250
|
+
);
|
|
239
251
|
const [sessionMemberships, setSessionMemberships] = useState<SessionMembershipEvidence[]>([]);
|
|
240
252
|
const [, startLogTransition] = useTransition();
|
|
241
253
|
const eventSourceRef = useRef<EventSource | null>(null);
|
|
242
254
|
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
243
255
|
const handledHashRef = useRef<string | null>(null);
|
|
244
256
|
const sessionPageRequestIdRef = useRef(0);
|
|
257
|
+
const sessionLoadStartedAtRef = useRef<number | null>(null);
|
|
258
|
+
const sessionLoadTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
245
259
|
|
|
246
260
|
// O(1) log lookup by id
|
|
247
261
|
const logIndexRef = useRef<Map<number, number>>(new Map());
|
|
@@ -256,6 +270,53 @@ export function ProxyViewerContainer({
|
|
|
256
270
|
[showToast],
|
|
257
271
|
);
|
|
258
272
|
|
|
273
|
+
const stopSessionLoadProgress = useCallback(() => {
|
|
274
|
+
if (sessionLoadTimerRef.current !== null) {
|
|
275
|
+
clearInterval(sessionLoadTimerRef.current);
|
|
276
|
+
sessionLoadTimerRef.current = null;
|
|
277
|
+
}
|
|
278
|
+
sessionLoadStartedAtRef.current = null;
|
|
279
|
+
setSessionLoadProgress(undefined);
|
|
280
|
+
}, []);
|
|
281
|
+
|
|
282
|
+
const startSessionLoadProgress = useCallback(
|
|
283
|
+
(label: string, requestId: number, loadedLogs = 0, loadedPages = 0) => {
|
|
284
|
+
if (sessionLoadTimerRef.current !== null) {
|
|
285
|
+
clearInterval(sessionLoadTimerRef.current);
|
|
286
|
+
}
|
|
287
|
+
const startedAt = Date.now();
|
|
288
|
+
sessionLoadStartedAtRef.current = startedAt;
|
|
289
|
+
const update = (): void => {
|
|
290
|
+
if (sessionPageRequestIdRef.current !== requestId) return;
|
|
291
|
+
setSessionLoadProgress({
|
|
292
|
+
label,
|
|
293
|
+
elapsedMs: Date.now() - startedAt,
|
|
294
|
+
timeoutMs: SESSION_LOGS_FETCH_TIMEOUT_MS,
|
|
295
|
+
loadedLogs,
|
|
296
|
+
loadedPages,
|
|
297
|
+
});
|
|
298
|
+
};
|
|
299
|
+
update();
|
|
300
|
+
sessionLoadTimerRef.current = setInterval(update, 250);
|
|
301
|
+
},
|
|
302
|
+
[],
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
const updateSessionLoadProgress = useCallback(
|
|
306
|
+
(label: string, requestId: number, loadedLogs: number, loadedPages: number) => {
|
|
307
|
+
const startedAt = sessionLoadStartedAtRef.current;
|
|
308
|
+
if (startedAt === null || sessionPageRequestIdRef.current !== requestId) return;
|
|
309
|
+
setSessionLoadProgress({
|
|
310
|
+
label,
|
|
311
|
+
elapsedMs: Date.now() - startedAt,
|
|
312
|
+
timeoutMs: SESSION_LOGS_FETCH_TIMEOUT_MS,
|
|
313
|
+
loadedLogs,
|
|
314
|
+
loadedPages,
|
|
315
|
+
});
|
|
316
|
+
},
|
|
317
|
+
[],
|
|
318
|
+
);
|
|
319
|
+
|
|
259
320
|
// Derived view: the logs the user actually sees. Recomputed only when the
|
|
260
321
|
// underlying set or filter changes — never on every SSE message.
|
|
261
322
|
const logs = useMemo(
|
|
@@ -369,6 +430,7 @@ export function ProxyViewerContainer({
|
|
|
369
430
|
const requestId = sessionPageRequestIdRef.current + 1;
|
|
370
431
|
sessionPageRequestIdRef.current = requestId;
|
|
371
432
|
setSessionPageLoading(true);
|
|
433
|
+
startSessionLoadProgress("Loading session logs", requestId);
|
|
372
434
|
dismissToast();
|
|
373
435
|
|
|
374
436
|
void fetchSessionLogsPage(initialSessionId, request)
|
|
@@ -380,6 +442,7 @@ export function ProxyViewerContainer({
|
|
|
380
442
|
setLogPage(page);
|
|
381
443
|
setSelectedSession(initialSessionId);
|
|
382
444
|
setSessionPageLoading(false);
|
|
445
|
+
stopSessionLoadProgress();
|
|
383
446
|
dismissToast();
|
|
384
447
|
})
|
|
385
448
|
.catch((err: unknown) => {
|
|
@@ -387,10 +450,17 @@ export function ProxyViewerContainer({
|
|
|
387
450
|
setAllLogs([]);
|
|
388
451
|
setLogPage(null);
|
|
389
452
|
setSessionPageLoading(false);
|
|
453
|
+
stopSessionLoadProgress();
|
|
390
454
|
showErrorToast(err instanceof Error ? err.message : "Failed to load session logs");
|
|
391
455
|
});
|
|
392
456
|
},
|
|
393
|
-
[
|
|
457
|
+
[
|
|
458
|
+
dismissToast,
|
|
459
|
+
initialSessionId,
|
|
460
|
+
showErrorToast,
|
|
461
|
+
startSessionLoadProgress,
|
|
462
|
+
stopSessionLoadProgress,
|
|
463
|
+
],
|
|
394
464
|
);
|
|
395
465
|
|
|
396
466
|
const reloadSessionLogs = useCallback(
|
|
@@ -398,6 +468,7 @@ export function ProxyViewerContainer({
|
|
|
398
468
|
const requestId = sessionPageRequestIdRef.current + 1;
|
|
399
469
|
sessionPageRequestIdRef.current = requestId;
|
|
400
470
|
setSessionPageLoading(true);
|
|
471
|
+
startSessionLoadProgress("Reloading full session", requestId);
|
|
401
472
|
dismissToast();
|
|
402
473
|
|
|
403
474
|
void (async () => {
|
|
@@ -408,6 +479,7 @@ export function ProxyViewerContainer({
|
|
|
408
479
|
SESSION_RELOAD_PAGE_LIMIT,
|
|
409
480
|
);
|
|
410
481
|
collected.push(...page.logs);
|
|
482
|
+
updateSessionLoadProgress("Reloading full session", requestId, collected.length, 1);
|
|
411
483
|
|
|
412
484
|
let loadedPages = 1;
|
|
413
485
|
while (
|
|
@@ -422,6 +494,12 @@ export function ProxyViewerContainer({
|
|
|
422
494
|
);
|
|
423
495
|
collected.push(...page.logs);
|
|
424
496
|
loadedPages += 1;
|
|
497
|
+
updateSessionLoadProgress(
|
|
498
|
+
"Reloading full session",
|
|
499
|
+
requestId,
|
|
500
|
+
collected.length,
|
|
501
|
+
loadedPages,
|
|
502
|
+
);
|
|
425
503
|
}
|
|
426
504
|
|
|
427
505
|
if (sessionPageRequestIdRef.current !== requestId) return;
|
|
@@ -432,6 +510,7 @@ export function ProxyViewerContainer({
|
|
|
432
510
|
});
|
|
433
511
|
setKnownSessions((prev) => mergeSessionIds(prev, [sessionId]));
|
|
434
512
|
setSessionPageLoading(false);
|
|
513
|
+
stopSessionLoadProgress();
|
|
435
514
|
if (page.hasNewer) {
|
|
436
515
|
showToast({
|
|
437
516
|
message: `Loaded ${String(collected.length)} logs for this session. More history is available.`,
|
|
@@ -443,10 +522,18 @@ export function ProxyViewerContainer({
|
|
|
443
522
|
})().catch((err: unknown) => {
|
|
444
523
|
if (sessionPageRequestIdRef.current !== requestId) return;
|
|
445
524
|
setSessionPageLoading(false);
|
|
525
|
+
stopSessionLoadProgress();
|
|
446
526
|
showErrorToast(err instanceof Error ? err.message : "Failed to reload session logs");
|
|
447
527
|
});
|
|
448
528
|
},
|
|
449
|
-
[
|
|
529
|
+
[
|
|
530
|
+
dismissToast,
|
|
531
|
+
showErrorToast,
|
|
532
|
+
showToast,
|
|
533
|
+
startSessionLoadProgress,
|
|
534
|
+
stopSessionLoadProgress,
|
|
535
|
+
updateSessionLoadProgress,
|
|
536
|
+
],
|
|
450
537
|
);
|
|
451
538
|
|
|
452
539
|
useEffect(() => {
|
|
@@ -479,6 +566,12 @@ export function ProxyViewerContainer({
|
|
|
479
566
|
refreshSessions();
|
|
480
567
|
}, [refreshSessions]);
|
|
481
568
|
|
|
569
|
+
useEffect(() => {
|
|
570
|
+
return () => {
|
|
571
|
+
stopSessionLoadProgress();
|
|
572
|
+
};
|
|
573
|
+
}, [stopSessionLoadProgress]);
|
|
574
|
+
|
|
482
575
|
useEffect(() => {
|
|
483
576
|
if (initialSessionId === undefined) return;
|
|
484
577
|
loadSessionPage({ kind: "newest" });
|
|
@@ -564,6 +657,8 @@ export function ProxyViewerContainer({
|
|
|
564
657
|
|
|
565
658
|
const handleClearAll = useCallback(() => {
|
|
566
659
|
const clearedSessions = extractSessions(allLogs);
|
|
660
|
+
stopSessionLoadProgress();
|
|
661
|
+
setSessionPageLoading(false);
|
|
567
662
|
logIndexRef.current.clear();
|
|
568
663
|
setAllLogs([]);
|
|
569
664
|
setLogPage(null);
|
|
@@ -572,7 +667,7 @@ export function ProxyViewerContainer({
|
|
|
572
667
|
message: "Cleared the current view. Stored logs are still reloadable.",
|
|
573
668
|
tone: "success",
|
|
574
669
|
});
|
|
575
|
-
}, [allLogs, showToast]);
|
|
670
|
+
}, [allLogs, showToast, stopSessionLoadProgress]);
|
|
576
671
|
|
|
577
672
|
const handleClearGroup = useCallback(
|
|
578
673
|
(ids: number[]) => {
|
|
@@ -590,6 +685,49 @@ export function ProxyViewerContainer({
|
|
|
590
685
|
[showToast],
|
|
591
686
|
);
|
|
592
687
|
|
|
688
|
+
const handleDeleteCurrentView = useCallback(
|
|
689
|
+
async (ids: number[]): Promise<void> => {
|
|
690
|
+
if (ids.length === 0) return;
|
|
691
|
+
const persistedIds = ids.filter((id) => Number.isInteger(id) && id >= 0);
|
|
692
|
+
if (persistedIds.length === 0) {
|
|
693
|
+
showToast({
|
|
694
|
+
message: "No persisted log records were found for this view.",
|
|
695
|
+
tone: "error",
|
|
696
|
+
});
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
const idSet = new Set(persistedIds);
|
|
700
|
+
const removed = allLogs.filter((log) => idSet.has(log.id));
|
|
701
|
+
const result = await fetchJson("/api/logs", DeleteLogsResponseSchema, {
|
|
702
|
+
method: "DELETE",
|
|
703
|
+
headers: { "content-type": "application/json" },
|
|
704
|
+
body: JSON.stringify({ ids: persistedIds }),
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
setAllLogs((prev) => {
|
|
708
|
+
const remaining = prev.filter((log) => !idSet.has(log.id));
|
|
709
|
+
logIndexRef.current = buildLogIndex(remaining);
|
|
710
|
+
return remaining;
|
|
711
|
+
});
|
|
712
|
+
setLogPage((prev) =>
|
|
713
|
+
prev === null
|
|
714
|
+
? null
|
|
715
|
+
: {
|
|
716
|
+
...prev,
|
|
717
|
+
logs: prev.logs.filter((log) => !idSet.has(log.id)),
|
|
718
|
+
},
|
|
719
|
+
);
|
|
720
|
+
setKnownSessions((prev) => mergeSessionIds(prev, extractSessions(removed)));
|
|
721
|
+
showToast({
|
|
722
|
+
message: `Deleted ${result.cleared.toString()} stored log${
|
|
723
|
+
result.cleared === 1 ? "" : "s"
|
|
724
|
+
} from the current view.`,
|
|
725
|
+
tone: "success",
|
|
726
|
+
});
|
|
727
|
+
},
|
|
728
|
+
[allLogs, showToast],
|
|
729
|
+
);
|
|
730
|
+
|
|
593
731
|
const handleReloadSession = useCallback(() => {
|
|
594
732
|
if (initialSessionId !== undefined) {
|
|
595
733
|
loadSessionPage({ kind: "newest" });
|
|
@@ -655,6 +793,7 @@ export function ProxyViewerContainer({
|
|
|
655
793
|
if (initialSessionId === undefined) return undefined;
|
|
656
794
|
return {
|
|
657
795
|
isLoading: sessionPageLoading,
|
|
796
|
+
loadProgress: sessionLoadProgress,
|
|
658
797
|
total: logPage?.total ?? null,
|
|
659
798
|
pageSize: logPage?.limit ?? SESSION_PAGE_LIMIT,
|
|
660
799
|
hasOlder: logPage?.hasOlder ?? false,
|
|
@@ -673,6 +812,7 @@ export function ProxyViewerContainer({
|
|
|
673
812
|
loadOlderPage,
|
|
674
813
|
loadOldestPage,
|
|
675
814
|
logPage,
|
|
815
|
+
sessionLoadProgress,
|
|
676
816
|
sessionPageLoading,
|
|
677
817
|
]);
|
|
678
818
|
|
|
@@ -689,6 +829,7 @@ export function ProxyViewerContainer({
|
|
|
689
829
|
onSessionChange={setSelectedSession}
|
|
690
830
|
onModelChange={setSelectedModel}
|
|
691
831
|
onClearAll={handleClearAll}
|
|
832
|
+
onDeleteCurrentView={handleDeleteCurrentView}
|
|
692
833
|
onReloadSession={
|
|
693
834
|
initialSessionId !== undefined || selectedSession !== "__all__"
|
|
694
835
|
? handleReloadSession
|
|
@@ -703,6 +844,7 @@ export function ProxyViewerContainer({
|
|
|
703
844
|
: sessionPageLoading
|
|
704
845
|
}
|
|
705
846
|
pagination={pagination}
|
|
847
|
+
sessionLoadProgress={sessionLoadProgress}
|
|
706
848
|
sessionMemberships={sessionMemberships}
|
|
707
849
|
viewMode={viewMode}
|
|
708
850
|
captureMode={captureMode}
|