Skip to content

[Bug]: iOS: scrolling up in a long thread teleports the viewport and draws rows outside the list bounds #12072

Description

@Pivii
t3code-ios-scroll-jump.mp4

Before submitting

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

Area

apps/mobile

Steps to reproduce

This one is intermittent and I cannot trigger it on demand. It hits me several times a week. The screen recording below captures a full episode; here is the shape of it:

  1. Open a long thread that contains at least one very tall tool-output row (mine has a skill listing of roughly 100 rows).
  2. Let the agent finish a reply while the viewport is parked somewhere above the bottom.
  3. Tap the scroll-to-bottom button. This works — the feed lands at the end.
  4. Drag up a short distance to re-read the reply.

Expected behavior

A short upward drag moves the viewport by the dragged distance, and rows stay inside the list bounds.

Actual behavior

Three distinct symptoms, all visible in the recording:

  1. The viewport teleports. A short upward drag jumps to the start or the middle of the thread instead of moving by the dragged distance. It flips between the conversation region and the large tool-output table many screens away.
  2. Rows render outside the list bounds. Conversation text is drawn over the navigation header and below the composer, so the list stops clipping its rows.
  3. The scroll-to-bottom chevron floats mid-screen instead of sitting above the composer, which suggests the computed layout is wrong rather than just the scroll offset.

Hypothesis, from reading the source: apps/mobile/src/features/threads/ThreadFeed.tsx already documents this failure mode above getFixedItemSize:

every row above the viewport is assumed to be estimatedItemSize tall, and scrolling up through unmeasured content corrects each row's height as it mounts — the feed visibly jumps

The mitigation from #4874 only assigns fixed heights to the small chrome rows. Message rows return undefined and fall back to LegendList's per-type running average against estimatedItemSize={180}. A single tool-output row an order of magnitude taller than that average would produce a large offset correction the moment it mounts while scrolling up, which matches symptom 1. Symptom 2 looks adjacent to #11813.

Question: #11813 landed after the 1.1.0 App Store build I am on. Does the 1.2.0 train already change this behavior? I am happy to re-test and close this if so — I just have no way to run that build yet.

Impact

Major degradation or frequent failure

Version or commit

App Store 1.1.0

Environment

iPhone 15 Pro Max, iOS 27

Logs or stack traces

Screenshots, recordings, or supporting files

Screen recording attached in a follow-up comment.

Workaround

Tapping the scroll-to-bottom button re-anchors the feed, but the next upward drag can teleport again.

Activity

  1. juliusmarminge commented on Sep 16, 2026

    @juliusmarminge
    Member

    Triage: real iOS thread-feed bug. Not a duplicate, and not already fixed on the 1.2.0 train.

    What you reported. On App Store 1.1.0 (iPhone 15 Pro Max, iOS 27), a long thread with one very tall tool-output / skill-listing row (~100 lines) intermittently teleports on a short upward drag after scroll-to-bottom. Rows also paint over the nav header and under the composer, and the scroll-to-bottom chevron can sit mid-screen. Re-tapping scroll-to-bottom re-anchors, then the next upward drag can teleport again.

    Your hypothesis is right, and it is still true on main. ThreadFeed documents this above getFixedItemSize: unmeasured rows are treated as estimatedItemSize (180) tall, and scrolling up through them corrects height on mount. #4874 only pins the small chrome rows (turn-fold, work-toggle, thinking, collapsed activity groups). Message rows and expanded activity groups still return undefined and use LegendList’s per-type running average. A single tool-output / skill table an order of magnitude taller than 180px is a multi-screen offset correction the moment that row mounts — that matches symptom 1.

    The scroll-to-bottom → short-upward-drag sequence makes that worse: while follow is armed, JS maintainVisibleContentPosition is off and maintainScrollAtEnd is on. The first drag flips that on the next React render, so a huge measure can land in the gap.

    Does 1.2.0 / #11813 already change this? Only partly, and not the teleport.

    Change In App Store 1.1.0? In 1.2.0 train? This bug
    #4874 chrome getFixedItemSize yes yes mitigation only; tall message/tool-output rows still unfixed
    #9013 scroll bounds after disclosures yes yes different trigger (expand/collapse), not this upward-drag case
    #10479 keep native iOS MVCP attached yes yes different failure (−1e7 blank-list jump); you still hit this on 1.1.0
    #11813 drop iOS row layout transitions no yes (merged 2026-09-15) may help symptom 2 (stale native positions / rows outside bounds). Does not give tall rows a real size

    So: please retest overflow/clipping (symptoms 2–3) on 1.2.0 when you have that build. Do not close this on the strength of #11813 — the estimate→actual teleport path is unchanged.

    Not the same as #10960 (Android keyboard inset), #11992 (Android composer / working pill covering newest lines), or #10840 (combine scroll button with working timer). No open PR targets this tall-row teleport.

    Where a fix should go. apps/mobile/src/features/threads/ThreadFeed.tsx (getFixedItemSize, estimatedItemSize={180}, follow/MVCP toggle) and expanded tool-output sizing in thread-work-log.tsx. The chevron sitting mid-screen is a consequence of a blown content-size / overlay layout (floating-working-control.tsx is position: absolute; top: -offset on the composer), not its own control bug.

    The screen recording mentioned in the report never landed. If you still have it, please attach it — especially useful for confirming whether the tall row is an assistant markdown table or an expanded work-log tool result.

  2. added
    bugSomething is broken or behaving incorrectly.
    acceptedfeature request accepted
    on Sep 16, 2026
  3. Pivii commented on Sep 16, 2026

    @Pivii
    Author

    Answering your question: the tall row is an assistant markdown table, not an expanded work-log tool result. Bordered cells, three columns — skill name, scope (User / Project / Plugin / Built-in), and approximate token cost.

    I remembered where it came from, and it gives you a deterministic repro. It is the output of Claude Code's /context command. That single assistant message is not one table but several stacked ones:

    Table in /context output Rows in my case
    MCP tools ~170
    Skills ~115
    Custom agents 15
    Memory files 10
    Usage by category 10

    So roughly 320 table rows in one message. The section visible in my recording is the Skills table. On a 15 Pro Max that message is many screens tall on its own, against estimatedItemSize={180}.

    Repro:

    1. Claude Code provider, on a machine with a lot of MCP servers and skills installed. The more there are, the taller the message.
    2. Send /context in a thread.
    3. Tap scroll-to-bottom, then drag up a short distance.

    Your point about the follow latch matches what I see on device: the teleport fires on the first upward drag after scroll-to-bottom, not on a drag from a resting position. A second scroll-to-bottom re-anchors and the next drag can teleport again.

    I will attach the recording and two stills shortly — one showing rows painted above the status bar, one showing the table.

  4. Pivii commented on Sep 16, 2026

    @Pivii
    Author

    Correction to my previous comment: please do not treat those three steps as a reliable repro. I overstated it.

    I went back and ran /context again in a fresh thread, and I could not make the feed teleport on demand. The tall message renders, scroll-to-bottom works, and dragging up behaves normally.

    So what /context gives you is the ingredient, not the trigger: a single assistant message holding ~320 markdown table rows, far taller than estimatedItemSize={180}. Something else has to coincide with it — my guess would be the timing of a thread update landing while the row is being measured, which is the gap you described between maintainScrollAtEnd and JS maintainVisibleContentPosition, but I have no evidence for that beyond the sequence I described.

    The bug itself stays as originally reported: intermittent, several times a week, no reliable repro on my side. Sorry for the noise.

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

    acceptedfeature request acceptedbugSomething is broken or behaving incorrectly.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions