From c0d33c0cfb30867ec0b832ba8c9ddc83dc1e1195 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Sat, 8 Aug 2026 09:47:25 -0700 Subject: [PATCH] [eric] browser: the docked mini re-measures when its slot announces a remount, retiring a 150ms forever-poll of layout reads --- frontend/src/app/pages/AgentChat/AgentChat.tsx | 8 ++++++-- .../src/app/pages/Dashboard/cards/BrowserCard.tsx | 11 ++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/pages/AgentChat/AgentChat.tsx b/frontend/src/app/pages/AgentChat/AgentChat.tsx index a5151ddc..e3e3ff00 100644 --- a/frontend/src/app/pages/AgentChat/AgentChat.tsx +++ b/frontend/src/app/pages/AgentChat/AgentChat.tsx @@ -292,6 +292,10 @@ const AgentChat: React.FC = ({ sessionId: sessionIdProp, onClose // The live overlay stamps data-mini-live; while it paints, the frozen-shot backdrop must not (the clamped overlay leaves margins where a misaligned second copy of the page peeked through). '&[data-mini-live="1"] img': { opacity: 0 }, } as const; + // The slot announces its own (re)mount so the docked mini re-measures exactly then, instead of polling rects on a timer (the windowed transcript remounts it with no resize/pan event firing). + const announceBrowserSlot = useCallback((el: HTMLElement | null) => { + if (el) window.dispatchEvent(new CustomEvent('openswarm:browser-slot-mounted', { detail: { id } })); + }, [id]); // A live webview cannot be clipped by the scroller, so the OVERLAY only shows while fully in view; this frozen shot is what scrolls and clips underneath it, ChatGPT-style. const dockedShot = dockedSurfaceId ? getMinimizedShot(dockedSurfaceId) : undefined; const browserSlotBody = dockedShot ? ( @@ -1937,7 +1941,7 @@ const AgentChat: React.FC = ({ sessionId: sessionIdProp, onClose return ( {rendered} - {browserSlotBody} + {browserSlotBody} ); } @@ -2090,7 +2094,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 && ( - {browserSlotBody} + {browserSlotBody} )} diff --git a/frontend/src/app/pages/Dashboard/cards/BrowserCard.tsx b/frontend/src/app/pages/Dashboard/cards/BrowserCard.tsx index 9a10ac2f..0e2704f4 100644 --- a/frontend/src/app/pages/Dashboard/cards/BrowserCard.tsx +++ b/frontend/src/app/pages/Dashboard/cards/BrowserCard.tsx @@ -310,18 +310,23 @@ const BrowserCard: React.FC = ({ window.addEventListener('openswarm:canvas-pan-changed', measure); document.addEventListener('visibilitychange', measure); const timers = [60, 250, 700].map((ms) => window.setTimeout(measure, ms)); - // Self-heal: the slot lives in the WINDOWED transcript and remounts without firing any of the events above, which left the mini scaled against a stale rect (visibly overflowing its frame) until the next pan. 150ms bounds how long any staleness can survive. - const heal = window.setInterval(measure, 150); + // The slot lives in the WINDOWED transcript and remounts without firing any of the events + // above; it announces itself on mount now, which retired a 150ms forever-poll that forced a + // layout read ~7x/sec per docked mini even while nothing moved (measure once per real change). + const onSlotMounted = (e: Event): void => { + if ((e as CustomEvent).detail?.id === dockedTo) measure(); + }; + window.addEventListener('openswarm:browser-slot-mounted', onSlotMounted); return () => { ro.disconnect(); mo.disconnect(); window.removeEventListener('resize', measure); window.removeEventListener('openswarm:canvas-pan-changed', measure); document.removeEventListener('visibilitychange', measure); + window.removeEventListener('openswarm:browser-slot-mounted', onSlotMounted); scrollHost?.removeEventListener('scroll', onScroll); if (scrollRaf) cancelAnimationFrame(scrollRaf); timers.forEach((tm) => window.clearTimeout(tm)); - window.clearInterval(heal); }; // dockParentCard x/y/w/h are re-measure triggers: the slot's client rect moves with the chat card. }, [dockedTo, dockParentExpanded, dockParentTiled, dockParentCard?.x, dockParentCard?.y, dockParentCard?.width, dockParentCard?.height, getCanvasState, dockParentCard]);