@tonyclaw/agent-inspector 3.0.14 → 3.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-DDfwHQUi.js → CompareDrawer-UaIxykqz.js} +1 -1
  3. package/.output/public/assets/ProxyViewerContainer-DlpfcX56.js +106 -0
  4. package/.output/public/assets/{ReplayDialog-D-26DMBt.js → ReplayDialog-BFFomU1m.js} +1 -1
  5. package/.output/public/assets/{RequestAnatomy-B6zNFqNX.js → RequestAnatomy-DKQJBt_c.js} +1 -1
  6. package/.output/public/assets/{ResponseView-Ccy95rWI.js → ResponseView-D7yotmne.js} +2 -2
  7. package/.output/public/assets/{StreamingChunkSequence-yIiscGuE.js → StreamingChunkSequence-Da-6WNFG.js} +1 -1
  8. package/.output/public/assets/{_sessionId-BnMB0zab.js → _sessionId-ByTXLS-w.js} +1 -1
  9. package/.output/public/assets/{index-BbtAIxbg.js → index-BrM8t5mc.js} +1 -1
  10. package/.output/public/assets/index-CGpPRty8.js +1 -0
  11. package/.output/public/assets/index-HRfrFVYw.css +1 -0
  12. package/.output/public/assets/{json-viewer-C838cVmD.js → json-viewer-DyjXQdVu.js} +1 -1
  13. package/.output/public/assets/{main-BfN4culo.js → main-Zv_gjZMb.js} +2 -2
  14. package/.output/server/_libs/lucide-react.mjs +215 -201
  15. package/.output/server/{_sessionId-DoprmwZV.mjs → _sessionId-GcFhWT0i.mjs} +2 -2
  16. package/.output/server/_ssr/{CompareDrawer-YMLJNxXR.mjs → CompareDrawer-BV7dw1TN.mjs} +3 -3
  17. package/.output/server/_ssr/{ProxyViewerContainer-C62eZnva.mjs → ProxyViewerContainer-CcgfvaS4.mjs} +725 -378
  18. package/.output/server/_ssr/{ReplayDialog-NmN7Ukjo.mjs → ReplayDialog-B7sBKxIO.mjs} +4 -4
  19. package/.output/server/_ssr/{RequestAnatomy-Dawm2qxe.mjs → RequestAnatomy-s_9Veje2.mjs} +3 -3
  20. package/.output/server/_ssr/{ResponseView-BHyAY83t.mjs → ResponseView-DsOFPCvF.mjs} +3 -3
  21. package/.output/server/_ssr/{StreamingChunkSequence-C4qgcJbg.mjs → StreamingChunkSequence-CyA1XtiF.mjs} +3 -3
  22. package/.output/server/_ssr/{index-B2Pf6W_T.mjs → index-CDDRt201.mjs} +2 -2
  23. package/.output/server/_ssr/index.mjs +2 -2
  24. package/.output/server/_ssr/{json-viewer-u67BASf8.mjs → json-viewer-BFV-xtRM.mjs} +3 -3
  25. package/.output/server/_ssr/{router-CZhFVMGN.mjs → router-B2Q7vHPM.mjs} +58 -14
  26. package/.output/server/{_tanstack-start-manifest_v-Dk2HfOoe.mjs → _tanstack-start-manifest_v-Dm_GmuRz.mjs} +1 -1
  27. package/.output/server/index.mjs +73 -73
  28. package/package.json +1 -1
  29. package/src/components/ProxyViewer.tsx +470 -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/proxy/formats/openai/codexMetadata.ts +61 -0
  34. package/src/proxy/formats/openai/handler.ts +8 -2
  35. package/styles/globals.css +54 -0
  36. package/.output/public/assets/ProxyViewerContainer-CbNsaoQw.js +0 -106
  37. package/.output/public/assets/index-49RC4dlD.js +0 -1
  38. 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,10 @@ 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(
2059
+ readStoredDetailsPanelCollapsed,
2060
+ );
1770
2061
  const [comparePair, setComparePair] = useState<[CapturedLog, CapturedLog] | null>(null);
1771
2062
  const [crabEntrancePhase, setCrabEntrancePhase] = useState<"hidden" | "playing" | "done">(
1772
2063
  "hidden",
@@ -1807,6 +2098,13 @@ export function ProxyViewer({
1807
2098
  };
1808
2099
  }, []);
1809
2100
 
