[eric] chat: the inline browser hugs the page's aspect and can never paint outside its chat card

This commit is contained in:
ciregenz
2026-08-04 19:45:22 -07:00
parent 4554af3b1e
commit 344f58b889
2 changed files with 36 additions and 7 deletions
+17 -5
View File
@@ -257,6 +257,21 @@ const AgentChat: React.FC<AgentChatProps> = ({ 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<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
return (
<React.Fragment key={`${item.id}-with-browser`}>
{rendered}
<Box data-browser-slot={id} sx={{ height: 'min(360px, 38vh)', minHeight: 180, mt: 1, mb: 0.5 }} />
<Box data-browser-slot={id} sx={browserSlotSx} />
</React.Fragment>
);
}
@@ -2014,10 +2029,7 @@ const AgentChat: React.FC<AgentChatProps> = ({ 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 && (
<Box
data-browser-slot={id}
sx={{ height: 'min(360px, 38vh)', minHeight: 180, mt: 1, mb: 0.5 }}
/>
<Box data-browser-slot={id} sx={browserSlotSx} />
)}
</Box>
</Box>
@@ -274,7 +274,18 @@ const BrowserCard: React.FC<Props> = ({
}
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<Props> = ({
})
.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<Props> = ({
};
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