From 344f58b88931f8d3fa6c71b85a9caa5e1d9799dc Mon Sep 17 00:00:00 2001 From: ciregenz Date: Tue, 4 Aug 2026 19:45:22 -0700 Subject: [PATCH] [eric] chat: the inline browser hugs the page's aspect and can never paint outside its chat card --- .../src/app/pages/AgentChat/AgentChat.tsx | 22 ++++++++++++++----- .../app/pages/Dashboard/cards/BrowserCard.tsx | 21 ++++++++++++++++-- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/pages/AgentChat/AgentChat.tsx b/frontend/src/app/pages/AgentChat/AgentChat.tsx index fbafc082..0f2fee3c 100644 --- a/frontend/src/app/pages/AgentChat/AgentChat.tsx +++ b/frontend/src/app/pages/AgentChat/AgentChat.tsx @@ -257,6 +257,21 @@ const AgentChat: React.FC = ({ sessionId: sessionIdProp, onClose const hasDockedBrowser = useAppSelector((st) => Object.values(st.dashboardLayout.browserCards).some((bc) => bc.docked_to === (sessionIdProp || routeId)) || Object.values(st.dashboardLayout.viewCards).some((vc) => vc.docked_to === (sessionIdProp || routeId))); + // The docked surface's aspect ratio, so the inline slot hugs the browser's shape instead of reserving a fixed letterbox band (primitive selectors so no fresh-object rerenders). + const dockedSurfaceW = useAppSelector((st) => + Object.values(st.dashboardLayout.browserCards).find((bc) => bc.docked_to === (sessionIdProp || routeId))?.width ?? 0); + const dockedSurfaceH = useAppSelector((st) => + Object.values(st.dashboardLayout.browserCards).find((bc) => bc.docked_to === (sessionIdProp || routeId))?.height ?? 0); + // Shared by the anchor slot and the fallback slot so both read as the same framed block. + const browserSlotSx = { + width: '100%', + aspectRatio: dockedSurfaceW > 0 && dockedSurfaceH > 0 ? `${dockedSurfaceW} / ${dockedSurfaceH}` : undefined, + height: dockedSurfaceW > 0 && dockedSurfaceH > 0 ? 'auto' : 'min(360px, 38vh)', + maxHeight: 'min(400px, 42vh)', + minHeight: 140, + mt: 1, + mb: 0.5, + } as const; // A card linked as a workflow sidecar (Test Agent, or a watched run) swaps its composer for a Force Stop button: continuing the chat is meaningless, but killing the run is the common need. Once a Test Agent finishes, the button flips to a green "close" (see workflow_test_state + ForceStopAgentBar). const linkedSidecar = useAppSelector((s) => { const found = Object.values(s.workflows.openCards).find( @@ -1861,7 +1876,7 @@ const AgentChat: React.FC = ({ sessionId: sessionIdProp, onClose return ( {rendered} - + ); } @@ -2014,10 +2029,7 @@ const AgentChat: React.FC = ({ sessionId: sessionIdProp, onClose )} {/* Fallback dock slot for a browser that docked before any browser tool row exists (or whose row was compacted away); once a row appears the slot anchors at it instead (see browserAnchorItemId). The real card overlays this rect geometrically, so the webview never remounts; the mini hides itself when its slot scrolls mostly out of view, since a live webview can't be clipped by the scroller. */} {hasDockedBrowser && !browserAnchorItemId && ( - + )} diff --git a/frontend/src/app/pages/Dashboard/cards/BrowserCard.tsx b/frontend/src/app/pages/Dashboard/cards/BrowserCard.tsx index 30674dbf..1ea5f8c1 100644 --- a/frontend/src/app/pages/Dashboard/cards/BrowserCard.tsx +++ b/frontend/src/app/pages/Dashboard/cards/BrowserCard.tsx @@ -274,7 +274,18 @@ const BrowserCard: React.FC = ({ } const z = getCanvasState().zoom || 1; const lr = layer.getBoundingClientRect(); - const sr = slot.getBoundingClientRect(); + let sr: { left: number; top: number; right: number; bottom: number; width: number; height: number } = slot.getBoundingClientRect(); + // Hard containment: the mini must NEVER paint outside its chat card, whatever a mid-animation or mismeasured slot claims; clamp to the card's real bounds and hide when the overlap collapses. + const parentEl = document.querySelector(`[data-select-id="${dockedTo}"]`); + if (parentEl) { + const pr = parentEl.getBoundingClientRect(); + const left = Math.max(sr.left, pr.left); + const top = Math.max(sr.top, pr.top); + const right = Math.min(sr.right, pr.right); + const bottom = Math.min(sr.bottom, pr.bottom); + if (right - left < 60 || bottom - top < 60) { setDockVisible(false); return; } + sr = { left, top, right, bottom, width: right - left, height: bottom - top }; + } // Slot and card share the transformed layer, so layer-relative coords are camera-invariant. setDockRect({ x: (sr.left - lr.left) / z, y: (sr.top - lr.top) / z, w: sr.width / z, h: sr.height / z }); if (scrollHost) { @@ -436,7 +447,8 @@ const BrowserCard: React.FC = ({ }) .catch(() => {}); try { - (wv as any).setVisualZoomLevelLimits?.(1, 1); + // Chrome-parity pinch: locked at (1,1) Electron DROPS trackpad pinch entirely, so Figma/Miro/Maps never saw the ctrl+wheel their canvas zoom listens for. Pages that preventDefault it (Figma) own the zoom; plain pages get Chrome's pinch magnify. + (wv as any).setVisualZoomLevelLimits?.(1, 3); (wv as any).setZoomFactor?.(1); } catch (_) {} }; @@ -449,6 +461,11 @@ const BrowserCard: React.FC = ({ }; wv.addEventListener('dom-ready', onReady, { once: true }); cleanups.push(() => wv.removeEventListener('dom-ready', onReady)); + // The preload re-runs on every full navigation, so re-tag the guest each dom-ready: browser surfaces keep ctrl/meta+wheel (pinch) IN the page instead of forwarding it to canvas zoom. + const tagSurface = () => { try { (wv as any).send?.('openswarm:set-surface', { kind: 'browser' }); } catch (_) {} }; + tagSurface(); + wv.addEventListener('dom-ready', tagSurface); + cleanups.push(() => wv.removeEventListener('dom-ready', tagSurface)); } // Every guest sits at about:blank before its real load (lazy tabs never leave it); mirroring