diff --git a/frontend/src/app/pages/Dashboard/Dashboard.tsx b/frontend/src/app/pages/Dashboard/Dashboard.tsx index 4c5d4975..e4b587b3 100644 --- a/frontend/src/app/pages/Dashboard/Dashboard.tsx +++ b/frontend/src/app/pages/Dashboard/Dashboard.tsx @@ -907,6 +907,38 @@ const DashboardInner: React.FC = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [expandedSessionIds, glowingAgentCards, cards, dispatch, measuredHeightsTick]); + useEffect(() => { + const DRIFT_THRESHOLD = 60; + + const sourceToSiblings = new Map(); + for (const [browserId, glow] of Object.entries(glowingBrowserCards)) { + const bc = browserCards[browserId]; + if (!bc) continue; + const sourceCard = cards[glow.sourceId]; + if (!sourceCard) continue; + const expectedX = sourceCard.x + sourceCard.width + GRID_GAP * 12; + if (Math.abs(bc.x - expectedX) > DRIFT_THRESHOLD) continue; + const list = sourceToSiblings.get(glow.sourceId) ?? []; + list.push(browserId); + sourceToSiblings.set(glow.sourceId, list); + } + + for (const siblings of sourceToSiblings.values()) { + if (siblings.length < 2) continue; + siblings.sort((a, b) => browserCards[a].y - browserCards[b].y); + + let cursor = browserCards[siblings[0]].y; + for (const id of siblings) { + const bc = browserCards[id]; + const dy = cursor - bc.y; + if (Math.abs(dy) > 1) { + dispatch(moveCards({ items: [{ id, type: 'browser' as const }], dx: 0, dy })); + } + cursor += bc.height + GRID_GAP * 2; + } + } + }, [glowingBrowserCards, browserCards, cards, dispatch]); + const TETHER_FADE_MS = 2500; const tethers = useMemo(() => { diff --git a/frontend/src/shared/ws/WebSocketManager.ts b/frontend/src/shared/ws/WebSocketManager.ts index acd8e14f..9c4adc53 100644 --- a/frontend/src/shared/ws/WebSocketManager.ts +++ b/frontend/src/shared/ws/WebSocketManager.ts @@ -244,10 +244,19 @@ class WebSocketManager { const layoutState = store.getState().dashboardLayout; const parentCard = layoutState.cards[parentId]; if (parentCard) { + const targetX = parentCard.x + parentCard.width + GRID_GAP * 12; + let targetY = parentCard.y; + const columnCards = Object.values(layoutState.browserCards).filter( + (c) => Math.abs(c.x - targetX) < 50 && c.browser_id !== data.browser_card.browser_id, + ); + if (columnCards.length > 0) { + const lowestBottom = Math.max(...columnCards.map((c) => c.y + c.height)); + targetY = lowestBottom + GRID_GAP; + } store.dispatch(setBrowserCardPosition({ browserId: data.browser_card.browser_id, - x: parentCard.x + parentCard.width + GRID_GAP * 12, - y: parentCard.y, + x: targetX, + y: targetY, })); store.dispatch(setGlowingBrowserCards({ browserIds: [data.browser_card.browser_id],