@tonyclaw/agent-inspector 3.0.8 → 3.0.9

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.
Files changed (40) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-Bth_Ywyt.js → CompareDrawer-CJKL4LMm.js} +1 -1
  3. package/.output/public/assets/ProxyViewerContainer-DBUy29O-.js +106 -0
  4. package/.output/public/assets/{ReplayDialog-C2YYAuTe.js → ReplayDialog-BYanl-a_.js} +1 -1
  5. package/.output/public/assets/{RequestAnatomy-8k9dAX2j.js → RequestAnatomy-DvPz1jqE.js} +1 -1
  6. package/.output/public/assets/{ResponseView-DEL6JcXm.js → ResponseView-sZnxp9GN.js} +2 -2
  7. package/.output/public/assets/{StreamingChunkSequence-DngmoR9j.js → StreamingChunkSequence-G-tiYiBd.js} +1 -1
  8. package/.output/public/assets/{_sessionId-4vk9RMZ5.js → _sessionId-_hDcsmXq.js} +1 -1
  9. package/.output/public/assets/agent-inspector-58K1_MsC.ico +0 -0
  10. package/.output/public/assets/index-DAPAWsAd.js +1 -0
  11. package/.output/public/assets/index-DspnLqUW.css +1 -0
  12. package/.output/public/assets/{index-DINLvnm9.js → index-SuOSQb2_.js} +1 -1
  13. package/.output/public/assets/{json-viewer-CH5dvS2Y.js → json-viewer-yQdhgjaw.js} +1 -1
  14. package/.output/public/assets/{main-GWkZn4xy.js → main-CgjcDxjR.js} +2 -2
  15. package/.output/server/_libs/radix-ui__react-dialog.mjs +2 -2
  16. package/.output/server/{_sessionId-EgsjdkN7.mjs → _sessionId-BGISf2En.mjs} +2 -2
  17. package/.output/server/_ssr/{CompareDrawer-CaM8FYuD.mjs → CompareDrawer-s-UF9gW3.mjs} +2 -2
  18. package/.output/server/_ssr/{ProxyViewerContainer-EJIogcGd.mjs → ProxyViewerContainer-C0Sbz9ED.mjs} +496 -229
  19. package/.output/server/_ssr/{ReplayDialog-BPpJ2-q8.mjs → ReplayDialog-DvS8gvJr.mjs} +3 -3
  20. package/.output/server/_ssr/{RequestAnatomy-DeQAWnZx.mjs → RequestAnatomy-BwhWglJL.mjs} +2 -2
  21. package/.output/server/_ssr/{ResponseView-Dshm8iQ7.mjs → ResponseView-wBVreqQf.mjs} +2 -2
  22. package/.output/server/_ssr/{StreamingChunkSequence-32yIU2hn.mjs → StreamingChunkSequence-Dr6eg-MJ.mjs} +2 -2
  23. package/.output/server/_ssr/{index-XRcx9blU.mjs → index-B98Wia1X.mjs} +2 -2
  24. package/.output/server/_ssr/index.mjs +2 -2
  25. package/.output/server/_ssr/{json-viewer-C0aX5rqC.mjs → json-viewer-D7L8y0FH.mjs} +2 -2
  26. package/.output/server/_ssr/{router-BI-Mvxz9.mjs → router-g9mo0nWT.mjs} +22 -28
  27. package/.output/server/{_tanstack-start-manifest_v-jKScklL_.mjs → _tanstack-start-manifest_v-CbGc-o50.mjs} +1 -1
  28. package/.output/server/index.mjs +74 -67
  29. package/package.json +1 -1
  30. package/src/components/ProxyViewer.tsx +194 -29
  31. package/src/components/ProxyViewerContainer.tsx +137 -3
  32. package/src/components/clients/ClientLogo.tsx +16 -1
  33. package/src/components/proxy-viewer/TurnGroup.tsx +0 -16
  34. package/src/components/proxy-viewer/log-formats/anthropic.ts +5 -5
  35. package/src/components/proxy-viewer/log-formats/openai.ts +7 -10
  36. package/src/components/proxy-viewer/requestTools.ts +5 -2
  37. package/src/routes/api/providers.$providerId.test.log.ts +28 -24
  38. package/.output/public/assets/ProxyViewerContainer-C73EU9Jq.js +0 -106
  39. package/.output/public/assets/index-DBEB1rHe.css +0 -1
  40. 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-7 text-amber-500" />;
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-2">
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 && "col-span-2",
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 view</span>
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 view</span>
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
- [dismissToast, initialSessionId, showErrorToast],
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
- [dismissToast, showErrorToast, showToast],
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,41 @@ 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 idSet = new Set(ids);
692
+ const removed = allLogs.filter((log) => idSet.has(log.id));
693
+ const result = await fetchJson("/api/logs", DeleteLogsResponseSchema, {
694
+ method: "DELETE",
695
+ headers: { "content-type": "application/json" },
696
+ body: JSON.stringify({ ids }),
697
+ });
698
+
699
+ setAllLogs((prev) => {
700
+ const remaining = prev.filter((log) => !idSet.has(log.id));
701
+ logIndexRef.current = buildLogIndex(remaining);
702
+ return remaining;
703
+ });
704
+ setLogPage((prev) =>
705
+ prev === null
706
+ ? null
707
+ : {
708
+ ...prev,
709
+ logs: prev.logs.filter((log) => !idSet.has(log.id)),
710
+ },
711
+ );
712
+ setKnownSessions((prev) => mergeSessionIds(prev, extractSessions(removed)));
713
+ showToast({
714
+ message: `Deleted ${result.cleared.toString()} stored log${
715
+ result.cleared === 1 ? "" : "s"
716
+ } from the current view.`,
717
+ tone: "success",
718
+ });
719
+ },
720
+ [allLogs, showToast],
721
+ );
722
+
593
723
  const handleReloadSession = useCallback(() => {
594
724
  if (initialSessionId !== undefined) {
595
725
  loadSessionPage({ kind: "newest" });
@@ -655,6 +785,7 @@ export function ProxyViewerContainer({
655
785
  if (initialSessionId === undefined) return undefined;
656
786
  return {
657
787
  isLoading: sessionPageLoading,
788
+ loadProgress: sessionLoadProgress,
658
789
  total: logPage?.total ?? null,
659
790
  pageSize: logPage?.limit ?? SESSION_PAGE_LIMIT,
660
791
  hasOlder: logPage?.hasOlder ?? false,
@@ -673,6 +804,7 @@ export function ProxyViewerContainer({
673
804
  loadOlderPage,
674
805
  loadOldestPage,
675
806
  logPage,
807
+ sessionLoadProgress,
676
808
  sessionPageLoading,
677
809
  ]);
678
810
 
@@ -689,6 +821,7 @@ export function ProxyViewerContainer({
689
821
  onSessionChange={setSelectedSession}
690
822
  onModelChange={setSelectedModel}
691
823
  onClearAll={handleClearAll}
824
+ onDeleteCurrentView={handleDeleteCurrentView}
692
825
  onReloadSession={
693
826
  initialSessionId !== undefined || selectedSession !== "__all__"
694
827
  ? handleReloadSession
@@ -703,6 +836,7 @@ export function ProxyViewerContainer({
703
836
  : sessionPageLoading
704
837
  }
705
838
  pagination={pagination}
839
+ sessionLoadProgress={sessionLoadProgress}
706
840
  sessionMemberships={sessionMemberships}
707
841
  viewMode={viewMode}
708
842
  captureMode={captureMode}