@tonyclaw/agent-inspector 3.0.14 → 3.0.16

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 (42) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-DDfwHQUi.js → CompareDrawer-BJmxAiM0.js} +1 -1
  3. package/.output/public/assets/ProxyViewerContainer-C40Nig73.js +106 -0
  4. package/.output/public/assets/{ReplayDialog-D-26DMBt.js → ReplayDialog-CkLdOMJr.js} +1 -1
  5. package/.output/public/assets/{RequestAnatomy-B6zNFqNX.js → RequestAnatomy-DemOFfSc.js} +1 -1
  6. package/.output/public/assets/{ResponseView-Ccy95rWI.js → ResponseView-9LhpuDo7.js} +2 -2
  7. package/.output/public/assets/{StreamingChunkSequence-yIiscGuE.js → StreamingChunkSequence-RRWYBOdz.js} +1 -1
  8. package/.output/public/assets/{_sessionId-BnMB0zab.js → _sessionId-CWEh2Jsn.js} +1 -1
  9. package/.output/public/assets/index-HRfrFVYw.css +1 -0
  10. package/.output/public/assets/{index-BbtAIxbg.js → index-Lwir70Rs.js} +1 -1
  11. package/.output/public/assets/index-hjNWJZGE.js +1 -0
  12. package/.output/public/assets/{json-viewer-C838cVmD.js → json-viewer-D8gvbDlB.js} +1 -1
  13. package/.output/public/assets/{main-BfN4culo.js → main-ovrz0tyD.js} +7 -7
  14. package/.output/server/_libs/lucide-react.mjs +215 -201
  15. package/.output/server/{_sessionId-DoprmwZV.mjs → _sessionId-zI_GFX_7.mjs} +2 -2
  16. package/.output/server/_ssr/{CompareDrawer-YMLJNxXR.mjs → CompareDrawer-Ckcx1RVu.mjs} +3 -3
  17. package/.output/server/_ssr/{ProxyViewerContainer-C62eZnva.mjs → ProxyViewerContainer-BgXewlIr.mjs} +729 -378
  18. package/.output/server/_ssr/{ReplayDialog-NmN7Ukjo.mjs → ReplayDialog-wx3nDtZn.mjs} +4 -4
  19. package/.output/server/_ssr/{RequestAnatomy-Dawm2qxe.mjs → RequestAnatomy-DN80zt0G.mjs} +3 -3
  20. package/.output/server/_ssr/{ResponseView-BHyAY83t.mjs → ResponseView-DD7rTu_l.mjs} +3 -3
  21. package/.output/server/_ssr/{StreamingChunkSequence-C4qgcJbg.mjs → StreamingChunkSequence-DAgz6Vvw.mjs} +3 -3
  22. package/.output/server/_ssr/{index-B2Pf6W_T.mjs → index-dcTjp8du.mjs} +14 -5
  23. package/.output/server/_ssr/index.mjs +2 -2
  24. package/.output/server/_ssr/{json-viewer-u67BASf8.mjs → json-viewer-N2cuzGWR.mjs} +3 -3
  25. package/.output/server/_ssr/{router-CZhFVMGN.mjs → router-BfQ0QsD5.mjs} +132 -17
  26. package/.output/server/{_tanstack-start-manifest_v-Dk2HfOoe.mjs → _tanstack-start-manifest_v-BXnYrbcJ.mjs} +1 -1
  27. package/.output/server/index.mjs +75 -75
  28. package/package.json +1 -1
  29. package/src/components/ProxyViewer.tsx +475 -197
  30. package/src/components/ProxyViewerContainer.tsx +145 -17
  31. package/src/components/proxy-viewer/ToolTraceEvents.tsx +36 -10
  32. package/src/components/proxy-viewer/TurnGroup.tsx +4 -1
  33. package/src/lib/chunkLoadRecovery.ts +83 -0
  34. package/src/proxy/formats/openai/codexMetadata.ts +61 -0
  35. package/src/proxy/formats/openai/handler.ts +8 -2
  36. package/src/routes/__root.tsx +6 -3
  37. package/src/routes/index.tsx +25 -1
  38. package/src/routes/session/$sessionId.tsx +1 -0
  39. package/styles/globals.css +54 -0
  40. package/.output/public/assets/ProxyViewerContainer-CbNsaoQw.js +0 -106
  41. package/.output/public/assets/index-49RC4dlD.js +0 -1
  42. package/.output/public/assets/index-DXKupW0r.css +0 -1
