From c404ef15f917d44b787753db271ebe32e9fb48eb Mon Sep 17 00:00:00 2001 From: ciregenz Date: Mon, 17 Aug 2026 12:10:48 -0700 Subject: [PATCH] Revert "[eric] canvas: a render ring keeps everything near the camera pre-drawn so pans land on warm pixels (ENG-301)" This reverts commit c854d353c064e6bfb6b8adaccbe8722c9b564bc5. --- .../app/pages/Dashboard/cards/AgentCard.tsx | 7 +- .../hooks/interaction/useRenderRing.ts | 91 ------------------- 2 files changed, 1 insertion(+), 97 deletions(-) delete mode 100644 frontend/src/app/pages/Dashboard/hooks/interaction/useRenderRing.ts diff --git a/frontend/src/app/pages/Dashboard/cards/AgentCard.tsx b/frontend/src/app/pages/Dashboard/cards/AgentCard.tsx index 2e2d2742..d3c41313 100644 --- a/frontend/src/app/pages/Dashboard/cards/AgentCard.tsx +++ b/frontend/src/app/pages/Dashboard/cards/AgentCard.tsx @@ -55,7 +55,6 @@ import { useClaudeTokens, DarkTokensScope } from '@/shared/styles/ThemeContext'; import { GLASS_SURFACE, GLASS_SURFACE_BLUR, GLASS_SURFACE_TEXT } from '@/shared/styles/glassSurface'; import { useDashboardActive } from '@/shared/hooks/useDashboardActive'; import { useOverlayScrollPassthrough } from '../hooks/interaction/useOverlayScrollPassthrough'; -import { useRenderRing } from '../hooks/interaction/useRenderRing'; import { useStreamingMessage } from '@/shared/state/streamingSlice'; import { isCanvasInteractionActive, onCanvasInteractionEnd } from '@/shared/canvasInteractionState'; import { setCardSidecar } from '@/shared/state/workflowsSlice'; @@ -724,8 +723,6 @@ const AgentCard: React.FC = ({ ); // Drafts collapse to the pill like everything else; only a pending approval keeps the full card, since you have to see what you are approving. const pillMode = !expanded && !hasPending && !tileZone; - // Pre-render a ring of canvas around the camera so a pan lands on already-drawn pills (ENG-301). - const ringNear = useRenderRing(cardX, cardY, cardWidth, cardHeight, getCanvasState, pillMode && !isFullscreen); // Two-phase expand: mounting a long transcript synchronously inside the expand click blocked its paint for ~630ms (the measured INP worst case), so the click paints the expanded shell first and the chat mounts on the next frame. // Keep-alive: once mounted, the chat STAYS mounted across collapse (hidden, not unmounted). The transcript windowing bounds its kept DOM to ~a screen of bubbles, and re-expand becomes a display toggle instead of a full subtree rebuild + WS reconnect. @@ -884,9 +881,7 @@ const AgentCard: React.FC = ({ // expanded card is the one you are reading. contain-intrinsic-size keeps a skipped card's // box the size it would have been, so tethers and fit-to-view still measure it correctly. ...(pillMode && !expanded && !isFullscreen && !tiledSize ? { - // Inside the camera ring: force-render so arriving pans hit pre-drawn pixels (ENG-301, - // Eric's render-ahead call); outside it: keep the ENG-261 skip that holds big boards. - contentVisibility: ringNear ? 'visible' : 'auto', + contentVisibility: 'auto', // `auto` = remember the size this card last really rendered at, falling back to the guess // only before its first paint. A pill is fit-content/auto sized, so a fixed guess is wrong // for every card and costs a relayout each time one scrolls in: measured 27.8ms p95 with a diff --git a/frontend/src/app/pages/Dashboard/hooks/interaction/useRenderRing.ts b/frontend/src/app/pages/Dashboard/hooks/interaction/useRenderRing.ts deleted file mode 100644 index de2f8980..00000000 --- a/frontend/src/app/pages/Dashboard/hooks/interaction/useRenderRing.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; - -// Chromium only pre-renders content-visibility:auto elements a sliver past the viewport, so a pan -// always lands on never-drawn cards and pays first-raster right under the gesture (ENG-301's -// 100-700ms singles). Eric's call: keep a RING of canvas around the camera always rendered, so by -// the time you arrive it is already drawn. Cards inside the ring force 'visible'; far cards keep -// 'auto' (that skip is what holds 150-card boards at frame budget, ENG-261). Enter at ENTER_VP -// viewports, leave at LEAVE_VP, so boundary jitter never flips a card back and forth. -const ENTER_VP = 1.5; -const LEAVE_VP = 2.25; -// Newly-near cards render at most this many per frame: a fast pan crossing 30 cards must warm them -// over a few frames, not stall the gesture frame it happens in. -const PROMOTIONS_PER_FRAME = 3; - -let p_pending: Array<() => void> = []; -let p_drainScheduled = false; - -function p_drain(): void { - p_drainScheduled = false; - const batch = p_pending.splice(0, PROMOTIONS_PER_FRAME); - for (const fn of batch) fn(); - if (p_pending.length > 0) { - p_drainScheduled = true; - requestAnimationFrame(p_drain); - } -} - -function p_enqueuePromotion(fn: () => void): void { - p_pending.push(fn); - if (!p_drainScheduled) { - p_drainScheduled = true; - requestAnimationFrame(p_drain); - } -} - -export function useRenderRing( - cardX: number, - cardY: number, - cardWidth: number, - cardHeight: number, - getCanvasState: () => { panX: number; panY: number; zoom: number }, - active: boolean, -): boolean { - const [near, setNear] = useState(false); - const nearRef = useRef(false); - const rectRef = useRef({ x: cardX, y: cardY, w: cardWidth, h: cardHeight }); - rectRef.current = { x: cardX, y: cardY, w: cardWidth, h: cardHeight }; - - useEffect(() => { - if (!active) return undefined; - let raf = 0; - const evaluate = (): void => { - raf = 0; - const { panX, panY, zoom } = getCanvasState(); - const vw = window.innerWidth; - const vh = window.innerHeight; - // Viewport in canvas coordinates. - const vx = -panX / zoom; - const vy = -panY / zoom; - const vwC = vw / zoom; - const vhC = vh / zoom; - const marginVp = nearRef.current ? LEAVE_VP : ENTER_VP; - const mx = vwC * marginVp; - const my = vhC * marginVp; - const r = rectRef.current; - const isNear = - r.x + r.w > vx - mx && r.x < vx + vwC + mx && - r.y + r.h > vy - my && r.y < vy + vhC + my; - if (isNear !== nearRef.current) { - nearRef.current = isNear; - // Leaving the ring is free (back to skipped); entering pays a render, so it rides the - // per-frame promotion budget instead of landing all at once mid-gesture. - if (isNear) p_enqueuePromotion(() => setNear(true)); - else setNear(false); - } - }; - const schedule = (): void => { - if (!raf) raf = requestAnimationFrame(evaluate); - }; - schedule(); - window.addEventListener('openswarm:canvas-pan-changed', schedule); - window.addEventListener('resize', schedule); - return () => { - window.removeEventListener('openswarm:canvas-pan-changed', schedule); - window.removeEventListener('resize', schedule); - if (raf) cancelAnimationFrame(raf); - }; - }, [getCanvasState, active]); - - return near; -}