From 1b0d34b026f645debf57c4d57e332627f71e1f48 Mon Sep 17 00:00:00 2001 From: Arnav Date: Sat, 25 Apr 2026 19:32:45 -0500 Subject: [PATCH] performance updates --- .../src/app/components/Layout/AppShell.tsx | 2 +- .../src/app/components/SelectionOverlay.tsx | 42 ++++++++++++-- .../src/app/pages/Dashboard/AgentCard.tsx | 33 +++++++---- .../src/app/pages/Dashboard/Dashboard.tsx | 55 ++++++++++++++----- .../src/shared/state/dashboardLayoutSlice.ts | 29 ++++++---- 5 files changed, 120 insertions(+), 41 deletions(-) diff --git a/frontend/src/app/components/Layout/AppShell.tsx b/frontend/src/app/components/Layout/AppShell.tsx index 2f700a6b..37a5f0ec 100644 --- a/frontend/src/app/components/Layout/AppShell.tsx +++ b/frontend/src/app/components/Layout/AppShell.tsx @@ -170,7 +170,7 @@ const AppShell: React.FC = () => { return; } } - dispatch(addBrowserCard({ url })); + dispatch(addBrowserCard({ url, viewportWidth: window.innerWidth })); } else { dispatch(setPendingBrowserUrl(url)); const lastId = (window as any).__openswarm_last_dashboard_id as string | undefined; diff --git a/frontend/src/app/components/SelectionOverlay.tsx b/frontend/src/app/components/SelectionOverlay.tsx index 51e534e4..2be12f27 100644 --- a/frontend/src/app/components/SelectionOverlay.tsx +++ b/frontend/src/app/components/SelectionOverlay.tsx @@ -45,7 +45,7 @@ const SelectionOverlay: React.FC = ({ overlay, dragRect, dragPreview = [] return; } - const updateRects = () => { + const measureRects = () => { const rects: PersistentRect[] = []; for (const sel of semanticEls) { try { @@ -65,13 +65,47 @@ const SelectionOverlay: React.FC = ({ overlay, dragRect, dragPreview = [] // selector might be invalid } } - setPersistentRects(rects); - rafRef.current = requestAnimationFrame(updateRects); + + setPersistentRects((prev) => { + if (prev.length !== rects.length) return rects; + for (let i = 0; i < rects.length; i++) { + const a = prev[i], b = rects[i]; + if (a.id !== b.id || a.top !== b.top || a.left !== b.left || + a.width !== b.width || a.height !== b.height) return rects; + } + return prev; + }); }; - rafRef.current = requestAnimationFrame(updateRects); + measureRects(); + + const scheduleUpdate = () => { + if (rafRef.current) cancelAnimationFrame(rafRef.current); + rafRef.current = requestAnimationFrame(measureRects); + }; + + window.addEventListener('scroll', scheduleUpdate, true); + window.addEventListener('resize', scheduleUpdate); + + // Observe size changes on the selected elements specifically. We + // deliberately avoid a document-wide MutationObserver here — that would + // fire on every streamed chat token (and many other unrelated mutations) + // and effectively re-create the runaway loop we're replacing. + const ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(scheduleUpdate) : null; + if (ro) { + for (const sel of semanticEls) { + try { + const domEl = document.querySelector(sel.selectorPath); + if (domEl) ro.observe(domEl); + } catch { /* selector might be invalid */ } + } + } + return () => { if (rafRef.current) cancelAnimationFrame(rafRef.current); + window.removeEventListener('scroll', scheduleUpdate, true); + window.removeEventListener('resize', scheduleUpdate); + if (ro) ro.disconnect(); }; }, [ctx?.selectedElements]); diff --git a/frontend/src/app/pages/Dashboard/AgentCard.tsx b/frontend/src/app/pages/Dashboard/AgentCard.tsx index f2d35332..0f0c8d38 100644 --- a/frontend/src/app/pages/Dashboard/AgentCard.tsx +++ b/frontend/src/app/pages/Dashboard/AgentCard.tsx @@ -109,6 +109,27 @@ function getAgentWorkTime(messages: Array<{ role: string; timestamp: string }>, return { total, last }; } +const ElapsedTime: React.FC<{ + messages: Array<{ role: string; timestamp: string }>; + status: string; + sx?: Record; +}> = React.memo(({ messages, status, sx }) => { + const [, setTick] = useState(0); + const isLive = status === 'running' || status === 'waiting_approval'; + + useEffect(() => { + if (!isLive) return; + const interval = setInterval(() => setTick((t) => t + 1), 1000); + return () => clearInterval(interval); + }, [isLive]); + + return ( + + {fmtSeconds(getAgentWorkTime(messages, status).last)} + + ); +}); + function summarizeToolInput(toolName: string, toolInput: Record): string { const mcp = parseMcpToolName(toolName); if (mcp.isMcp) { @@ -295,7 +316,6 @@ const AgentCard: React.FC = ({ draft: { color: c.accent.primary, bg: c.bg.secondary }, }; - const [, setTick] = useState(0); const isDraft = session.status === 'draft'; // ---- Drag via header (pointer events) ---- @@ -485,13 +505,6 @@ const AgentCard: React.FC = ({ }; - useEffect(() => { - if (session.status === 'running' || session.status === 'waiting_approval') { - const interval = setInterval(() => setTick((t) => t + 1), 1000); - return () => clearInterval(interval); - } - }, [session.status]); - const lastMessage = session.messages[session.messages.length - 1]; const isStreaming = !!session.streamingMessage; const previewContent = isStreaming @@ -881,9 +894,7 @@ const AgentCard: React.FC = ({ {session.mode} - - {fmtSeconds(getAgentWorkTime(session.messages, session.status).last)} - + {session.cost_usd > 0 && hasApiKey && ( ${session.cost_usd.toFixed(4)} diff --git a/frontend/src/app/pages/Dashboard/Dashboard.tsx b/frontend/src/app/pages/Dashboard/Dashboard.tsx index 4f2ef27a..e1bd607a 100644 --- a/frontend/src/app/pages/Dashboard/Dashboard.tsx +++ b/frontend/src/app/pages/Dashboard/Dashboard.tsx @@ -115,6 +115,38 @@ const DashboardInner: React.FC = ({ dashboardId, isActive = true const glowingBrowserCards = useAppSelector((state) => state.dashboardLayout.glowingBrowserCards); const sessionList = Object.values(sessions); + // Reference-stable list of browser-agent sessions that need tethers. + // Without this, the tether useMemo below recomputes ~190 lines of SVG + // path math on every streamed token in any chat, because `sessionList` + // is a fresh array on every Dashboard re-render. + const browserAgentTetherSeedsRef = useRef< + Array<{ id: string; browser_id: string; parent_session_id: string }> + >([]); + const browserAgentTetherSeeds = useMemo(() => { + const next: Array<{ id: string; browser_id: string; parent_session_id: string }> = []; + for (const s of Object.values(sessions)) { + if (s.mode !== 'browser-agent') continue; + if (s.status !== 'running' && s.status !== 'waiting_approval') continue; + if (!s.browser_id || !s.parent_session_id) continue; + next.push({ id: s.id, browser_id: s.browser_id, parent_session_id: s.parent_session_id }); + } + const prev = browserAgentTetherSeedsRef.current; + if (prev.length === next.length) { + let same = true; + for (let i = 0; i < prev.length; i++) { + if (prev[i].id !== next[i].id + || prev[i].browser_id !== next[i].browser_id + || prev[i].parent_session_id !== next[i].parent_session_id) { + same = false; + break; + } + } + if (same) return prev; + } + browserAgentTetherSeedsRef.current = next; + return next; + }, [sessions]); + const contentBounds = useMemo(() => { const allRects = [ ...Object.values(cards).map((c) => ({ x: c.x, y: c.y, w: c.width, h: c.height })), @@ -483,7 +515,7 @@ const DashboardInner: React.FC = ({ dashboardId, isActive = true useEffect(() => { if (!pendingBrowserUrl || !layoutInitialized) return; - dispatch(addBrowserCard({ url: pendingBrowserUrl, expandedSessionIds })); + dispatch(addBrowserCard({ url: pendingBrowserUrl, expandedSessionIds, viewportWidth: window.innerWidth })); dispatch(clearPendingBrowserUrl()); }, [pendingBrowserUrl, layoutInitialized, dispatch, expandedSessionIds]); @@ -605,7 +637,7 @@ const DashboardInner: React.FC = ({ dashboardId, isActive = true const liveIds = dashboardSessionIds.sort().join(','); if (liveIds === prevSessionIdsRef.current) return; prevSessionIdsRef.current = liveIds; - dispatch(reconcileSessions({ sessionIds: dashboardSessionIds, expandedSessionIds })); + dispatch(reconcileSessions({ sessionIds: dashboardSessionIds, expandedSessionIds, viewportWidth: window.innerWidth })); }, [sessions, layoutInitialized, dispatch, dashboardId, expandedSessionIds]); // ---- Auto-reveal / collapse / unreveal sub-agent cards ---- @@ -1241,7 +1273,7 @@ const DashboardInner: React.FC = ({ dashboardId, isActive = true ); const handleAddView = useCallback((outputId: string) => { - dispatch(addViewCard({ outputId, expandedSessionIds })); + dispatch(addViewCard({ outputId, expandedSessionIds, viewportWidth: window.innerWidth })); setTimeout(() => { const card = store.getState().dashboardLayout.viewCards[outputId]; if (card) { @@ -1254,7 +1286,7 @@ const DashboardInner: React.FC = ({ dashboardId, isActive = true const handleAddBrowser = useCallback(() => { trackEvent('dashboard.browser_added'); const prevIds = new Set(Object.keys(store.getState().dashboardLayout.browserCards)); - dispatch(addBrowserCard({ url: browserHomepage, expandedSessionIds })); + dispatch(addBrowserCard({ url: browserHomepage, expandedSessionIds, viewportWidth: window.innerWidth })); setTimeout(() => { const allBrowserCards = store.getState().dashboardLayout.browserCards; const newId = Object.keys(allBrowserCards).find((id) => !prevIds.has(id)); @@ -1299,7 +1331,7 @@ const DashboardInner: React.FC = ({ dashboardId, isActive = true const handleTidy = useCallback(() => { trackEvent('dashboard.tidy_layout'); const currentExpanded = store.getState().agents.expandedSessionIds; - dispatch(tidyLayout({ expandedSessionIds: currentExpanded })); + dispatch(tidyLayout({ expandedSessionIds: currentExpanded, viewportWidth: window.innerWidth })); const expandedSet = new Set(currentExpanded); const { cards: tidied, viewCards: tidiedViews, browserCards: tidiedBrowsers } = store.getState().dashboardLayout; @@ -1563,20 +1595,17 @@ const DashboardInner: React.FC = ({ dashboardId, isActive = true } // Source 2: active browser-agent sessions (persistent — survives parent turn completion) - for (const s of sessionList) { - if (s.mode !== 'browser-agent') continue; - if (s.status !== 'running' && s.status !== 'waiting_approval') continue; - if (!s.browser_id || !s.parent_session_id) continue; - if (glowTethers.has(s.browser_id)) continue; // glow already covers this one - const t = browserTether(s.browser_id, s.parent_session_id, false, ''); - if (t) glowTethers.set(s.browser_id, t); + for (const seed of browserAgentTetherSeeds) { + if (glowTethers.has(seed.browser_id)) continue; // glow already covers this one + const t = browserTether(seed.browser_id, seed.parent_session_id, false, ''); + if (t) glowTethers.set(seed.browser_id, t); } const browserTethers = Array.from(glowTethers.values()).filter(Boolean) as Array<{ key: string; path: string; labelX: number; labelY: number; label: string; fading: boolean }>; return [...agentTethers, ...browserTethers]; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [glowingAgentCards, glowingBrowserCards, cards, browserCards, expandedSessionIds, liveDragInfo, measuredHeightsTick, sessionList]); + }, [glowingAgentCards, glowingBrowserCards, cards, browserCards, expandedSessionIds, liveDragInfo, measuredHeightsTick, browserAgentTetherSeeds]); const dotSize = Math.max(1, 1.5 * canvas.zoom); const dotSpacing = 24 * canvas.zoom; diff --git a/frontend/src/shared/state/dashboardLayoutSlice.ts b/frontend/src/shared/state/dashboardLayoutSlice.ts index a7f05fc3..63603b85 100644 --- a/frontend/src/shared/state/dashboardLayoutSlice.ts +++ b/frontend/src/shared/state/dashboardLayoutSlice.ts @@ -185,12 +185,14 @@ export function findOpenGridCell( occupiedRects: Rect[], newW: number, newH: number, + viewportWidth?: number, ): { x: number; y: number } { const cellW = DEFAULT_CARD_W + GRID_GAP; const cellH = DEFAULT_CARD_H + GRID_GAP; + const vw = viewportWidth ?? (typeof window !== 'undefined' ? window.innerWidth : 0); const maxCols = Math.max( 1, - Math.floor((window.innerWidth - GRID_ORIGIN.x) / cellW) || GRID_COLS_FALLBACK, + Math.floor((vw - GRID_ORIGIN.x) / cellW) || GRID_COLS_FALLBACK, ); for (let row = 0; ; row++) { @@ -265,9 +267,9 @@ const dashboardLayoutSlice = createSlice({ reconcileSessions( state, - action: PayloadAction<{ sessionIds: string[]; expandedSessionIds: string[] }>, + action: PayloadAction<{ sessionIds: string[]; expandedSessionIds: string[]; viewportWidth?: number }>, ) { - const { sessionIds, expandedSessionIds } = action.payload; + const { sessionIds, expandedSessionIds, viewportWidth } = action.payload; const liveIds = new Set(sessionIds); for (const id of Object.keys(state.cards)) { @@ -287,7 +289,7 @@ const dashboardLayoutSlice = createSlice({ delete state.closedCardPositions[id]; } else { const rects = collectOccupiedRects(state, expandedSessionIds); - const pos = findOpenGridCell(rects, DEFAULT_CARD_W, DEFAULT_CARD_H); + const pos = findOpenGridCell(rects, DEFAULT_CARD_W, DEFAULT_CARD_H, viewportWidth); state.cards[id] = { session_id: id, x: pos.x, @@ -302,8 +304,9 @@ const dashboardLayoutSlice = createSlice({ tidyLayout( state, - action: PayloadAction<{ expandedSessionIds: string[] }>, + action: PayloadAction<{ expandedSessionIds: string[]; viewportWidth?: number }>, ) { + const { viewportWidth } = action.payload; const expanded = new Set(action.payload.expandedSessionIds); const agentCards = Object.values(state.cards); const viewCards = Object.values(state.viewCards); @@ -330,7 +333,7 @@ const dashboardLayoutSlice = createSlice({ h = item.storedH; } - const pos = findOpenGridCell(placedRects, w, h); + const pos = findOpenGridCell(placedRects, w, h, viewportWidth); placedRects.push({ x: pos.x, y: pos.y, w, h }); if (item.kind === 'agent') { @@ -349,8 +352,9 @@ const dashboardLayoutSlice = createSlice({ addViewCard(state, action: PayloadAction<{ outputId: string; expandedSessionIds?: string[]; x?: number; y?: number; width?: number; height?: number; + viewportWidth?: number; }>) { - const { outputId, expandedSessionIds, x, y, width, height } = action.payload; + const { outputId, expandedSessionIds, x, y, width, height, viewportWidth } = action.payload; if (state.viewCards[outputId]) return; let posX: number, posY: number; if (x != null && y != null) { @@ -358,7 +362,7 @@ const dashboardLayoutSlice = createSlice({ posY = y; } else { const rects = collectOccupiedRects(state, expandedSessionIds); - const pos = findOpenGridCell(rects, DEFAULT_VIEW_CARD_W, DEFAULT_VIEW_CARD_H); + const pos = findOpenGridCell(rects, DEFAULT_VIEW_CARD_W, DEFAULT_VIEW_CARD_H, viewportWidth); posX = pos.x; posY = pos.y; } @@ -397,11 +401,11 @@ const dashboardLayoutSlice = createSlice({ delete state.viewCards[action.payload]; }, - addBrowserCard(state, action: PayloadAction<{ url: string; expandedSessionIds?: string[] }>) { + addBrowserCard(state, action: PayloadAction<{ url: string; expandedSessionIds?: string[]; viewportWidth?: number }>) { const id = `browser-${Date.now().toString(36)}`; const tabId = generateTabId(); const rects = collectOccupiedRects(state, action.payload.expandedSessionIds); - const pos = findOpenGridCell(rects, DEFAULT_BROWSER_CARD_W, DEFAULT_BROWSER_CARD_H); + const pos = findOpenGridCell(rects, DEFAULT_BROWSER_CARD_W, DEFAULT_BROWSER_CARD_H, action.payload.viewportWidth); state.browserCards[id] = { browser_id: id, url: action.payload.url, @@ -462,9 +466,10 @@ const dashboardLayoutSlice = createSlice({ action: PayloadAction<{ tabs: BrowserTab[]; url: string; expandedSessionIds?: string[]; id?: string; x?: number; y?: number; width?: number; height?: number; + viewportWidth?: number; }> ) { - const { x, y, width, height } = action.payload; + const { x, y, width, height, viewportWidth } = action.payload; const id = action.payload.id || `browser-${Date.now().toString(36)}`; const newTabs = action.payload.tabs.map((t) => ({ id: generateTabId(), @@ -479,7 +484,7 @@ const dashboardLayoutSlice = createSlice({ posY = y; } else { const rects = collectOccupiedRects(state, action.payload.expandedSessionIds); - const pos = findOpenGridCell(rects, DEFAULT_BROWSER_CARD_W, DEFAULT_BROWSER_CARD_H); + const pos = findOpenGridCell(rects, DEFAULT_BROWSER_CARD_W, DEFAULT_BROWSER_CARD_H, viewportWidth); posX = pos.x; posY = pos.y; }