Skip to content

[Bug]: The composer doesn't rest on scroll when a thread opens above its end, until I scroll to the very end #14232

Description

@Vantrongs

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/web

Steps to reproduce

  1. With Collapse composer on scroll on, open a thread and scroll a few turns up from the end.
  2. Switch to another thread while the agent in the first one keeps working and adds turns.
  3. Switch back. The thread reopens where I left it, above its end, as intended; the new turns below it haven't been rendered yet.
  4. Leave the composer empty and scroll the thread with the mouse wheel, up or down.

The same happens whenever a thread opens above its end before its last rows have rendered, for example through #14051.

Expected behavior

The composer rests to a single line on the first scroll, as it does when the thread was opened at its end.

Actual behavior

It stays fully expanded however much I scroll. Only when I reach the very end of the thread does it rest, and after that it collapses and expands normally.

Likely cause

From reading the code:

  • The composer rests only while timelineOverflows is true (shouldUseRestingComposerLayout in composerFooterLayout.ts).
  • timelineContentOverflowsViewport (timelineScrollAnchoring.ts) computes that from the bottom of the last row via getRowBottom, which needs state.sizeAtIndex(last).
  • In LegendList, sizeAtIndex returns sizesKnown.get(id): only rows that have been rendered and measured. When the thread opens above its end, the last row hasn't been rendered, getRowBottom returns null, and the function reports "fits", as its comment says ("Unknown row geometry … counts as fitting").
  • The wheel still sets the scroll-collapse flag, because canScrollCollapseComposer doesn't depend on timelineOverflows. That's why the composer rests the moment the last row renders at the end.

If the thread's latest rows have already rendered in this session (I was at its end and nothing was added since), LegendList knows the last row's size and resting works; that's why this only shows up after new turns or an above-the-end open.

Suggested fix

positionAtIndex(last) is known even when the size isn't: whenever the data changes, LegendList positions every row, using estimated sizes for rows it hasn't measured. getRowBottom could treat an unknown size as its lower bound (it already clamps the height to at least 1px) instead of returning null. Resting still needs a real scroll gesture, so a thread that genuinely fits is unaffected.

Related

This is not #14051: it happens when position restore works as intended, and #13602 doesn't touch it.

Impact

Minor bug or occasional failure

Version or commit

0.0.43-nightly.20260928.2375; code references are to main @ d2c9281 with @legendapp/list 3.3.5

Environment

Desktop app on Linux (NixOS, Wayland, niri)

Logs or stack traces

Screenshots, recordings, or supporting files

No response

Workaround

Scroll to the very end of the thread once.

Activity

  1. added
    bugSomething is broken or behaving incorrectly.
    needs-triageIssue needs maintainer review and initial categorization.
    on Sep 29, 2026
  2. juliusmarminge commented on Sep 29, 2026

    @juliusmarminge
    Member

    Triage

    Confirmed on main at d2c9281. This is a real bug, and it is not a duplicate of #14051, #13602, or #14231.

    Collapse-on-scroll is the default (composerCollapseOnScroll decodes to true). The wheel handler does set the scroll-collapsed flag: canScrollCollapseComposer in ChatComposer.tsx does not look at overflow. Resting still requires timelineOverflows in shouldUseRestingComposerLayout, and that flag stays false until the last row has a measured size.

    timelineContentOverflowsViewport takes the last row's bottom from getRowBottom. That needs both positionAtIndex and sizeAtIndex. In @legendapp/list 3.3.5, getState() implements those as positions[index] and sizesKnown.get(id). On a data change, updateItemPositions lays out every row and fills positions with estimated sizes (the timeline's estimatedItemSize is 90). sizesKnown is only filled after a row is rendered and measured. An unmeasured tail therefore makes getRowBottom return null, and the overflow helper treats that as "fits" (timelineScrollAnchoring.ts). The composer stays expanded no matter how far you scroll, until the last row mounts at the end and a later onItemSizeChanged / scroll report flips overflow on. A thread whose tail was already measured in this session does not hit this, which matches the report.

    That "unknown geometry counts as fitting" rule came from #9965 so a short thread does not rest before it is measured. Keep that. Treat a missing size as a 1px lower bound only when positionAtIndex is already a finite number, and only inside the overflow check. If the estimated top of the last row is already past scrollLength - composerInset - anchorOffset, the thread overflows even before the tail is measured. If that top is still inside the viewport, keep reporting "fits" until the size is known. Do not change getRowBottom itself: getAnchoredTurnMetrics uses it, and a synthetic bottom there would stop returning null and could move the anchored-turn scroll in ChatView.

    #14231 is the same symptom through a different gate. A pending question sets composerHasExpandedChrome, so scrolling never even arms collapse. This issue is the overflow measurement while the composer is empty.

  3. added
    via-triageFiled through npx t3 triage
    and removed
    needs-triageIssue needs maintainer review and initial categorization.
    on Sep 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions