From e2e87e1b9db4849ddda88d5e265be7b9a3acd4ab Mon Sep 17 00:00:00 2001 From: tyulyukov Date: Sat, 28 Mar 2026 01:07:24 +0200 Subject: [PATCH] Debounce syntax highlighting and improve work row height estimation - Skip expensive highlighting when diff preview is collapsed - Debounce highlighting with 150ms delay to reduce redundant work - Calculate accurate heights for virtualized work rows based on visible inline diffs - This improves responsiveness and virtualizer accuracy during rapid interactions --- .../src/components/chat/InlineDiffPreview.tsx | 45 ++++++++++--------- .../src/components/chat/MessagesTimeline.tsx | 34 +++++++++++++- 2 files changed, 58 insertions(+), 21 deletions(-) diff --git a/apps/web/src/components/chat/InlineDiffPreview.tsx b/apps/web/src/components/chat/InlineDiffPreview.tsx index dfd8b63e44f1..3e68f4af6cbc 100644 --- a/apps/web/src/components/chat/InlineDiffPreview.tsx +++ b/apps/web/src/components/chat/InlineDiffPreview.tsx @@ -189,6 +189,7 @@ export const InlineDiffPreview = memo(function InlineDiffPreview(props: { hunk: const highlightVersionRef = useRef(0); useEffect(() => { + if (collapsed) return; const version = ++highlightVersionRef.current; const language = resolveLanguageFromPath(hunk.filePath); if (language === "text") return; @@ -206,28 +207,32 @@ export const InlineDiffPreview = memo(function InlineDiffPreview(props: { hunk: const code = codeFragments.join("\n"); const themeName = resolveDiffThemeName(resolvedTheme); - getHighlighterPromise(language) - .then((highlighter) => { - if (highlightVersionRef.current !== version) return; - try { - const html = highlighter.codeToHtml(code, { lang: language, theme: themeName }); - const extracted = extractLineHtmls(html); - if (extracted.length === codeFragments.length) { - const mapped: (string | null)[] = Array(hunk.lines.length).fill(null) as ( - | string - | null - )[]; - for (let i = 0; i < codeLineIndices.length; i++) { - mapped[codeLineIndices[i]!] = extracted[i]!; + const timerId = setTimeout(() => { + getHighlighterPromise(language) + .then((highlighter) => { + if (highlightVersionRef.current !== version) return; + try { + const html = highlighter.codeToHtml(code, { lang: language, theme: themeName }); + const extracted = extractLineHtmls(html); + if (extracted.length === codeFragments.length) { + const mapped: (string | null)[] = Array(hunk.lines.length).fill(null) as ( + | string + | null + )[]; + for (let i = 0; i < codeLineIndices.length; i++) { + mapped[codeLineIndices[i]!] = extracted[i]!; + } + setLineHtmls(mapped as string[]); } - setLineHtmls(mapped as string[]); + } catch { + // noop } - } catch { - // noop - } - }) - .catch(() => {}); - }, [hunk.filePath, hunk.lines, resolvedTheme]); + }) + .catch(() => {}); + }, 150); + + return () => clearTimeout(timerId); + }, [collapsed, hunk.filePath, hunk.lines, resolvedTheme]); const CollapseIcon = collapsed ? ChevronRightIcon : ChevronDownIcon; diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index 5d8d439158ba..677d8128d676 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -60,6 +60,38 @@ import { InlineDiffPreview } from "./InlineDiffPreview"; const MAX_VISIBLE_WORK_LOG_ENTRIES = 6; const ALWAYS_UNVIRTUALIZED_TAIL_ROWS = 8; +const WORK_ROW_BASE_HEIGHT = 112; +const DIFF_PREVIEW_HEADER_HEIGHT = 32; +const DIFF_PREVIEW_LINE_HEIGHT = 18; +const DIFF_PREVIEW_MAX_HEIGHT = 260; + +function estimateWorkRowHeight( + groupedEntries: ReadonlyArray, + showInlineDiffs: boolean, +): number { + if (!showInlineDiffs) return WORK_ROW_BASE_HEIGHT; + + let diffHeight = 0; + const visibleEntries = + groupedEntries.length > MAX_VISIBLE_WORK_LOG_ENTRIES + ? groupedEntries.slice(-MAX_VISIBLE_WORK_LOG_ENTRIES) + : groupedEntries; + + for (const entry of visibleEntries) { + const previews = entry.diffPreviews; + if (!previews || previews.length === 0) continue; + for (const hunk of previews) { + const bodyHeight = Math.min( + hunk.lines.length * DIFF_PREVIEW_LINE_HEIGHT, + DIFF_PREVIEW_MAX_HEIGHT, + ); + diffHeight += DIFF_PREVIEW_HEADER_HEIGHT + bodyHeight + 8; + } + } + + return WORK_ROW_BASE_HEIGHT + diffHeight; +} + interface MessagesTimelineProps { hasMessages: boolean; isWorking: boolean; @@ -255,7 +287,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({ estimateSize: (index: number) => { const row = rows[index]; if (!row) return 96; - if (row.kind === "work") return 112; + if (row.kind === "work") return estimateWorkRowHeight(row.groupedEntries, showInlineDiffs); if (row.kind === "proposed-plan") return estimateTimelineProposedPlanHeight(row.proposedPlan); if (row.kind === "working") return 40; return estimateTimelineMessageHeight(row.message, { timelineWidthPx });