mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-28 04:24:51 +02:00
[eric] dashboard: fullscreen chat docks the real browser (slot in full view, posEl-aware tile applier w/ live camera tracking, dock re-measure on slot moves, no dupe embed)
This commit is contained in:
@@ -2402,7 +2402,7 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
|
||||
{/* 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 && !fullscreenChat && (
|
||||
{hasDockedBrowser && (
|
||||
<Box
|
||||
data-browser-slot={id}
|
||||
sx={{
|
||||
|
||||
@@ -108,8 +108,10 @@ const InlineSurfaceEmbeds: React.FC<{ c: ClaudeTokens; sessionId: string; fullsc
|
||||
.filter((s) => s.parent_session_id === sessionId && s.browser_id)
|
||||
.map((s) => s.browser_id as string),
|
||||
);
|
||||
// A docked browser renders its REAL card inside the chat (even in full size view), so the
|
||||
// snapshot embed would be a stale duplicate right next to the live thing.
|
||||
return Object.values(browserCards).filter(
|
||||
(bc) => bc.spawned_by === sessionId || childBrowserIds.has(bc.browser_id),
|
||||
(bc) => !bc.docked_to && (bc.spawned_by === sessionId || childBrowserIds.has(bc.browser_id)),
|
||||
);
|
||||
}, [browserCards, sessions, sessionId]);
|
||||
const linkedApps = useMemo(
|
||||
|
||||
@@ -247,6 +247,7 @@ const BrowserCard: React.FC<Props> = ({
|
||||
const dockedTo = useAppSelector((state) => state.dashboardLayout.browserCards[browserId]?.docked_to ?? null);
|
||||
const dockParentCard = useAppSelector((state) => (dockedTo ? state.dashboardLayout.cards[dockedTo] ?? null : null));
|
||||
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);
|
||||
const rootElRef = useRef<HTMLDivElement | null>(null);
|
||||
useEffect(() => {
|
||||
@@ -267,9 +268,18 @@ const BrowserCard: React.FC<Props> = ({
|
||||
if (slot) ro.observe(slot);
|
||||
if (slot?.parentElement) ro.observe(slot.parentElement);
|
||||
window.addEventListener('resize', measure);
|
||||
return () => { ro.disconnect(); window.removeEventListener('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.
|
||||
window.addEventListener('openswarm:canvas-pan-changed', measure);
|
||||
const timers = [60, 250, 700].map((ms) => window.setTimeout(measure, ms));
|
||||
return () => {
|
||||
ro.disconnect();
|
||||
window.removeEventListener('resize', measure);
|
||||
window.removeEventListener('openswarm:canvas-pan-changed', measure);
|
||||
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.
|
||||
}, [dockedTo, dockParentExpanded, dockParentCard?.x, dockParentCard?.y, dockParentCard?.width, dockParentCard?.height, getCanvasState, dockParentCard]);
|
||||
}, [dockedTo, dockParentExpanded, dockParentTiled, dockParentCard?.x, dockParentCard?.y, dockParentCard?.width, dockParentCard?.height, getCanvasState, dockParentCard]);
|
||||
const dockParentZ = dockParentCard?.zOrder ?? 0;
|
||||
|
||||
const suspendedSnap = useAppSelector((state) => state.dashboardLayout.suspendedBrowserCards[browserId]);
|
||||
@@ -1032,7 +1042,7 @@ const BrowserCard: React.FC<Props> = ({
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
zIndex: tiledStyle ? 999990 : (isDragging || isResizing) ? 999999 : dockActive ? dockParentZ + 1 : cardZOrder,
|
||||
zIndex: tiledStyle ? 999990 : (isDragging || isResizing) ? 999999 : dockActive ? (dockParentTiled ? 999991 : dockParentZ + 1) : cardZOrder,
|
||||
transition: noTransition ? 'none' : 'box-shadow 0.4s ease, border 0.3s ease',
|
||||
'&:hover .resize-handle': { opacity: 1 },
|
||||
...(isHighlighted && {
|
||||
|
||||
@@ -173,6 +173,7 @@ const DashboardViewCard: React.FC<Props> = ({
|
||||
const dockedTo = useAppSelector((state) => state.dashboardLayout.viewCards[cardKey]?.docked_to ?? null);
|
||||
const dockParentCard = useAppSelector((state) => (dockedTo ? state.dashboardLayout.cards[dockedTo] ?? null : null));
|
||||
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);
|
||||
const dockRootRef = useRef<HTMLDivElement | null>(null);
|
||||
useEffect(() => {
|
||||
@@ -192,8 +193,17 @@ const DashboardViewCard: React.FC<Props> = ({
|
||||
if (slot) ro.observe(slot);
|
||||
if (slot?.parentElement) ro.observe(slot.parentElement);
|
||||
window.addEventListener('resize', measure);
|
||||
return () => { ro.disconnect(); window.removeEventListener('resize', measure); };
|
||||
}, [dockedTo, dockParentExpanded, dockParentCard?.x, dockParentCard?.y, dockParentCard?.width, dockParentCard?.height, getCanvasState, dockParentCard]);
|
||||
// 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.
|
||||
window.addEventListener('openswarm:canvas-pan-changed', measure);
|
||||
const timers = [60, 250, 700].map((ms) => window.setTimeout(measure, ms));
|
||||
return () => {
|
||||
ro.disconnect();
|
||||
window.removeEventListener('resize', measure);
|
||||
window.removeEventListener('openswarm:canvas-pan-changed', measure);
|
||||
timers.forEach((tm) => window.clearTimeout(tm));
|
||||
};
|
||||
}, [dockedTo, dockParentExpanded, dockParentTiled, dockParentCard?.x, dockParentCard?.y, dockParentCard?.width, dockParentCard?.height, getCanvasState, dockParentCard]);
|
||||
const dockParentZ = dockParentCard?.zOrder ?? 0;
|
||||
|
||||
// Keep the live preview mounted only when the user can actually see/use this app card. Always live
|
||||
@@ -628,7 +638,7 @@ const DashboardViewCard: React.FC<Props> = ({
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
zIndex: tiledStyle ? 999990 : (isDragging || isResizing) ? 999999 : cardZOrder,
|
||||
zIndex: tiledStyle ? 999990 : (isDragging || isResizing) ? 999999 : dockActive ? (dockParentTiled ? 999991 : dockParentZ + 1) : cardZOrder,
|
||||
transition: noTransition ? 'none' : 'box-shadow 0.4s ease, border 0.3s ease',
|
||||
'&:hover .resize-handle': { opacity: 1 },
|
||||
...(isHighlighted && {
|
||||
|
||||
@@ -108,27 +108,65 @@ export function useTiledStyle(
|
||||
if (!zone || !getLive || !selectId) return undefined;
|
||||
const el = document.querySelector(`[data-select-id="${CSS.escape(selectId)}"]`) as HTMLElement | null;
|
||||
if (!el) return undefined;
|
||||
const props = ['left', 'top', 'width', 'height', 'transform', 'transform-origin'];
|
||||
// left/top belong on whichever element OWNS position: AgentCard splits into a position:absolute
|
||||
// motion wrapper around a position:relative inner Box (writing left/top on the inner Box ADDS to
|
||||
// the wrapper's, rendering the tile at exactly double the offset); Browser/View cards are one
|
||||
// element. Size + counter-scale always live on the select-id element, mirroring the sx path.
|
||||
const posEl = getComputedStyle(el).position === 'absolute' || getComputedStyle(el).position === 'fixed'
|
||||
? el
|
||||
: (el.parentElement as HTMLElement | null) ?? el;
|
||||
const posProps = ['left', 'top'];
|
||||
const sizeProps = ['width', 'height', 'transform', 'transform-origin'];
|
||||
const clearAll = (): void => {
|
||||
posProps.forEach((pr) => posEl.style.removeProperty(pr));
|
||||
sizeProps.forEach((pr) => el.style.removeProperty(pr));
|
||||
};
|
||||
const apply = (): void => {
|
||||
// A parked card (kept alive off-screen / minimized) must keep its sx parking position.
|
||||
if (el.getAttribute('data-keepalive-hidden') === '1') { props.forEach((pr) => el.style.removeProperty(pr)); return; }
|
||||
if (el.getAttribute('data-keepalive-hidden') === '1') { clearAll(); return; }
|
||||
const cam = getLive();
|
||||
const s = computeTiledStyle(zone, cam.panX, cam.panY, cam.zoom);
|
||||
if (!s) return;
|
||||
el.style.left = `${s.left}px`;
|
||||
el.style.top = `${s.top}px`;
|
||||
el.style.width = `${s.width}px`;
|
||||
el.style.height = `${s.height}px`;
|
||||
el.style.transform = s.transform;
|
||||
el.style.transformOrigin = s.transformOrigin;
|
||||
// 'important' so framer-motion's own left/top writes (computed off the LAGGING React-committed
|
||||
// camera) can't land after us and shift the tile by the stale-camera delta.
|
||||
posEl.style.setProperty('left', `${s.left}px`, 'important');
|
||||
posEl.style.setProperty('top', `${s.top}px`, 'important');
|
||||
el.style.setProperty('width', `${s.width}px`, 'important');
|
||||
el.style.setProperty('height', `${s.height}px`, 'important');
|
||||
el.style.setProperty('transform', s.transform, 'important');
|
||||
el.style.setProperty('transform-origin', s.transformOrigin, 'important');
|
||||
};
|
||||
apply();
|
||||
window.addEventListener('openswarm:canvas-pan-changed', apply);
|
||||
window.addEventListener('resize', apply);
|
||||
// The workspace can change size WITHOUT a window resize (chrome collapsing on fullscreen-enter,
|
||||
// a banner appearing). These inline writes OVERRIDE the class styles, so they must re-measure on
|
||||
// the same signals the React path uses or the tile keeps the pre-collapse viewport (the
|
||||
// fullscreen card that stopped short of the bottom edge).
|
||||
const vp = document.querySelector('[data-canvas-viewport]');
|
||||
const ro = new ResizeObserver(apply);
|
||||
if (vp) ro.observe(vp);
|
||||
const timers = [60, 250, 700].map((ms) => window.setTimeout(apply, ms));
|
||||
// animateTo moves the camera WITHOUT emitting pan-changed, and the important-priority writes
|
||||
// above block React's late self-heal, so a tiled card must track the live camera itself:
|
||||
// a write-on-change rAF loop (frozen while hidden is fine; nothing moves visually then).
|
||||
let raf = 0;
|
||||
let lastKey = '';
|
||||
const tick = (): void => {
|
||||
const cam = getLive();
|
||||
const size = workspaceSize();
|
||||
const key = `${cam.panX}:${cam.panY}:${cam.zoom}:${size.w}:${size.h}`;
|
||||
if (key !== lastKey) { lastKey = key; apply(); }
|
||||
raf = window.requestAnimationFrame(tick);
|
||||
};
|
||||
raf = window.requestAnimationFrame(tick);
|
||||
return () => {
|
||||
window.removeEventListener('openswarm:canvas-pan-changed', apply);
|
||||
window.removeEventListener('resize', apply);
|
||||
props.forEach((pr) => el.style.removeProperty(pr));
|
||||
ro.disconnect();
|
||||
timers.forEach((tm) => window.clearTimeout(tm));
|
||||
window.cancelAnimationFrame(raf);
|
||||
clearAll();
|
||||
};
|
||||
}, [zone, getLive, selectId]);
|
||||
return zone ? computeTiledStyle(zone, panX, panY, zoom) : null;
|
||||
|
||||
Reference in New Issue
Block a user