Skip to content

Make chat timeline height estimates width-aware with attachment-aware tests#167

Merged
juliusmarminge merged 11 commits intomainfrom
t3code/fix-chat-attachment-height
Mar 5, 2026
Merged

Make chat timeline height estimates width-aware with attachment-aware tests#167
juliusmarminge merged 11 commits intomainfrom
t3code/fix-chat-attachment-height

Conversation

@juliusmarminge
Copy link
Copy Markdown
Member

@juliusmarminge juliusmarminge commented Mar 4, 2026

Summary

  • extract timeline row height estimation into timelineHeight.ts and reuse it from ChatView
  • make message height estimates responsive to measured timeline width via ResizeObserver
  • trigger virtualizer re-measure when timeline width changes to improve scroll/row sizing accuracy
  • add focused unit coverage for assistant/user sizing, attachment row behavior, newline handling, uncapped long messages, and width-dependent wrapping

Testing

  • Added/updated unit tests: apps/web/src/components/timelineHeight.test.ts
  • Concrete checks covered:
  • assistant sizing baseline behavior
  • user attachment row increments (1-2 vs 3-4 attachments)
  • long user message height growth (no cap)
  • explicit newline line counting
  • narrower vs wider width wrapping for both user and assistant messages
  • Not run: bun lint
  • Not run: bun typecheck
  • Not run: test suite execution in this PR description context

Note

Medium Risk
Changes ChatView virtualization sizing logic and introduces width-driven remeasurement, which can affect scrolling behavior and performance across viewports. Adds new Playwright/MSW browser-test infrastructure and CI steps that may introduce flakiness if timings or browser deps differ in CI.

Overview
Chat timeline virtualization now estimates row heights using measured width. The inline estimateTimelineMessageHeight logic is extracted to timelineHeight.ts, updated to account for wrapping based on timelineWidthPx (and attachment row sizing), and wired into MessagesTimeline via a ResizeObserver + rowVirtualizer.measure() on width changes.

Adds automated coverage and CI for real browser layout parity. Introduces unit tests for estimateTimelineMessageHeight, plus a Vitest+Playwright browser test (ChatView.browser.tsx) that boots the full app with MSW-mocked WS/HTTP and asserts measured DOM heights stay within tolerance across multiple viewport sizes and with attachments; CI now installs/caches Playwright Chromium and runs apps/web test:browser. Also factors router creation into getRouter, adds MSW worker config/asset, and updates .gitignore for Playwright artifacts/screenshots.

Written by Cursor Bugbot for commit 93412ac. This will update automatically on new commits. Configure here.

Note

Make apps/web/src/components/ChatView.MessagesTimeline height estimation width-aware using timelineHeight.estimateTimelineMessageHeight and add Playwright-driven browser tests to validate attachment and text wrapping behavior

Introduce a width-aware timelineHeight.estimateTimelineMessageHeight and wire it into ChatView.MessagesTimeline; add browser tests with Playwright/Vitest and MSW; update CI to install/cache Playwright and run browser tests; refactor router setup into a factory.

📍Where to Start

Start with the estimator in timelineHeight.ts, then review its integration in ChatView.tsx and validations in ChatView.browser.tsx.

Macroscope summarized 93412ac.

- extract `estimateTimelineMessageHeight` into `timelineHeight.ts`
- account for timeline width and explicit newlines when estimating wrapped lines
- re-measure virtualized rows on width changes to fix attachment/message height sizing
- add Vitest coverage for assistant/user wrapping and attachment row height rules
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 4, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0d941110-f4e5-4a28-a68b-50fcf6e47356

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch t3code/fix-chat-attachment-height

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: ResizeObserver never activates for initially empty threads
    • Replaced useRef with a state-backed callback ref (useState + setTimelineRootNode as the ref callback) so the useLayoutEffect re-runs when the timeline div mounts after transitioning from the empty-thread state.

Create PR

Or push these changes by commenting:

