From d79bcda32c894a6188309ac9e3b4b3778ace92f0 Mon Sep 17 00:00:00 2001 From: NiveditJain Date: Tue, 5 May 2026 11:42:13 -0700 Subject: [PATCH] [luv-292] fix: keep virtualizer scrollMargin in sync with layout shifts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The session log viewer's `useWindowVirtualizer` captured `scrollMargin` once via a callback ref on the list wrapper's `offsetTop`. The ref only fires on mount, so any layout change above the list — async hook-activity panel mounting, subagent collapse/expand, window resize — left the value stale. The virtualizer then mapped window scroll → list coords with the wrong offset, positioning each item at the wrong y-offset and exposing the page `--background` (#031035) under the otherwise empty container, giving the "page goes blue while scrolling" symptom. Replace the callback ref with a stable `useRef` and a `useLayoutEffect` that re-measures via `getBoundingClientRect().top + window.scrollY` from a `ResizeObserver` on `document.body` plus a window resize listener. Functional `setScrollMargin` short-circuits same-value updates so the body resize the update itself causes can't loop. Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 1 + app/components/raw-log-viewer.tsx | 30 +++++++++++++++++++++++++----- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ad121ef..23465efc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased ### Fixes +- Session log viewer: stop rendering log entries at the wrong y-offset (which exposed the page background and looked like the page "going blue" while scrolling). `app/components/raw-log-viewer.tsx` was capturing the virtualizer's `scrollMargin` once via a callback ref on the list wrapper's `offsetTop`. That ref fires only on mount, so any layout shift above the list — most commonly the async `searchHookActivityAction` resolving and mounting the `` panel above the Logs section, but also Subagents-section / per-subagent collapse-expand, and window resizes that re-flow the StatsBar / ToolStatsGrid — left `scrollMargin` stale. The `useWindowVirtualizer` then computed the wrong visible window in list-local coordinates and positioned each item at `transform: translateY(virtualRow.start - staleScrollMargin)`, so items appeared shifted by the layout-delta (typically tens to hundreds of pixels). Replace the callback ref with a stable `useRef` and a `useLayoutEffect` that reads `getBoundingClientRect().top + window.scrollY` (more robust than `offsetTop` against future positioned ancestors) and re-reads it from a `ResizeObserver` watching `document.body` plus a `window` resize listener. Functional `setScrollMargin(prev => prev === top ? prev : top)` short-circuits same-value updates so the body-resize that the state-update itself causes can't loop. (#292) - Detect when `failproofai` on the user's PATH is shadowed by a different, older install (classic cause: a leftover `bun link` from a prior dev session, or a previously-installed `bun install -g failproofai` whose `~/.bun/bin` prefix sorts ahead of npm's). New `scripts/install-diagnosis.mjs` helper resolves the PATH-first install via `command -v` (POSIX) / `where` (Win32), compares its package root + version against the running install, and surfaces a copy-pasteable cleanup command (`rm -f ~/.bun/bin/failproofai && rm -rf ~/.bun/install/global/node_modules/failproofai` for bun-side shadows, `npm rm -g failproofai` for npm-side ones). Wired into two places: (1) `scripts/postinstall.mjs` warns at install time when the just-installed copy is being shadowed, before the customer ever sees the runtime error, (2) `scripts/launch.ts` rewrites the existing "Cannot find server.js at" error to point at the actual stale install (with both versions and the cleanup command) when the missing build output is caused by a PATH shadow rather than a genuinely broken build. Replaces the previous misleading recommendation (`npm install -g failproofai@latest`) which doesn't help when the new install is itself being shadowed (#286). ## 0.0.10-beta.0 — 2026-05-04 diff --git a/app/components/raw-log-viewer.tsx b/app/components/raw-log-viewer.tsx index a1c5e0c6..2f1d53d3 100644 --- a/app/components/raw-log-viewer.tsx +++ b/app/components/raw-log-viewer.tsx @@ -5,7 +5,7 @@ */ "use client"; -import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { useWindowVirtualizer } from "@tanstack/react-virtual"; import { ChevronDown, Wrench } from "lucide-react"; import type { LogEntry, ToolUseBlock } from "@/lib/log-entries"; @@ -209,9 +209,29 @@ function VirtualizedEntryList({ entries, entriesBySource, projectName, sessionId const [highlightedUuid, setHighlightedUuid] = useState(() => parseHashUuid()); const highlightTimerRef = useRef>(undefined); const hasScrolledRef = useRef(false); - - const listCallbackRef = useCallback((node: HTMLDivElement | null) => { - if (node) setScrollMargin(node.offsetTop); + const listRef = useRef(null); + + // Keep `scrollMargin` in sync with the list's actual document offset. + // A one-shot callback ref would go stale whenever something above the list + // shifts (async hook-activity panel mounting, subagent collapse/expand, + // window resize) — leaving the virtualizer to render items at the wrong + // y-offset and exposing the page background under the (otherwise empty) + // virtualized container. + useLayoutEffect(() => { + const el = listRef.current; + if (!el) return; + const update = () => { + const top = el.getBoundingClientRect().top + window.scrollY; + setScrollMargin((prev) => (prev === top ? prev : top)); + }; + update(); + const ro = new ResizeObserver(update); + ro.observe(document.body); + window.addEventListener("resize", update); + return () => { + ro.disconnect(); + window.removeEventListener("resize", update); + }; }, []); const segments = useMemo(() => computeSegments(entries), [entries]); @@ -337,7 +357,7 @@ function VirtualizedEntryList({ entries, entriesBySource, projectName, sessionId }, [highlightedUuid, parentUuidForSubagent, uuidToIndex, virtualizer]); return ( -
+