2101
+ useEffect(() => {
2102
+ window.localStorage.setItem(
2103
+ DETAILS_PANEL_COLLAPSED_STORAGE_KEY,
2104
+ detailsPanelCollapsed ? "1" : "0",
2105
+ );
2106
+ }, [detailsPanelCollapsed]);
2107
+
1810
2108
  useEffect(() => {
1811
2109
  if (pinnedSessionId === undefined) {
1812
2110
  document.title = "Agent Inspector";
@@ -1898,7 +2196,6 @@ export function ProxyViewer({
1898
2196
  }, [cancelTransientTasks, onClearAll]);
1899
2197
 
1900
2198
  const handleRequestDeleteCurrentView = useCallback(() => {
1901
- setDeleteStoredData(false);
1902
2199
  setDeleteDialogOpen(true);
1903
2200
  }, []);
1904
2201
 
@@ -1908,14 +2205,10 @@ export function ProxyViewer({
1908
2205
  setImportError(null);
1909
2206
  setComparePair(null);
1910
2207
 
1911
- if (!deleteStoredData || onDeleteCurrentView === undefined) {
1912
- setDeleteDialogOpen(false);
1913
- onClearAll();
1914
- return;
1915
- }
2208
+ if (onDeleteCurrentView === undefined) return;
1916
2209
 
1917
2210
  setDeletingCurrentView(true);
1918
- void onDeleteCurrentView(logs.map((log) => log.id))
2211
+ void onDeleteCurrentView()
1919
2212
  .then(() => {
1920
2213
  setDeleteDialogOpen(false);
1921
2214
  })
@@ -1930,7 +2223,7 @@ export function ProxyViewer({
1930
2223
  .finally(() => {
1931
2224
  setDeletingCurrentView(false);
1932
2225
  });
1933
- }, [cancelTransientTasks, deleteStoredData, logs, onClearAll, onDeleteCurrentView, onNotify]);
2226
+ }, [cancelTransientTasks, onDeleteCurrentView, onNotify]);
1934
2227
 
1935
2228
  const handleReloadCurrentSession = useCallback(() => {
1936
2229
  if (onReloadSession === undefined) return;
@@ -1945,6 +2238,10 @@ export function ProxyViewer({
1945
2238
  setComparePair(null);
1946
2239
  }, []);
1947
2240
 
2241
+ const toggleDetailsPanelCollapsed = useCallback(() => {
2242
+ setDetailsPanelCollapsed((value) => !value);
2243
+ }, []);
2244
+
1948
2245
  const groups = useMemo(() => groupLogsByConversation(logs), [logs]);
1949
2246
  const displayNumberByLogId = useMemo(() => buildDisplayNumberByLogId(groups), [groups]);
1950
2247
  const sessionContextScope = useMemo(
@@ -1966,6 +2263,13 @@ export function ProxyViewer({
1966
2263
  const exportActionVisibility = getExportActionVisibility(captureMode, logs.length);
1967
2264
  const isPinnedSessionPage = pinnedSessionId !== undefined;
1968
2265
  const canClearCurrentView = logs.length > 0;
2266
+ const canDeleteCurrentView =
2267
+ onDeleteCurrentView !== undefined && (logs.length > 0 || selectedSession !== "__all__");
2268
+ const deleteTargetLabel = selectedSession === "__all__" ? "all sessions" : "this session";
2269
+ const deleteTargetDescription =
2270
+ selectedSession === "__all__"
2271
+ ? "Deletes every stored log, streaming chunk, and session archive entry across all sessions."
2272
+ : "Deletes every stored log, streaming chunk, and session archive entry for this session, including history that is not currently loaded.";
1969
2273
  const reloadSessionLabel = logs.length === 0 ? "Load" : "Reload";
1970
2274
  const sessionInputValue = selectedSession === "__all__" ? "" : selectedSession;
1971
2275
  const sessionSelectValue =
@@ -2019,173 +2323,159 @@ export function ProxyViewer({
2019
2323
 
2020
2324
  return (
2021
2325
  <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
- />
2326
+ <button
2327
+ type="button"
2328
+ onClick={toggleDetailsPanelCollapsed}
2329
+ className={cn(
2330
+ "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",
2331
+ detailsPanelCollapsed ? "right-3" : "lg:right-[326px] xl:right-[346px]",
2332
+ )}
2333
+ aria-label={detailsPanelCollapsed ? "Show details panel" : "Hide details panel"}
2334
+ title={detailsPanelCollapsed ? "Show details panel" : "Hide details panel"}
2335
+ >
2336
+ <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]" />
2337
+ {detailsPanelCollapsed ? (
2338
+ <PanelRightOpen className="size-3.5" />
2339
+ ) : (
2340
+ <PanelRightClose className="size-3.5" />
2341
+ )}
2342
+ </button>
2343
+ {detailsPanelCollapsed && <FloatingInspectorLogo />}
2344
+ <div
2345
+ className={cn(
2346
+ "grid min-h-screen w-full grid-cols-1",
2347
+ detailsPanelCollapsed
2348
+ ? "lg:grid-cols-[minmax(0,1fr)]"
2349
+ : "lg:grid-cols-[minmax(0,1fr)_340px] xl:grid-cols-[minmax(0,1fr)_360px]",
2350
+ )}
2351
+ >
2352
+ {!detailsPanelCollapsed && (
2353
+ <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]">
2354
+ <div className="space-y-4 p-4">
2355
+ <BrandHeader
2356
+ compact={true}
2357
+ crabEntrancePhase={crabEntrancePhase}
2358
+ currentSessionId={labSessionId}
2359
+ logCount={logs.length}
2360
+ />
2031
2361
 
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>
2362
+ <SidebarPanel label="Session">
2363
+ <div className="grid grid-cols-2 gap-2">
2364
+ <SidebarMetric label="Requests" value={logs.length} />
2365
+ <SidebarMetric label="Sessions" value={sessions.length} />
2366
+ <SidebarMetric
2367
+ label="Input"
2368
+ value={formatTokens(hasSessionContext ? sessionContextTotalIn : totalIn)}
2369
+ />
2370
+ <SidebarMetric
2371
+ label="Output"
2372
+ value={formatTokens(hasSessionContext ? sessionContextTotalOut : totalOut)}
2373
+ />
2374
+ </div>
2375
+ {sessionLoadProgress !== undefined && (
2376
+ <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)]">
2377
+ <SessionLoadProgressBar progress={sessionLoadProgress} />
2378
+ </div>
2057
2379
  )}
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."
2380
+ </SidebarPanel>
2381
+
2382
+ <SidebarPanel label="Workspace">
2383
+ <div className="bg-black/20 rounded-md px-3 py-2 shadow-[inset_0_1px_0_rgba(255,255,255,0.035)]">
2384
+ <div
2385
+ className="truncate font-mono text-xs font-semibold text-cyan-100"
2386
+ title={activeScopeLabel}
2080
2387
  >
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} />
2388
+ {activeScopeLabel}
2389
+ </div>
2390
+ <div className="mt-1 text-[11px] text-muted-foreground">
2391
+ {isPinnedSessionPage ? "Session workspace" : "Live workspace"}
2392
+ </div>
2089
2393
  </div>
2394
+ </SidebarPanel>
2395
+
2396
+ {hasSessionContext && sessionContextScope !== null && (
2397
+ <SessionContextBar
2398
+ label={sessionContextScope.label}
2399
+ logs={sessionContextLogs}
2400
+ totalIn={sessionContextTotalIn}
2401
+ totalOut={sessionContextTotalOut}
2402
+ timeDisplayFormat={timeDisplayFormat}
2403
+ contextSummary={sessionContextSummary}
2404
+ showBackLink={sessionContextScope.showBackLink}
2405
+ />
2090
2406
  )}
2091
- </SidebarPanel>
2092
2407
 
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
-
2119
- {hasSessionContext &&
2120
- pinnedSessionId !== undefined &&
2121
- sessionMemberships.length > 0 && (
2122
- <SessionMembershipPanel memberships={sessionMemberships} />
2123
- )}
2408
+ {hasSessionContext &&
2409
+ pinnedSessionId !== undefined &&
2410
+ sessionMemberships.length > 0 && (
2411
+ <SessionMembershipPanel memberships={sessionMemberships} />
2412
+ )}
2124
2413
 
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>
2414
+ <SidebarPanel label="Filters">
2415
+ <div className="space-y-2">
2416
+ {!hideSessionFilter && (
2417
+ <>
2418
+ <div className="relative">
2419
+ <Search className="pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground" />
2420
+ <input
2421
+ type="search"
2422
+ value={sessionInputValue}
2423
+ onChange={handleSessionInputChange}
2424
+ placeholder="Paste session ID"
2425
+ 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"
2426
+ aria-label="Filter by session ID"
2427
+ title="Filter by exact session ID"
2428
+ />
2429
+ {sessionInputValue !== "" && (
2430
+ <button
2431
+ type="button"
2432
+ onClick={() => onSessionChange("__all__")}
2433
+ 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"
2434
+ aria-label="Clear session filter"
2435
+ title="Clear session filter"
2436
+ >
2437
+ <X className="size-3.5" />
2438
+ </button>
2162
2439
  )}
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>
2440
+ </div>
2441
+ <Select value={sessionSelectValue} onValueChange={handleSessionSelectChange}>
2442
+ <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)]">
2443
+ <SelectValue placeholder="All sessions" />
2444
+ </SelectTrigger>
2445
+ <SelectContent>
2446
+ <SelectItem value="__all__">All sessions</SelectItem>
2447
+ {sessionSelectValue === "__typed__" && (
2448
+ <SelectItem value="__typed__">
2449
+ Typed: {truncateSessionId(selectedSession)}
2450
+ </SelectItem>
2451
+ )}
2452
+ {sessions.map((s) => (
2453
+ <SelectItem key={s} value={s}>
2454
+ {truncateSessionId(s)}
2455
+ </SelectItem>
2456
+ ))}
2457
+ </SelectContent>
2458
+ </Select>
2459
+ </>
2460
+ )}
2461
+ <Select value={selectedModel} onValueChange={onModelChange}>
2462
+ <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)]">
2463
+ <SelectValue placeholder="All models" />
2464
+ </SelectTrigger>
2465
+ <SelectContent>
2466
+ <SelectItem value="__all__">All models</SelectItem>
2467
+ {models.map((m) => (
2468
+ <SelectItem key={m} value={m}>
2469
+ {m}
2470
+ </SelectItem>
2471
+ ))}
2472
+ </SelectContent>
2473
+ </Select>
2474
+ </div>
2475
+ </SidebarPanel>
2476
+ </div>
2477
+ </aside>
2478
+ )}
2189
2479
 
2190
2480
  <main className="order-2 min-w-0 lg:order-1">
2191
2481
  <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 +2579,9 @@ export function ProxyViewer({
2289
2579
  <button
2290
2580
  type="button"
2291
2581
  onClick={handleRequestDeleteCurrentView}
2292
- disabled={!canClearCurrentView || deletingCurrentView}
2582
+ disabled={!canDeleteCurrentView || deletingCurrentView}
2293
2583
  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."
2584
+ title="Delete persisted history for the selected session."
2295
2585
  >
2296
2586
  <Trash2 className="size-3.5" />
2297
2587
  <span>Delete</span>
@@ -2431,28 +2721,15 @@ export function ProxyViewer({
2431
2721
  <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
2432
2722
  <DialogContent className="border-red-500/20 bg-zinc-950 text-zinc-100 sm:max-w-md">
2433
2723
  <DialogHeader>
2434
- <DialogTitle>Delete current logs?</DialogTitle>
2724
+ <DialogTitle>Delete {deleteTargetLabel} history?</DialogTitle>
2435
2725
  <DialogDescription>
2436
- Choose whether this only clears the UI or also deletes the stored log data for the
2437
- current view.
2726
+ Clear only hides logs from the UI. Delete removes stored history from disk.
2438
2727
  </DialogDescription>
2439
2728
  </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>
2729
+ <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)]">
2730
+ <span className="block font-medium">This is a full history delete.</span>
2731
+ <span className="mt-1 block text-xs text-red-200/80">{deleteTargetDescription}</span>
2732
+ </div>
2456
2733
  <div className="flex justify-end gap-2">
2457
2734
  <Button
2458
2735
  type="button"
@@ -2465,16 +2742,12 @@ export function ProxyViewer({
2465
2742
  </Button>
2466
2743
  <Button
2467
2744
  type="button"
2468
- variant={deleteStoredData ? "destructive" : "secondary"}
2745
+ variant="destructive"
2469
2746
  size="sm"
2470
2747
  disabled={deletingCurrentView}
2471
2748
  onClick={handleConfirmDeleteCurrentView}
2472
2749
  >
2473
- {deletingCurrentView
2474
- ? "Deleting..."
2475
- : deleteStoredData
2476
- ? "Delete data"
2477
- : "Clear only"}
2750
+ {deletingCurrentView ? "Deleting..." : "Delete history"}
2478
2751
  </Button>
2479
2752
  </div>
2480
2753
  </DialogContent>