mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-28 10:49:46 +02:00
[eric] chat: the agent's browser rides the transcript inline like a tool output, the pinned bottom slot is gone
This commit is contained in:
@@ -1987,6 +1987,13 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{/* Inline dock slot: the agent's browser rides HERE, in the transcript flow like a tool output (the real card overlays this rect geometrically, so the webview never remounts). It scrolls with the conversation; the mini hides itself when this scrolls mostly out of view, since a live webview can't be clipped by the scroller. */}
|
||||
{hasDockedBrowser && (
|
||||
<Box
|
||||
data-browser-slot={id}
|
||||
sx={{ height: 'min(360px, 38vh)', minHeight: 180, mt: 1, mb: 0.5 }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
{showScrollButton && (
|
||||
@@ -2418,27 +2425,6 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
|
||||
</Fade>
|
||||
);
|
||||
})()}
|
||||
{/* Dock slot: a browser this agent spawned lives HERE by default (the real card overlays
|
||||
this rect geometrically, so the webview never remounts). Pinned between transcript
|
||||
and composer, never inside the scroller, so it can't be clipped by chat scroll. */}
|
||||
{hasDockedBrowser && (
|
||||
<Box
|
||||
data-browser-slot={id}
|
||||
sx={{
|
||||
// Percentage heights resolve against a NESTED wrapper here, not the card, which
|
||||
// pushed the slot (and the docked browser riding it) clean out of the chat.
|
||||
// Viewport units are the only stable yardstick in this column; shrink allowed so
|
||||
// a short card squeezes the slot instead of overflowing.
|
||||
flex: '0 1 auto',
|
||||
height: 'min(360px, 38vh)',
|
||||
minHeight: 180,
|
||||
mx: 1.5,
|
||||
mb: 1,
|
||||
// Pure geometry: the docked mini overlays this rect, so any visible chrome here
|
||||
// (the old dashed outline) just framed the letterbox margins as an ugly gap.
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{readOnly ? null : isStoppableSidecar ? (
|
||||
<ForceStopAgentBar onStop={handleStop} onSaveWorkflow={onTestSaveWorkflow} onContinueEditing={onTestContinueEditing} testState={testState} />
|
||||
) : (
|
||||
|
||||
@@ -247,24 +247,44 @@ const BrowserCard: React.FC<Props> = ({
|
||||
const dockParentExpanded = useAppSelector((state) => (dockedTo ? state.agents.expandedSessionIds.includes(dockedTo) : false));
|
||||
const dockParentTiled = useAppSelector((state) => (dockedTo ? state.dashboardLayout.tiledCards[dockedTo] : undefined));
|
||||
const [dockRect, setDockRect] = useState<{ x: number; y: number; w: number; h: number } | null>(null);
|
||||
// The slot now lives INSIDE the transcript scroller (inline like a tool output), and a live webview cannot be clipped by a scroll container, so the mini hides when its slot scrolls mostly out of view instead.
|
||||
const [dockVisible, setDockVisible] = useState(true);
|
||||
const rootElRef = useRef<HTMLDivElement | null>(null);
|
||||
useEffect(() => {
|
||||
if (!dockedTo || !dockParentCard || !dockParentExpanded) { setDockRect(null); return undefined; }
|
||||
if (!dockedTo || !dockParentCard || !dockParentExpanded) { setDockRect(null); setDockVisible(true); return undefined; }
|
||||
let scrollHost: Element | null = null;
|
||||
let hookedSlot: Element | null = null;
|
||||
let scrollRaf = 0;
|
||||
const onScroll = (): void => { if (!scrollRaf) scrollRaf = requestAnimationFrame(() => { scrollRaf = 0; measure(); }); };
|
||||
const ro = new ResizeObserver(() => measure());
|
||||
const measure = (): void => {
|
||||
const slot = document.querySelector(`[data-browser-slot="${dockedTo}"]`);
|
||||
const layer = rootElRef.current?.parentElement;
|
||||
if (!slot || !layer) { setDockRect(null); return; }
|
||||
// The slot mounts a beat after docking (and remounts with chat re-renders), so observers hook the live node whenever it changes; a one-shot hookup at effect time reliably missed it and froze the rect.
|
||||
if (slot !== hookedSlot) {
|
||||
ro.disconnect();
|
||||
ro.observe(slot);
|
||||
if (slot.parentElement) ro.observe(slot.parentElement);
|
||||
scrollHost?.removeEventListener('scroll', onScroll);
|
||||
scrollHost = slot.closest('[data-chat-transcript]');
|
||||
scrollHost?.addEventListener('scroll', onScroll, { passive: true });
|
||||
hookedSlot = slot;
|
||||
}
|
||||
const z = getCanvasState().zoom || 1;
|
||||
const lr = layer.getBoundingClientRect();
|
||||
const sr = slot.getBoundingClientRect();
|
||||
// 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) {
|
||||
const cr = scrollHost.getBoundingClientRect();
|
||||
const overlap = Math.min(sr.bottom, cr.bottom) - Math.max(sr.top, cr.top);
|
||||
setDockVisible(overlap / Math.max(1, sr.height) >= 0.35);
|
||||
} else {
|
||||
setDockVisible(true);
|
||||
}
|
||||
};
|
||||
measure();
|
||||
const slot = document.querySelector(`[data-browser-slot="${dockedTo}"]`);
|
||||
const ro = new ResizeObserver(measure);
|
||||
if (slot) ro.observe(slot);
|
||||
if (slot?.parentElement) ro.observe(slot.parentElement);
|
||||
window.addEventListener('resize', measure);
|
||||
// A RO only fires on slot RESIZE; the chat tiling/untiling MOVES the slot without resizing the
|
||||
// window, so re-measure on camera writes + settle timers or the docked card lags behind.
|
||||
@@ -276,6 +296,8 @@ const BrowserCard: React.FC<Props> = ({
|
||||
window.removeEventListener('resize', measure);
|
||||
window.removeEventListener('openswarm:canvas-pan-changed', measure);
|
||||
document.removeEventListener('visibilitychange', measure);
|
||||
scrollHost?.removeEventListener('scroll', onScroll);
|
||||
if (scrollRaf) cancelAnimationFrame(scrollRaf);
|
||||
timers.forEach((tm) => window.clearTimeout(tm));
|
||||
};
|
||||
// dockParentCard x/y/w/h are re-measure triggers: the slot's client rect moves with the chat card.
|
||||
@@ -1091,8 +1113,8 @@ const BrowserCard: React.FC<Props> = ({
|
||||
}}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
// Kept-alive card from another dashboard: parked far off-screen so its webview surface can't bleed onto the dashboard you're viewing; click-through, webContents stays mounted.
|
||||
pointerEvents: keepAliveHidden || isMinimized || dockParked ? 'none' : undefined,
|
||||
// Kept-alive card from another dashboard: parked far off-screen so its webview surface can't bleed onto the dashboard you're viewing; click-through, webContents stays mounted. A dock-hidden mini (slot scrolled away) is click-through too.
|
||||
pointerEvents: keepAliveHidden || isMinimized || dockParked || (dockActive && !dockVisible) ? 'none' : undefined,
|
||||
// contain: webview repaints don't shake neighbor cards.
|
||||
contain: 'layout style',
|
||||
// Own compositor layer so hover/paint invalidations stay contained to this card. See AgentCard for full rationale.
|
||||
@@ -1114,7 +1136,9 @@ const BrowserCard: React.FC<Props> = ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
zIndex: isTiled ? 999990 : (isDragging || isResizing) ? 999999 : dockActive ? (dockParentTiled ? 999991 : dockParentZ + 1) : cardZOrder,
|
||||
transition: noTransition ? 'none' : 'box-shadow 0.4s ease, border 0.3s ease',
|
||||
// The inline slot scrolls with the transcript; a webview can't be clipped by the scroller, so the mini fades out when its slot is mostly out of view instead of floating over unrelated messages.
|
||||
opacity: dockActive && !dockVisible ? 0 : 1,
|
||||
transition: noTransition ? 'none' : 'box-shadow 0.4s ease, border 0.3s ease, opacity 0.14s ease',
|
||||
'&:hover .resize-handle': { opacity: 1 },
|
||||
...(isHighlighted && {
|
||||
animation: 'card-highlight-pulse 2s ease-out forwards',
|
||||
|
||||
Reference in New Issue
Block a user