@@ -1,6 +1,8 @@
1
1
  import {
2
2
  type ChangeEvent,
3
+ type CSSProperties,
3
4
  type JSX,
5
+ type PointerEvent as ReactPointerEvent,
4
6
  type ReactNode,
5
7
  useCallback,
6
8
  useEffect,
@@ -26,6 +28,8 @@ import {
26
28
  FileJson,
27
29
  Gauge,
28
30
  Flag,
31
+ PanelRightClose,
32
+ PanelRightOpen,
29
33
  Plus,
30
34
  RefreshCw,
31
35
  Search,
@@ -1381,6 +1385,291 @@ function BrandHeader({
1381
1385
  );
1382
1386
  }
1383
1387
 
1388
+ type FloatingLogoPosition = {
1389
+ x: number;
1390
+ y: number;
1391
+ };
1392
+
1393
+ type FloatingLogoDragState = {
1394
+ pointerId: number;
1395
+ offsetX: number;
1396
+ offsetY: number;
1397
+ };
1398
+
1399
+ type FloatingLogoEdge = "left" | "right" | "top" | "bottom";
1400
+
1401
+ const FLOATING_LOGO_SIZE = 56;
1402
+ const FLOATING_LOGO_MARGIN = 14;
1403
+ const FLOATING_LOGO_MIN_DRIFT_MS = 5200;
1404
+ const FLOATING_LOGO_DRIFT_WINDOW_MS = 2400;
1405
+ const FLOATING_LOGO_MIN_STEP = 72;
1406
+ const FLOATING_LOGO_MAX_STEP = 156;
1407
+ const DETAILS_PANEL_COLLAPSED_STORAGE_KEY = "agent-inspector.detailsPanelCollapsed";
1408
+
1409
+ function readStoredDetailsPanelCollapsed(): boolean {
1410
+ if (typeof window === "undefined") return true;
1411
+ const stored = window.localStorage.getItem(DETAILS_PANEL_COLLAPSED_STORAGE_KEY);
1412
+ return stored === null || stored === "1";
1413
+ }
1414
+
1415
+ function clampFloatingLogoPosition(position: FloatingLogoPosition): FloatingLogoPosition {
1416
+ const maxX = Math.max(
1417
+ FLOATING_LOGO_MARGIN,
1418
+ window.innerWidth - FLOATING_LOGO_SIZE - FLOATING_LOGO_MARGIN,
1419
+ );
1420
+ const maxY = Math.max(
1421
+ FLOATING_LOGO_MARGIN,
1422
+ window.innerHeight - FLOATING_LOGO_SIZE - FLOATING_LOGO_MARGIN,
1423
+ );
1424
+ return {
1425
+ x: Math.min(Math.max(position.x, FLOATING_LOGO_MARGIN), maxX),
1426
+ y: Math.min(Math.max(position.y, FLOATING_LOGO_MARGIN), maxY),
1427
+ };
1428
+ }
1429
+
1430
+ function snapFloatingLogoToEdge(position: FloatingLogoPosition): FloatingLogoPosition {
1431
+ const clamped = clampFloatingLogoPosition(position);
1432
+ const maxX = Math.max(
1433
+ FLOATING_LOGO_MARGIN,
1434
+ window.innerWidth - FLOATING_LOGO_SIZE - FLOATING_LOGO_MARGIN,
1435
+ );
1436
+ const maxY = Math.max(
1437
+ FLOATING_LOGO_MARGIN,
1438
+ window.innerHeight - FLOATING_LOGO_SIZE - FLOATING_LOGO_MARGIN,
1439
+ );
1440
+ const leftDistance = clamped.x - FLOATING_LOGO_MARGIN;
1441
+ const rightDistance = maxX - clamped.x;
1442
+ const topDistance = clamped.y - FLOATING_LOGO_MARGIN;
1443
+ const bottomDistance = maxY - clamped.y;
1444
+ const nearestDistance = Math.min(leftDistance, rightDistance, topDistance, bottomDistance);
1445
+
1446
+ if (nearestDistance === leftDistance) return { x: FLOATING_LOGO_MARGIN, y: clamped.y };
1447
+ if (nearestDistance === rightDistance) return { x: maxX, y: clamped.y };
1448
+ if (nearestDistance === topDistance) return { x: clamped.x, y: FLOATING_LOGO_MARGIN };
1449
+ return { x: clamped.x, y: maxY };
1450
+ }
1451
+
1452
+ function randomFloatingLogoEdgePosition(): FloatingLogoPosition {
1453
+ const maxX = Math.max(
1454
+ FLOATING_LOGO_MARGIN,
1455
+ window.innerWidth - FLOATING_LOGO_SIZE - FLOATING_LOGO_MARGIN,
1456
+ );
1457
+ const maxY = Math.max(
1458
+ FLOATING_LOGO_MARGIN,
1459
+ window.innerHeight - FLOATING_LOGO_SIZE - FLOATING_LOGO_MARGIN,
1460
+ );
1461
+ const randomX = FLOATING_LOGO_MARGIN + Math.random() * (maxX - FLOATING_LOGO_MARGIN);
1462
+ const randomY = FLOATING_LOGO_MARGIN + Math.random() * (maxY - FLOATING_LOGO_MARGIN);
1463
+ const edgeIndex = Math.floor(Math.random() * 4);
1464
+
1465
+ switch (edgeIndex) {
1466
+ case 0:
1467
+ return { x: FLOATING_LOGO_MARGIN, y: randomY };
1468
+ case 1:
1469
+ return { x: maxX, y: randomY };
1470
+ case 2:
1471
+ return { x: randomX, y: FLOATING_LOGO_MARGIN };
1472
+ case 3:
1473
+ return { x: randomX, y: maxY };
1474
+ default:
1475
+ return { x: maxX, y: FLOATING_LOGO_MARGIN };
1476
+ }
1477
+ }
1478
+
1479
+ function nearestFloatingLogoEdge(position: FloatingLogoPosition): FloatingLogoEdge {
1480
+ const clamped = clampFloatingLogoPosition(position);
1481
+ const maxX = Math.max(
1482
+ FLOATING_LOGO_MARGIN,
1483
+ window.innerWidth - FLOATING_LOGO_SIZE - FLOATING_LOGO_MARGIN,
1484
+ );
1485
+ const maxY = Math.max(
1486
+ FLOATING_LOGO_MARGIN,
1487
+ window.innerHeight - FLOATING_LOGO_SIZE - FLOATING_LOGO_MARGIN,
1488
+ );
1489
+ const distances: { edge: FloatingLogoEdge; distance: number }[] = [
1490
+ { edge: "left", distance: clamped.x - FLOATING_LOGO_MARGIN },
1491
+ { edge: "right", distance: maxX - clamped.x },
1492
+ { edge: "top", distance: clamped.y - FLOATING_LOGO_MARGIN },
1493
+ { edge: "bottom", distance: maxY - clamped.y },
1494
+ ];
1495
+ return distances.reduce((best, current) => (current.distance < best.distance ? current : best))
1496
+ .edge;
1497
+ }
1498
+
1499
+ function nextFloatingLogoWalkingPosition(position: FloatingLogoPosition): FloatingLogoPosition {
1500
+ const clamped = snapFloatingLogoToEdge(position);
1501
+ const maxX = Math.max(
1502
+ FLOATING_LOGO_MARGIN,
1503
+ window.innerWidth - FLOATING_LOGO_SIZE - FLOATING_LOGO_MARGIN,
1504
+ );
1505
+ const maxY = Math.max(
1506
+ FLOATING_LOGO_MARGIN,
1507
+ window.innerHeight - FLOATING_LOGO_SIZE - FLOATING_LOGO_MARGIN,
1508
+ );
1509
+ const step =
1510
+ FLOATING_LOGO_MIN_STEP + Math.random() * (FLOATING_LOGO_MAX_STEP - FLOATING_LOGO_MIN_STEP);
1511
+ const direction = Math.random() > 0.5 ? 1 : -1;
1512
+ const edge = nearestFloatingLogoEdge(clamped);
1513
+
1514
+ switch (edge) {
1515
+ case "left": {
1516
+ const nextY = clamped.y + step * direction;
1517
+ if (nextY <= FLOATING_LOGO_MARGIN)
1518
+ return { x: FLOATING_LOGO_MARGIN, y: FLOATING_LOGO_MARGIN };
1519
+ if (nextY >= maxY) return { x: FLOATING_LOGO_MARGIN, y: maxY };
1520
+ return { x: FLOATING_LOGO_MARGIN, y: nextY };
1521
+ }
1522
+ case "right": {
1523
+ const nextY = clamped.y + step * direction;
1524
+ if (nextY <= FLOATING_LOGO_MARGIN) return { x: maxX, y: FLOATING_LOGO_MARGIN };
1525
+ if (nextY >= maxY) return { x: maxX, y: maxY };
1526
+ return { x: maxX, y: nextY };
1527
+ }
1528
+ case "top": {
1529
+ const nextX = clamped.x + step * direction;
1530
+ if (nextX <= FLOATING_LOGO_MARGIN)
1531
+ return { x: FLOATING_LOGO_MARGIN, y: FLOATING_LOGO_MARGIN };
1532
+ if (nextX >= maxX) return { x: maxX, y: FLOATING_LOGO_MARGIN };
1533
+ return { x: nextX, y: FLOATING_LOGO_MARGIN };
1534
+ }
1535
+ case "bottom": {
1536
+ const nextX = clamped.x + step * direction;
1537
+ if (nextX <= FLOATING_LOGO_MARGIN) return { x: FLOATING_LOGO_MARGIN, y: maxY };
1538
+ if (nextX >= maxX) return { x: maxX, y: maxY };
1539
+ return { x: nextX, y: maxY };
1540
+ }
1541
+ }
1542
+ }
1543
+
1544
+ function FloatingInspectorLogo(): JSX.Element {
1545
+ const [position, setPosition] = useState<FloatingLogoPosition>({
1546
+ x: FLOATING_LOGO_MARGIN,
1547
+ y: FLOATING_LOGO_MARGIN,
1548
+ });
1549
+ const [motionTick, setMotionTick] = useState(0);
1550
+ const [dragging, setDragging] = useState(false);
1551
+ const dragStateRef = useRef<FloatingLogoDragState | null>(null);
1552
+
1553
+ useEffect(() => {
1554
+ setPosition(randomFloatingLogoEdgePosition());
1555
+ }, []);
1556
+
1557
+ useEffect(() => {
1558
+ if (dragging) return undefined;
1559
+ let timeoutId: number | null = null;
1560
+
1561
+ const scheduleDrift = () => {
1562
+ const delay = FLOATING_LOGO_MIN_DRIFT_MS + Math.random() * FLOATING_LOGO_DRIFT_WINDOW_MS;
1563
+ timeoutId = window.setTimeout(() => {
1564
+ setMotionTick((current) => current + 1);
1565
+ setPosition((current) => nextFloatingLogoWalkingPosition(current));
1566
+ scheduleDrift();
1567
+ }, delay);
1568
+ };
1569
+
1570
+ scheduleDrift();
1571
+ return () => {
1572
+ if (timeoutId !== null) {
1573
+ window.clearTimeout(timeoutId);
1574
+ }
1575
+ };
1576
+ }, [dragging]);
1577
+
1578
+ useEffect(() => {
1579
+ const handleResize = () => {
1580
+ setPosition((current) => snapFloatingLogoToEdge(current));
1581
+ };
1582
+ window.addEventListener("resize", handleResize);
1583
+ return () => window.removeEventListener("resize", handleResize);
1584
+ }, []);
1585
+
1586
+ const handlePointerDown = useCallback((event: ReactPointerEvent<HTMLButtonElement>) => {
1587
+ const rect = event.currentTarget.getBoundingClientRect();
1588
+ dragStateRef.current = {
1589
+ pointerId: event.pointerId,
1590
+ offsetX: event.clientX - rect.left,
1591
+ offsetY: event.clientY - rect.top,
1592
+ };
1593
+ event.currentTarget.setPointerCapture(event.pointerId);
1594
+ setDragging(true);
1595
+ }, []);
1596
+
1597
+ const handlePointerMove = useCallback((event: ReactPointerEvent<HTMLButtonElement>) => {
1598
+ const dragState = dragStateRef.current;
1599
+ if (dragState === null || dragState.pointerId !== event.pointerId) return;
1600
+ setPosition(
1601
+ clampFloatingLogoPosition({
1602
+ x: event.clientX - dragState.offsetX,
1603
+ y: event.clientY - dragState.offsetY,
1604
+ }),
1605
+ );
1606
+ }, []);
1607
+
1608
+ const handlePointerUp = useCallback((event: ReactPointerEvent<HTMLButtonElement>) => {
1609
+ const dragState = dragStateRef.current;
1610
+ if (dragState === null || dragState.pointerId !== event.pointerId) return;
1611
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) {
1612
+ event.currentTarget.releasePointerCapture(event.pointerId);
1613
+ }
1614
+ dragStateRef.current = null;
1615
+ setDragging(false);
1616
+ setPosition((current) => snapFloatingLogoToEdge(current));
1617
+ }, []);
1618
+
1619
+ const style = useMemo<CSSProperties>(
1620
+ () => ({
1621
+ transform: `translate3d(${position.x}px, ${position.y}px, 0) rotate(${
1622
+ motionTick % 2 === 0 ? "-4deg" : "5deg"
1623
+ }) scale(${dragging ? "1.08" : "1"})`,
1624
+ transition: dragging ? "none" : "transform 4800ms cubic-bezier(0.33, 1, 0.68, 1)",
1625
+ touchAction: "none",
1626
+ }),
1627
+ [dragging, motionTick, position.x, position.y],
1628
+ );
1629
+
1630
+ return (
1631
+ <button
1632
+ type="button"
1633
+ className={cn(
1634
+ "fixed left-0 top-0 z-40 flex size-14 items-center justify-center rounded-full bg-transparent text-foreground outline-none transition-[filter,opacity] duration-200 hover:opacity-95 hover:drop-shadow-[0_0_24px_rgba(245,158,11,0.36)] focus-visible:ring-1 focus-visible:ring-amber-300/55",
1635
+ dragging ? "cursor-grabbing" : "cursor-grab",
1636
+ )}
1637
+ style={style}
1638
+ onPointerDown={handlePointerDown}
1639
+ onPointerMove={handlePointerMove}
1640
+ onPointerUp={handlePointerUp}
1641
+ onPointerCancel={handlePointerUp}
1642
+ aria-label="Floating Inspector logo"
1643
+ title="Drag Inspector logo"
1644
+ >
1645
+ <span
1646
+ className={cn(
1647
+ "relative flex size-12 items-center justify-center",
1648
+ !dragging && "floating-crab-body",
1649
+ )}
1650
+ aria-hidden="true"
1651
+ >
1652
+ <CrabLogo className="relative z-10 size-11 text-amber-500 drop-shadow-[0_0_15px_rgba(245,158,11,0.30)]" />
1653
+ <span className="pointer-events-none absolute inset-x-0 bottom-0 z-0 flex items-end justify-center gap-1 text-amber-400/80">
1654
+ {[0, 1, 2, 3, 4, 5].map((leg) => (
1655
+ <span
1656
+ key={leg}
1657
+ className={cn(
1658
+ "floating-crab-leg h-2 w-0.5 rounded-full bg-current shadow-[0_0_7px_rgba(245,158,11,0.24)]",
1659
+ leg < 3 ? "floating-crab-leg-left" : "floating-crab-leg-right",
1660
+ dragging && "animate-none",
1661
+ )}
1662
+ style={{
1663
+ animationDelay: `${leg % 2 === 0 ? 0 : 180}ms`,
1664
+ }}
1665
+ />
1666
+ ))}
1667
+ </span>
1668
+ </span>
1669
+ </button>
1670
+ );
1671
+ }
1672
+
1384
1673
  function SessionContextLogButton({ label, logId }: { label: string; logId: number }): JSX.Element {
1385
1674
  return (
1386
1675
  <button
@@ -1697,8 +1986,8 @@ export type ProxyViewerProps = {
1697
1986
  onSessionChange: (session: string) => void;
1698
1987
  onModelChange: (model: string) => void;
1699
1988
  onClearAll: () => void;
1700
- /** Delete visible logs from persisted storage when the user explicitly confirms it. */
1701
- onDeleteCurrentView?: (ids: number[]) => Promise<void>;
1989
+ /** Delete persisted storage for the selected session, or all storage from the all-sessions view. */
1990
+ onDeleteCurrentView?: () => Promise<void>;
1702
1991
  /** Reload the selected/pinned session from stored logs without requiring a page refresh. */
1703
1992
  onReloadSession?: () => void;
1704
1993
  /** Clear only the logs whose ids are passed. Called by the per-group
@@ -1765,8 +2054,9 @@ export function ProxyViewer({
1765
2054
  const [importStatus, setImportStatus] = useState<string | null>(null);
1766
2055
  const [importError, setImportError] = useState<string | null>(null);
1767
2056
  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
1768
- const [deleteStoredData, setDeleteStoredData] = useState(false);
1769
2057
  const [deletingCurrentView, setDeletingCurrentView] = useState(false);
2058
+ const [detailsPanelCollapsed, setDetailsPanelCollapsed] = useState(true);
2059
+ const [detailsPanelStorageLoaded, setDetailsPanelStorageLoaded] = useState(false);
1770
2060
  const [comparePair, setComparePair] = useState<[CapturedLog, CapturedLog] | null>(null);
1771
2061
  const [crabEntrancePhase, setCrabEntrancePhase] = useState<"hidden" | "playing" | "done">(
1772
2062
  "hidden",
@@ -1807,6 +2097,19 @@ export function ProxyViewer({
1807
2097
  };
1808
2098
  }, []);
1809
2099
 
2100
+ useEffect(() => {
2101
+ setDetailsPanelCollapsed(readStoredDetailsPanelCollapsed());
2102
+ setDetailsPanelStorageLoaded(true);
2103
+ }, []);
2104
+
2105
+ useEffect(() => {
2106
+ if (!detailsPanelStorageLoaded) return;
2107
+ window.localStorage.setItem(
2108
+ DETAILS_PANEL_COLLAPSED_STORAGE_KEY,
2109
+ detailsPanelCollapsed ? "1" : "0",
2110
+ );
2111
+ }, [detailsPanelCollapsed, detailsPanelStorageLoaded]);
2112
+
1810
2113
  useEffect(() => {
1811
2114
  if (pinnedSessionId === undefined) {
1812
2115
  document.title = "Agent Inspector";
@@ -1898,7 +2201,6 @@ export function ProxyViewer({
1898
2201
  }, [cancelTransientTasks, onClearAll]);
1899
2202
 
1900
2203
  const handleRequestDeleteCurrentView = useCallback(() => {
1901
- setDeleteStoredData(false);
1902
2204
  setDeleteDialogOpen(true);
1903
2205
  }, []);
1904
2206
 
@@ -1908,14 +2210,10 @@ export function ProxyViewer({
1908
2210
  setImportError(null);
1909
2211
  setComparePair(null);
1910
2212
 
1911
- if (!deleteStoredData || onDeleteCurrentView === undefined) {
1912
- setDeleteDialogOpen(false);
1913
- onClearAll();
1914
- return;
1915
- }
2213
+ if (onDeleteCurrentView === undefined) return;
1916
2214
 
1917
2215
  setDeletingCurrentView(true);
1918
- void onDeleteCurrentView(logs.map((log) => log.id))
2216
+ void onDeleteCurrentView()
1919
2217
  .then(() => {
1920
2218
  setDeleteDialogOpen(false);
1921
2219
  })
@@ -1930,7 +2228,7 @@ export function ProxyViewer({
1930
2228
  .finally(() => {
1931
2229
  setDeletingCurrentView(false);
1932
2230
  });
1933
- }, [cancelTransientTasks, deleteStoredData, logs, onClearAll, onDeleteCurrentView, onNotify]);
2231
+ }, [cancelTransientTasks, onDeleteCurrentView, onNotify]);
1934
2232
 
1935
2233
  const handleReloadCurrentSession = useCallback(() => {
1936
2234
  if (onReloadSession === undefined) return;
@@ -1945,6 +2243,10 @@ export function ProxyViewer({
1945
2243
  setComparePair(null);
1946
2244
  }, []);
1947
2245
 
2246
+ const toggleDetailsPanelCollapsed = useCallback(() => {
2247
+ setDetailsPanelCollapsed((value) => !value);
2248
+ }, []);
2249
+
1948
2250
  const groups = useMemo(() => groupLogsByConversation(logs), [logs]);
1949
2251
  const displayNumberByLogId = useMemo(() => buildDisplayNumberByLogId(groups), [groups]);
1950
2252
  const sessionContextScope = useMemo(
@@ -1966,6 +2268,13 @@ export function ProxyViewer({
1966
2268
  const exportActionVisibility = getExportActionVisibility(captureMode, logs.length);
1967
2269
  const isPinnedSessionPage = pinnedSessionId !== undefined;
1968
2270
  const canClearCurrentView = logs.length > 0;
2271
+ const canDeleteCurrentView =
2272
+ onDeleteCurrentView !== undefined && (logs.length > 0 || selectedSession !== "__all__");
2273
+ const deleteTargetLabel = selectedSession === "__all__" ? "all sessions" : "this session";
2274
+ const deleteTargetDescription =
2275
+ selectedSession === "__all__"
2276
+ ? "Deletes every stored log, streaming chunk, and session archive entry across all sessions."
2277
+ : "Deletes every stored log, streaming chunk, and session archive entry for this session, including history that is not currently loaded.";
1969
2278
  const reloadSessionLabel = logs.length === 0 ? "Load" : "Reload";
1970
2279
  const sessionInputValue = selectedSession === "__all__" ? "" : selectedSession;
1971
2280
  const sessionSelectValue =
@@ -2019,173 +2328,159 @@ export function ProxyViewer({
2019
2328
 
2020
2329
  return (
2021
2330
  <div className="bg-background min-h-screen w-full text-foreground">
2022
- <div className="grid min-h-screen w-full grid-cols-1 lg:grid-cols-[minmax(0,1fr)_340px] xl:grid-cols-[minmax(0,1fr)_360px]">
2023
- <aside className="bg-background/95 order-1 border-b border-white/[0.045] lg:sticky lg:top-0 lg:order-2 lg:h-screen lg:overflow-y-auto lg:border-b-0 lg:border-l lg:border-white/[0.055]">
2024
- <div className="space-y-4 p-4">
2025
- <BrandHeader
2026
- compact={true}
2027
- crabEntrancePhase={crabEntrancePhase}
2028
- currentSessionId={labSessionId}
2029
- logCount={logs.length}
2030
- />
2331
+ <button
2332
+ type="button"
2333
+ onClick={toggleDetailsPanelCollapsed}
2334
+ className={cn(
2335
+ "group fixed top-1/2 right-3 z-40 inline-flex h-14 w-7 -translate-y-1/2 items-center justify-center rounded-l-md border border-white/[0.08] bg-background/90 text-muted-foreground shadow-[0_12px_34px_rgba(0,0,0,0.26),inset_0_1px_0_rgba(255,255,255,0.06)] backdrop-blur transition-[right,background-color,color,border-color] duration-200 hover:border-white/[0.16] hover:bg-white/[0.07] hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none",
2336
+ detailsPanelCollapsed ? "right-3" : "lg:right-[326px] xl:right-[346px]",
2337
+ )}
2338
+ aria-label={detailsPanelCollapsed ? "Show details panel" : "Hide details panel"}
2339
+ title={detailsPanelCollapsed ? "Show details panel" : "Hide details panel"}
2340
+ >
2341
+ <span className="absolute left-1 top-2 bottom-2 w-px rounded-full bg-white/[0.16] transition-colors group-hover:bg-white/[0.28]" />
2342
+ {detailsPanelCollapsed ? (
2343
+ <PanelRightOpen className="size-3.5" />
2344
+ ) : (
2345
+ <PanelRightClose className="size-3.5" />
2346
+ )}
2347
+ </button>
2348
+ {detailsPanelCollapsed && <FloatingInspectorLogo />}
2349
+ <div
2350
+ className={cn(
2351
+ "grid min-h-screen w-full grid-cols-1",
2352
+ detailsPanelCollapsed
2353
+ ? "lg:grid-cols-[minmax(0,1fr)]"
2354
+ : "lg:grid-cols-[minmax(0,1fr)_340px] xl:grid-cols-[minmax(0,1fr)_360px]",
2355
+ )}
2356
+ >
2357
+ {!detailsPanelCollapsed && (
2358
+ <aside className="bg-background/95 order-1 border-b border-white/[0.045] lg:sticky lg:top-0 lg:order-2 lg:h-screen lg:overflow-y-auto lg:border-b-0 lg:border-l lg:border-white/[0.055]">
2359
+ <div className="space-y-4 p-4">
2360
+ <BrandHeader
2361
+ compact={true}
2362
+ crabEntrancePhase={crabEntrancePhase}
2363
+ currentSessionId={labSessionId}
2364
+ logCount={logs.length}
2365
+ />
2031
2366
 
2032
- <SidebarPanel label="Session">
2033
- <div className="grid grid-cols-2 gap-2">
2034
- <SidebarMetric label="Requests" value={logs.length} />
2035
- <SidebarMetric label="Sessions" value={sessions.length} />
2036
- <SidebarMetric
2037
- label="Input"
2038
- value={formatTokens(hasSessionContext ? sessionContextTotalIn : totalIn)}
2039
- />
2040
- <SidebarMetric
2041
- label="Output"
2042
- value={formatTokens(hasSessionContext ? sessionContextTotalOut : totalOut)}
2043
- />
2044
- </div>
2045
- <div className="mt-2 grid grid-cols-2 gap-2">
2046
- {onReloadSession !== undefined && (
2047
- <button
2048
- type="button"
2049
- onClick={handleReloadCurrentSession}
2050
- disabled={isLoading}
2051
- className="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"
2052
- title="Load stored logs for this session"
2053
- >
2054
- <RefreshCw className={cn("size-3.5", isLoading && "animate-spin")} />
2055
- <span>{reloadSessionLabel}</span>
2056
- </button>
2367
+ <SidebarPanel label="Session">
2368
+ <div className="grid grid-cols-2 gap-2">
2369
+ <SidebarMetric label="Requests" value={logs.length} />
2370
+ <SidebarMetric label="Sessions" value={sessions.length} />
2371
+ <SidebarMetric
2372
+ label="Input"
2373
+ value={formatTokens(hasSessionContext ? sessionContextTotalIn : totalIn)}
2374
+ />
2375
+ <SidebarMetric
2376
+ label="Output"
2377
+ value={formatTokens(hasSessionContext ? sessionContextTotalOut : totalOut)}
2378
+ />
2379
+ </div>
2380
+ {sessionLoadProgress !== undefined && (
2381
+ <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)]">
2382
+ <SessionLoadProgressBar progress={sessionLoadProgress} />
2383
+ </div>
2057
2384
  )}
2058
- <button
2059
- type="button"
2060
- onClick={handleClearCurrentView}
2061
- disabled={!canClearCurrentView}
2062
- className={cn(
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",
2064
- onReloadSession === undefined &&
2065
- onDeleteCurrentView === undefined &&
2066
- "col-span-2",
2067
- )}
2068
- title="Clear the current view. Stored logs remain reloadable."
2069
- >
2070
- <Trash2 className="size-3.5" />
2071
- <span>Clear</span>
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."
2385
+ </SidebarPanel>
2386
+
2387
+ <SidebarPanel label="Workspace">
2388
+ <div className="bg-black/20 rounded-md px-3 py-2 shadow-[inset_0_1px_0_rgba(255,255,255,0.035)]">
2389
+ <div
2390
+ className="truncate font-mono text-xs font-semibold text-cyan-100"
2391
+ title={activeScopeLabel}
2080
2392
  >
2081
- <Trash2 className="size-3.5" />
2082
- <span>Delete</span>
2083
- </button>
2084
- )}
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} />
2393
+ {activeScopeLabel}
2394
+ </div>
2395
+ <div className="mt-1 text-[11px] text-muted-foreground">
2396
+ {isPinnedSessionPage ? "Session workspace" : "Live workspace"}
2397
+ </div>
2089
2398
  </div>
2399
+ </SidebarPanel>
2400
+
2401
+ {hasSessionContext && sessionContextScope !== null && (
2402
+ <SessionContextBar
2403
+ label={sessionContextScope.label}
2404
+ logs={sessionContextLogs}
2405
+ totalIn={sessionContextTotalIn}
2406
+ totalOut={sessionContextTotalOut}
2407
+ timeDisplayFormat={timeDisplayFormat}
2408
+ contextSummary={sessionContextSummary}
2409
+ showBackLink={sessionContextScope.showBackLink}
2410
+ />
2090
2411
  )}
2091
- </SidebarPanel>
2092
-
2093
- <SidebarPanel label="Workspace">
2094
- <div className="bg-black/20 rounded-md px-3 py-2 shadow-[inset_0_1px_0_rgba(255,255,255,0.035)]">
2095
- <div
2096
- className="truncate font-mono text-xs font-semibold text-cyan-100"
2097
- title={activeScopeLabel}
2098
- >
2099
- {activeScopeLabel}
2100
- </div>
2101
- <div className="mt-1 text-[11px] text-muted-foreground">
2102
- {isPinnedSessionPage ? "Session workspace" : "Live workspace"}
2103
- </div>
2104
- </div>
2105
- </SidebarPanel>
2106
-
2107
- {hasSessionContext && sessionContextScope !== null && (
2108
- <SessionContextBar
2109
- label={sessionContextScope.label}
2110
- logs={sessionContextLogs}
2111
- totalIn={sessionContextTotalIn}
2112
- totalOut={sessionContextTotalOut}
2113
- timeDisplayFormat={timeDisplayFormat}
2114
- contextSummary={sessionContextSummary}
2115
- showBackLink={sessionContextScope.showBackLink}
2116
- />
2117
- )}
2118
2412
 
2119
- {hasSessionContext &&
2120
- pinnedSessionId !== undefined &&
2121
- sessionMemberships.length > 0 && (
2122
- <SessionMembershipPanel memberships={sessionMemberships} />
2123
- )}
2413
+ {hasSessionContext &&
2414
+ pinnedSessionId !== undefined &&
2415
+ sessionMemberships.length > 0 && (
2416
+ <SessionMembershipPanel memberships={sessionMemberships} />
2417
+ )}
2124
2418
 
2125
- <SidebarPanel label="Filters">
2126
- <div className="space-y-2">
2127
- {!hideSessionFilter && (
2128
- <>
2129
- <div className="relative">
2130
- <Search className="pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground" />
2131
- <input
2132
- type="search"
2133
- value={sessionInputValue}
2134
- onChange={handleSessionInputChange}
2135
- placeholder="Paste session ID"
2136
- className="bg-black/25 hover:bg-white/[0.045] focus-visible:ring-ring h-9 w-full rounded-md px-8 font-mono text-xs text-foreground outline-none shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)] transition-colors placeholder:font-sans placeholder:text-muted-foreground focus-visible:ring-1"
2137
- aria-label="Filter by session ID"
2138
- title="Filter by exact session ID"
2139
- />
2140
- {sessionInputValue !== "" && (
2141
- <button
2142
- type="button"
2143
- onClick={() => onSessionChange("__all__")}
2144
- className="absolute right-1.5 top-1/2 inline-flex size-6 -translate-y-1/2 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-white/[0.06] hover:text-foreground"
2145
- aria-label="Clear session filter"
2146
- title="Clear session filter"
2147
- >
2148
- <X className="size-3.5" />
2149
- </button>
2150
- )}
2151
- </div>
2152
- <Select value={sessionSelectValue} onValueChange={handleSessionSelectChange}>
2153
- <SelectTrigger className="bg-black/25 hover:bg-white/[0.045] h-9 w-full border-0 text-xs text-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)]">
2154
- <SelectValue placeholder="All sessions" />
2155
- </SelectTrigger>
2156
- <SelectContent>
2157
- <SelectItem value="__all__">All sessions</SelectItem>
2158
- {sessionSelectValue === "__typed__" && (
2159
- <SelectItem value="__typed__">
2160
- Typed: {truncateSessionId(selectedSession)}
2161
- </SelectItem>
2419
+ <SidebarPanel label="Filters">
2420
+ <div className="space-y-2">
2421
+ {!hideSessionFilter && (
2422
+ <>
2423
+ <div className="relative">
2424
+ <Search className="pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground" />
2425
+ <input
2426
+ type="search"
2427
+ value={sessionInputValue}
2428
+ onChange={handleSessionInputChange}
2429
+ placeholder="Paste session ID"
2430
+ className="bg-black/25 hover:bg-white/[0.045] focus-visible:ring-ring h-9 w-full rounded-md px-8 font-mono text-xs text-foreground outline-none shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)] transition-colors placeholder:font-sans placeholder:text-muted-foreground focus-visible:ring-1"
2431
+ aria-label="Filter by session ID"
2432
+ title="Filter by exact session ID"
2433
+ />
2434
+ {sessionInputValue !== "" && (
2435
+ <button
2436
+ type="button"
2437
+ onClick={() => onSessionChange("__all__")}
2438
+ className="absolute right-1.5 top-1/2 inline-flex size-6 -translate-y-1/2 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-white/[0.06] hover:text-foreground"
2439
+ aria-label="Clear session filter"
2440
+ title="Clear session filter"
2441
+ >
2442
+ <X className="size-3.5" />
2443
+ </button>
2162
2444
  )}
2163
- {sessions.map((s) => (
2164
- <SelectItem key={s} value={s}>
2165
- {truncateSessionId(s)}
2166
- </SelectItem>
2167
- ))}
2168
- </SelectContent>
2169
- </Select>
2170
- </>
2171
- )}
2172
- <Select value={selectedModel} onValueChange={onModelChange}>
2173
- <SelectTrigger className="bg-black/25 hover:bg-white/[0.045] h-9 w-full border-0 text-xs text-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)]">
2174
- <SelectValue placeholder="All models" />
2175
- </SelectTrigger>
2176
- <SelectContent>
2177
- <SelectItem value="__all__">All models</SelectItem>
2178
- {models.map((m) => (
2179
- <SelectItem key={m} value={m}>
2180
- {m}
2181
- </SelectItem>
2182
- ))}
2183
- </SelectContent>
2184
- </Select>
2185
- </div>
2186
- </SidebarPanel>
2187
- </div>
2188
- </aside>
2445
+ </div>
2446
+ <Select value={sessionSelectValue} onValueChange={handleSessionSelectChange}>
2447
+ <SelectTrigger className="bg-black/25 hover:bg-white/[0.045] h-9 w-full border-0 text-xs text-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)]">
2448
+ <SelectValue placeholder="All sessions" />
2449
+ </SelectTrigger>
2450
+ <SelectContent>
2451
+ <SelectItem value="__all__">All sessions</SelectItem>
2452
+ {sessionSelectValue === "__typed__" && (
2453
+ <SelectItem value="__typed__">
2454
+ Typed: {truncateSessionId(selectedSession)}
2455
+ </SelectItem>
2456
+ )}
2457
+ {sessions.map((s) => (
2458
+ <SelectItem key={s} value={s}>
2459
+ {truncateSessionId(s)}
2460
+ </SelectItem>
2461
+ ))}
2462
+ </SelectContent>
2463
+ </Select>
2464
+ </>
2465
+ )}
2466
+ <Select value={selectedModel} onValueChange={onModelChange}>
2467
+ <SelectTrigger className="bg-black/25 hover:bg-white/[0.045] h-9 w-full border-0 text-xs text-foreground shadow-[inset_0_0_0_1px_rgba(255,255,255,0.06)]">
2468
+ <SelectValue placeholder="All models" />
2469
+ </SelectTrigger>
2470
+ <SelectContent>
2471
+ <SelectItem value="__all__">All models</SelectItem>
2472
+ {models.map((m) => (
2473
+ <SelectItem key={m} value={m}>
2474
+ {m}
2475
+ </SelectItem>
2476
+ ))}
2477
+ </SelectContent>
2478
+ </Select>
2479
+ </div>
2480
+ </SidebarPanel>
2481
+ </div>
2482
+ </aside>
2483
+ )}
2189
2484
 
2190
2485
  <main className="order-2 min-w-0 lg:order-1">
2191
2486
  <div className="bg-background/95 shadow-[0_12px_36px_rgba(0,0,0,0.18),inset_0_-1px_0_rgba(255,255,255,0.045)] sticky top-0 z-30 px-4 py-3 lg:px-5">
@@ -2289,9 +2584,9 @@ export function ProxyViewer({
2289
2584
  <button
2290
2585
  type="button"
2291
2586
  onClick={handleRequestDeleteCurrentView}
2292
- disabled={!canClearCurrentView || deletingCurrentView}
2587
+ disabled={!canDeleteCurrentView || deletingCurrentView}
2293
2588
  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."
2589
+ title="Delete persisted history for the selected session."
2295
2590
  >
2296
2591
  <Trash2 className="size-3.5" />
2297
2592
  <span>Delete</span>
@@ -2431,28 +2726,15 @@ export function ProxyViewer({
2431
2726
  <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
2432
2727
  <DialogContent className="border-red-500/20 bg-zinc-950 text-zinc-100 sm:max-w-md">
2433
2728
  <DialogHeader>
2434
- <DialogTitle>Delete current logs?</DialogTitle>
2729
+ <DialogTitle>Delete {deleteTargetLabel} history?</DialogTitle>
2435
2730
  <DialogDescription>
2436
- Choose whether this only clears the UI or also deletes the stored log data for the
2437
- current view.
2731
+ Clear only hides logs from the UI. Delete removes stored history from disk.
2438
2732
  </DialogDescription>
2439
2733
  </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>
2734
+ <div className="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)]">
2735
+ <span className="block font-medium">This is a full history delete.</span>
2736
+ <span className="mt-1 block text-xs text-red-200/80">{deleteTargetDescription}</span>
2737
+ </div>
2456
2738
  <div className="flex justify-end gap-2">
2457
2739
  <Button
2458
2740
  type="button"
@@ -2465,16 +2747,12 @@ export function ProxyViewer({
2465
2747
  </Button>
2466
2748
  <Button
2467
2749
  type="button"
2468
- variant={deleteStoredData ? "destructive" : "secondary"}
2750
+ variant="destructive"
2469
2751
  size="sm"
2470
2752
  disabled={deletingCurrentView}
2471
2753
  onClick={handleConfirmDeleteCurrentView}
2472
2754
  >
2473
- {deletingCurrentView
2474
- ? "Deleting..."
2475
- : deleteStoredData
2476
- ? "Delete data"
2477
- : "Clear only"}
2755
+ {deletingCurrentView ? "Deleting..." : "Delete history"}
2478
2756
  </Button>
2479
2757
  </div>
2480
2758
  </DialogContent>