@cursor push fd13dceb67
Preview (fd13dceb67)
diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx
--- a/apps/web/src/components/ChatView.tsx
+++ b/apps/web/src/components/ChatView.tsx
@@ -3187,12 +3187,11 @@
   onImageExpand,
   markdownCwd,
 }: MessagesTimelineProps) {
-  const timelineRootRef = useRef<HTMLDivElement | null>(null);
+  const [timelineRootNode, setTimelineRootNode] = useState<HTMLDivElement | null>(null);
   const [timelineWidthPx, setTimelineWidthPx] = useState<number | null>(null);
 
   useLayoutEffect(() => {
-    const timelineRoot = timelineRootRef.current;
-    if (!timelineRoot) return;
+    if (!timelineRootNode) return;
 
     const updateWidth = (nextWidth: number) => {
       setTimelineWidthPx((previousWidth) => {
@@ -3203,7 +3202,7 @@
       });
     };
 
-    updateWidth(timelineRoot.getBoundingClientRect().width);
+    updateWidth(timelineRootNode.getBoundingClientRect().width);
 
     if (typeof ResizeObserver === "undefined") return;
     const observer = new ResizeObserver((entries) => {
@@ -3211,11 +3210,11 @@
       if (!entry) return;
       updateWidth(entry.contentRect.width);
     });
-    observer.observe(timelineRoot);
+    observer.observe(timelineRootNode);
     return () => {
       observer.disconnect();
     };
-  }, []);
+  }, [timelineRootNode]);
 
   const rows = useMemo<TimelineRow[]>(() => {
     const nextRows: TimelineRow[] = [];
@@ -3645,7 +3644,7 @@
   }
 
   return (
-    <div ref={timelineRootRef} className="mx-auto w-full min-w-0 max-w-3xl overflow-x-hidden">
+    <div ref={setTimelineRootNode} className="mx-auto w-full min-w-0 max-w-3xl overflow-x-hidden">
       {virtualizedRowCount > 0 && (
         <div className="relative" style={{ height: `${rowVirtualizer.getTotalSize()}px` }}>
           {virtualRows.map((virtualRow: VirtualItem) => {

Comment thread apps/web/src/components/ChatView.tsx Outdated
Comment thread apps/web/src/components/timelineHeight.ts Outdated
- add browser E2E tests validating timeline height estimator against rendered DOM
- configure Playwright test runner and scripts in `apps/web`
- run browser tests in CI with Playwright browser caching and install step
- Replace Playwright e2e timeline-height test setup with Vitest browser config
- Add `ChatView.browser.tsx` parity tests for text wrapping and attachment height estimation
- Add row data attributes in `ChatView` to support in-browser measurement targeting
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Inconsistent width measurement APIs cause initial value mismatch
    • Replaced getBoundingClientRect().width (border-box) with clientWidth - paddingLeft - paddingRight (content-box) for the initial measurement, making it consistent with the ResizeObserver's contentRect.width.

Create PR

Or push these changes by commenting:

@cursor push e1d5bbad14
Preview (e1d5bbad14)
diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx
--- a/apps/web/src/components/ChatView.tsx
+++ b/apps/web/src/components/ChatView.tsx
@@ -3203,7 +3203,12 @@
       });
     };
 
-    updateWidth(timelineRoot.getBoundingClientRect().width);
+    const cs = getComputedStyle(timelineRoot);
+    updateWidth(
+      timelineRoot.clientWidth -
+        parseFloat(cs.paddingLeft) -
+        parseFloat(cs.paddingRight),
+    );
 
     if (typeof ResizeObserver === "undefined") return;
     const observer = new ResizeObserver((entries) => {

Comment thread apps/web/src/components/ChatView.tsx Outdated
- Replace mocked router hooks with a memory router + RouterProvider
- Render ChatView through a test route to exercise attachment height layout in realistic routing context
- Run ChatView browser tests through the real app router/store stack
- Add MSW worker setup and mocked WS/attachment responses for full-app rendering
- Extract shared `getRouter` setup and add a timeline root data hook for reliable measurement
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Document title no longer set after refactor
    • Fixed the meta format from { name: "title", content: ... } to { title: APP_DISPLAY_NAME } and added the missing <HeadContent /> component to both render paths in the root route so TanStack Router's head management actually takes effect.

Create PR

Or push these changes by commenting:

@cursor push 7f77794bdd
Preview (7f77794bdd)
diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx
--- a/apps/web/src/routes/__root.tsx
+++ b/apps/web/src/routes/__root.tsx
@@ -1,5 +1,6 @@
 import { ThreadId } from "@t3tools/contracts";
 import {
+  HeadContent,
   Outlet,
   createRootRouteWithContext,
   type ErrorComponentProps,
@@ -27,26 +28,30 @@
   component: RootRouteView,
   errorComponent: RootRouteErrorView,
   head: () => ({
-    meta: [{ name: "title", content: APP_DISPLAY_NAME }],
+    meta: [{ title: APP_DISPLAY_NAME }],
   }),
 });
 
 function RootRouteView() {
   if (!readNativeApi()) {
     return (
-      <div className="flex h-screen flex-col bg-background text-foreground">
-        <div className="flex flex-1 items-center justify-center">
-          <p className="text-sm text-muted-foreground">
-            Connecting to {APP_DISPLAY_NAME} server...
-          </p>
+      <>
+        <HeadContent />
+        <div className="flex h-screen flex-col bg-background text-foreground">
+          <div className="flex flex-1 items-center justify-center">
+            <p className="text-sm text-muted-foreground">
+              Connecting to {APP_DISPLAY_NAME} server...
+            </p>
+          </div>
         </div>
-      </div>
+      </>
     );
   }
 
   return (
     <ToastProvider>
       <AnchoredToastProvider>
+        <HeadContent />
         <EventRouter />
         <DesktopProjectBootstrap />
         <Outlet />

Comment thread apps/web/src/routes/__root.tsx
Comment thread apps/web/src/components/ChatView.tsx Outdated
- replace manual `createRoot` mounting with `render`/`unmount` from vitest-browser-react
- add `vitest-browser-react` to web devDependencies and lockfile
- Replace manual timeout polling loops with `vi.waitFor` in test helpers
- Improve reliability of locating and measuring virtualized user rows in attachment-height tests
- recompute timeline width from the root element on ResizeObserver updates
- rerun width effect when message/working state changes
- reduce minimum user chars per line for narrow layouts and add regression test
- Add viewport matrix coverage for long text and attachment row-height parity
- Reuse a mount/measure test harness to validate resize behavior in one session
- Ensure production CSS and viewport sizing are applied before measurements
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Condition inversion changes system message height estimation
    • Changed message.role !== "user" back to message.role === "assistant" so system messages use user sizing rules (matching the original behavior) instead of being incorrectly routed to assistant sizing.

Create PR

Or push these changes by commenting:

@cursor push 7fbf52b7af
Preview (7fbf52b7af)
diff --git a/apps/web/src/components/timelineHeight.ts b/apps/web/src/components/timelineHeight.ts
--- a/apps/web/src/components/timelineHeight.ts
+++ b/apps/web/src/components/timelineHeight.ts
@@ -67,7 +67,7 @@
   message: TimelineMessageHeightInput,
   layout: TimelineHeightEstimateLayout = { timelineWidthPx: null },
 ): number {
-  if (message.role !== "user") {
+  if (message.role === "assistant") {
     const charsPerLine = estimateCharsPerLineForAssistant(layout.timelineWidthPx);
     const estimatedLines = estimateWrappedLineCount(message.text, charsPerLine);
     return ASSISTANT_BASE_HEIGHT_PX + estimatedLines * LINE_HEIGHT_PX;

Comment thread apps/web/src/components/timelineHeight.ts
- Route `system` messages through assistant height estimation
- Keep user attachment height logic scoped to user messages
- Add regression test for system-message sizing behavior
@juliusmarminge juliusmarminge merged commit 7c7fcf4 into main Mar 5, 2026
4 checks passed
@juliusmarminge juliusmarminge deleted the t3code/fix-chat-attachment-height branch March 5, 2026 18:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant