Skip to content

fix(web): preserve scroll when composer grows - #5234

Open
caezium wants to merge 1 commit into
pingdotgg:mainfrom
caezium:agent/preserve-scroll-on-composer-resize
Open

fix(web): preserve scroll when composer grows#5234
caezium wants to merge 1 commit into
pingdotgg:mainfrom
caezium:agent/preserve-scroll-on-composer-resize

Conversation

@caezium

@caezium caezium commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

What Changed

  • Bind wheel and touch navigation directly to the timeline, and treat only scrollbar presses as pointer navigation, so reader intent cannot be missed without ordinary timeline clicks disabling live follow.
  • Disable layout-based end following after the reader leaves the live edge, while preserving existing data-change and item-layout behavior.
  • Scope the layout-follow state to the active thread so a scrolled-up thread cannot leak stale state into the next thread.
  • Add focused regression coverage for the ChatView state handoff, LegendList configuration, and navigation bindings.

Why

When manual-navigation listeners miss the timeline during mount, scrolling upward does not cancel live following. Growing the composer with Shift+Enter then updates the timeline's bottom inset and reruns end-following logic, forcing the transcript to the bottom.

The timeline now receives navigation handlers through its normal component lifecycle and only keeps layout-based end following enabled while the reader is at the live edge. Explicit scroll-to-end, thread changes, and sends restore following.

UI Changes

Before

Shift+Enter grows the composer and moves the transcript from the reader's earlier position to the live edge.

Before: Shift+Enter snaps the transcript to the bottom

Before result at the live edge

After

The composer grows while the same earlier transcript position remains in view.

After: Shift+Enter preserves the transcript position

After result with the earlier position preserved

Validation

  • vp test run apps/web/src/components/chat/MessagesTimeline.test.tsx apps/web/src/components/chat/timelineScrollAnchoring.test.tsx apps/web/src/components/ChatView.logic.test.ts (59 passed)
  • vp lint --report-unused-disable-directives apps/web/src/components/ChatView.tsx apps/web/src/components/chat/MessagesTimeline.tsx apps/web/src/components/chat/MessagesTimeline.test.tsx
  • vp run --filter @t3tools/web typecheck
  • Browser regression on untouched upstream/main: real Shift+Enter grew the composer from 70px to 90.96px and moved scrollTop from 652.08px to the 1252.08px live edge.
  • Browser verification on this branch: the same Shift+Enter and composer growth kept scrollTop at 652.08px.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes

Built with GPT-5.6-Sol in T3 Code through the Codex harness.


Note

Low Risk
Chat timeline scroll and composer layout behavior only; changes are localized with regression tests and no auth, data, or API impact.

Overview
Fixes the transcript jumping to the live edge when the composer grows (e.g. Shift+Enter) after the user has scrolled up.

Layout follow is now gated per thread: ChatView tracks TimelineLayoutFollowState and passes maintainScrollAtEndOnLayout into MessagesTimeline. LegendList still maintains scroll on data/item layout changes, but layout end-following is turned off once the reader leaves the bottom; scrolling back, sending, or switching threads restores it. A new thread defaults to follow-at-end so scrolled-up state does not leak across threads.

Manual navigation is wired on the timeline list instead of deferred DOM listeners in ChatView: wheel, touchmove, and pointer-down on the scrollbar (via isTimelineScrollbarPointerDown) call onManualNavigation and update layout-follow state. Ordinary clicks on message content no longer cancel live follow.

Reviewed by Cursor Bugbot for commit 911cd65. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Preserve scroll position in chat timeline when composer grows

  • Introduces per-thread layout-follow state in ChatViewContent that tracks whether the timeline should snap to the end during layout changes (e.g. composer resize).
  • Layout-follow is disabled when the user scrolls away from the end and re-enabled when they return to the end, send a message, or switch threads.
  • Wheel, touchmove, and scrollbar pointerdown events in MessagesTimeline now trigger manual-navigation, which disables layout-follow until the user returns to the end.
  • A new isTimelineScrollbarPointerDown utility in MessagesTimeline.logic.ts distinguishes scrollbar drags from content pointer interactions so only scrollbar pointerdowns count as navigation.
  • State transitions are managed by pure reducer functions in timelineScrollAnchoring.ts via reduceTimelineLayoutFollowState.

Macroscope summarized 911cd65.

Summary by CodeRabbit

  • Bug Fixes

    • Improved chat timeline scrolling when navigating between threads.
    • Preserved the correct follow position when new messages or layout changes occur.
    • Prevented automatic scrolling after manually navigating away from the latest messages.
    • Improved scrollbar, wheel, touch, and pointer interactions for manual navigation.
    • Returning to the end of the timeline now reliably restores automatic follow behavior.
  • Tests

    • Added coverage for thread switching, scrollbar interactions, manual navigation, and layout scroll behavior.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The timeline now tracks layout-follow state by thread. Manual navigation can disable layout retention until the timeline returns to the end. MessagesTimeline detects wheel, touch, and scrollbar interactions and applies conditional layout retention.

Changes

Timeline scroll control

Layer / File(s) Summary
Timeline follow contract and scrollbar detection
apps/web/src/components/chat/timelineScrollAnchoring.ts, apps/web/src/components/chat/MessagesTimeline.logic.ts, apps/web/src/components/chat/MessagesTimeline.tsx
The timeline defines thread-aware follow-state actions. It detects scrollbar pointer navigation and applies the follow flag only to layout retention.
Timeline follow validation
apps/web/src/components/chat/MessagesTimeline.test.tsx, apps/web/src/components/chat/timelineScrollAnchoring.test.tsx
Tests cover scrollbar detection, manual-navigation callbacks, state transitions, thread changes, and independent retention modes.
ChatView follow-state integration
apps/web/src/components/ChatView.tsx
ChatView updates follow state during navigation, end-position changes, thread switches, message sends, and plan follow-ups. It passes the derived flag to MessagesTimeline.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ChatView
  participant MessagesTimeline
  participant LegendList
  User->>MessagesTimeline: Navigate with wheel, touch, or scrollbar
  MessagesTimeline->>ChatView: Report manual navigation
  ChatView->>ChatView: Update thread follow state
  ChatView->>MessagesTimeline: Pass layout-follow flag
  MessagesTimeline->>LegendList: Configure layout retention
  LegendList-->>MessagesTimeline: Retain data and item-layout positions
Loading

Possibly related PRs

Suggested reviewers: juliusmarminge

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the main fix: preserving chat scroll position when the composer grows.
Description check ✅ Passed The description includes complete What Changed, Why, UI Changes, validation, and Checklist sections with relevant details and evidence.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:S 10-29 changed lines (additions + deletions). labels Aug 2, 2026
@caezium
caezium marked this pull request as ready for review August 2, 2026 13:17
@caezium

caezium commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
apps/web/src/components/chat/MessagesTimeline.test.tsx (1)

428-442: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Add a focused test for the ChatViewContent handoff.

This test verifies the LegendList configuration after the prop is manually set to false. It does not verify that ChatViewContent disables retention after a real scroll-away event or restores it after scroll-to-end, thread changes, and sends. Add one focused interaction test for that state handoff.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/chat/MessagesTimeline.test.tsx` around lines 428 -
442, The existing test only validates LegendList configuration with
maintainScrollAtEndOnLayout manually set to false; add a focused interaction
test covering the ChatViewContent handoff after a real scroll-away event. Verify
retention is disabled when the user scrolls away, then restored after scrolling
to the end, changing threads, and sending a message, using the existing
ChatViewContent test helpers and observable state.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@apps/web/src/components/chat/MessagesTimeline.test.tsx`:
- Around line 428-442: The existing test only validates LegendList configuration
with maintainScrollAtEndOnLayout manually set to false; add a focused
interaction test covering the ChatViewContent handoff after a real scroll-away
event. Verify retention is disabled when the user scrolls away, then restored
after scrolling to the end, changing threads, and sending a message, using the
existing ChatViewContent test helpers and observable state.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bf36d633-3918-4460-900c-04ed0059e4f0

📥 Commits

Reviewing files that changed from the base of the PR and between e60821f and cebb506.

📒 Files selected for processing (3)
  • apps/web/src/components/ChatView.tsx
  • apps/web/src/components/chat/MessagesTimeline.test.tsx
  • apps/web/src/components/chat/MessagesTimeline.tsx

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

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 using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit cebb506. Configure here.

Comment thread apps/web/src/components/ChatView.tsx Outdated
@macroscopeapp

macroscopeapp Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This is a focused UI bug fix for scroll behavior preservation, well-tested with clear intent. However, the author is a first-time contributor to these scroll-related files, and the timeline scroll system has subtle interactions that warrant review by someone familiar with it.

You can customize Macroscope's approvability policy. Learn more.

@caezium
caezium marked this pull request as draft August 2, 2026 14:23
@caezium
caezium force-pushed the agent/preserve-scroll-on-composer-resize branch from cebb506 to b2ae555 Compare August 2, 2026 14:57
@github-actions github-actions Bot added size:M 30-99 changed lines (additions + deletions). and removed size:S 10-29 changed lines (additions + deletions). labels Aug 2, 2026
Comment thread apps/web/src/components/chat/MessagesTimeline.tsx Outdated
@caezium
caezium force-pushed the agent/preserve-scroll-on-composer-resize branch from b2ae555 to 722cfcd Compare August 2, 2026 15:05
@caezium
caezium marked this pull request as ready for review August 2, 2026 15:07
@caezium
caezium force-pushed the agent/preserve-scroll-on-composer-resize branch from 722cfcd to 5c12cf9 Compare August 2, 2026 19:45
@github-actions github-actions Bot added size:L 100-499 changed lines (additions + deletions). and removed size:M 30-99 changed lines (additions + deletions). labels Aug 2, 2026
@caezium

caezium commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
apps/web/src/components/chat/timelineScrollAnchoring.test.tsx (1)

7-7: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unnecessary explicit state annotations.

TypeScript infers both state object types. Remove TimelineLayoutFollowState from the import when no annotation uses it.

Proposed cleanup
-  type TimelineLayoutFollowState,
 } from "./timelineScrollAnchoring";
 
-    let state: TimelineLayoutFollowState = { threadKey, enabled: true };
+    let state = { threadKey, enabled: true };
 
-    const previousThreadState: TimelineLayoutFollowState = {
+    const previousThreadState = {

As per coding guidelines, **/*.{ts,tsx}: “Prefer inferred types over explicit annotations and do not use any.”

Also applies to: 33-33, 62-65

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/chat/timelineScrollAnchoring.test.tsx` at line 7,
Remove the unused TimelineLayoutFollowState import and delete the explicit state
type annotations at the referenced state declarations, allowing TypeScript to
infer both state object types while preserving their existing values and
behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@apps/web/src/components/chat/timelineScrollAnchoring.test.tsx`:
- Line 7: Remove the unused TimelineLayoutFollowState import and delete the
explicit state type annotations at the referenced state declarations, allowing
TypeScript to infer both state object types while preserving their existing
values and behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5d2682d3-4ef6-4da7-868c-9950499416e0

📥 Commits

Reviewing files that changed from the base of the PR and between cebb506 and 5c12cf9.

📒 Files selected for processing (6)
  • apps/web/src/components/ChatView.tsx
  • apps/web/src/components/chat/MessagesTimeline.logic.ts
  • apps/web/src/components/chat/MessagesTimeline.test.tsx
  • apps/web/src/components/chat/MessagesTimeline.tsx
  • apps/web/src/components/chat/timelineScrollAnchoring.test.tsx
  • apps/web/src/components/chat/timelineScrollAnchoring.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/src/components/chat/MessagesTimeline.test.tsx
  • apps/web/src/components/ChatView.tsx

@caezium
caezium force-pushed the agent/preserve-scroll-on-composer-resize branch from 5c12cf9 to 911cd65 Compare August 2, 2026 19:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant