Skip to content

Keep Android chat scrollable while agents stream#2338

Open
boudra wants to merge 2 commits into
mainfrom
fix/android-streaming-chat-gesture
Open

Keep Android chat scrollable while agents stream#2338
boudra wants to merge 2 commits into
mainfrom
fix/android-streaming-chat-gesture

Conversation

@boudra

@boudra boudra commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Linked issue

No matching open issue.

Type of change

  • Bug fix
  • New feature (with prior issue + design alignment)
  • Refactor / code improvement
  • Docs

What does this PR do

Android users can now drag or fling away from the bottom of a chat while an agent is streaming without new tokens taking the viewport back. The bottom-anchor controller pauses sticky maintenance for the full user gesture, including momentum, and genuine drag events bypass the small event budget reserved for programmatic offset corrections.

How did you verify it

  • Built and installed the Expo Android dev client (sh.paseo.debug) on the emulator.
  • Exercised the five-minute mock stream in that client through agent-device.
  • Reproduced the old snap-back behavior while the mock was streaming.
  • After the fix, both a slow 750 px pull and a fast 750 px fling kept the same earlier messages visible for another 3 seconds while tokens continued; Scroll to bottom remained visible in both cases.
  • npx vitest run packages/app/src/agent-stream/bottom-anchor-controller.test.ts --bail=1 (19 tests)
  • npm run typecheck
  • npm run lint
  • npm run format

Risk surface: native chat scrolling on Android and iOS. The behavioral smoke covered Android; the shared native event wiring was not manually exercised on iOS.

Checklist

  • One focused change. Unrelated cleanups split out.
  • npm run typecheck passes
  • npm run lint passes
  • npm run format ran (Biome)
  • UI changes include screenshots or video for every affected platform
  • Tests added or updated where it made sense

Continuous Android stream updates kept bottom-anchor verification pending, so genuine finger drags were mistaken for programmatic corrections. Preserve explicit drag ownership so user scrolling can detach immediately.
@greptile-apps

greptile-apps Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds user-scroll lifecycle tracking to the Android/iOS native chat scroll strategy so that agent-streaming snap-backs no longer interrupt an active drag or momentum gesture. Four FlatList scroll-lifecycle events (onScrollBeginDrag, onScrollEndDrag, onMomentumScrollBegin, onMomentumScrollEnd) are wired to a new beginUserScroll/endUserScroll interface on BottomAnchorController, which gates all four programmatic-maintenance handlers behind an isUserScrollActive flag.

  • bottom-anchor-controller.ts gains beginUserScroll/endUserScroll methods; beginUserScroll cancels any in-flight programmatic request, and endUserScroll either marks the sticky measurement verified (still near bottom) or detaches (user scrolled away).
  • strategy-native.tsx introduces a one-frame deferred end-of-drag signal to bridge the gap before momentum begins; the programmaticScrollEventBudgetRef guard is also updated so user-active scroll events bypass the budget check.

Confidence Score: 5/5

Safe to merge; the change is confined to the native scroll strategy and its controller, the logic correctly handles all gesture lifecycle transitions, and the existing test suite was extended to cover the primary new path.

The isUserScrollActive flag is consistently set and cleared across all gesture lifecycle events. The early-exit guards in all four maintenance handlers correctly skip programmatic corrections while a user gesture owns the viewport. endUserScroll cleanly resolves the final state. Edge cases around momentum scrolls and concurrent programmatic scrolls are handled safely.

No files require special attention; all three changed files are self-contained within the agent-stream module.

Important Files Changed

Filename Overview
packages/app/src/agent-stream/bottom-anchor-controller.ts Adds isUserScrollActive flag, beginUserScroll/endUserScroll methods, and early-exit guards in the four handlers that drive programmatic scroll maintenance — the logic is correct, with endUserScroll cleanly branching between verify-in-place and detach based on final scroll position.
packages/app/src/agent-stream/strategy-native.tsx Wires four new FlatList scroll-lifecycle events (drag begin/end, momentum begin/end) to track user intent; the one-frame defer on drag-end correctly bridges the gap before momentum begins, and the reset paths on agent change are properly handled.
packages/app/src/agent-stream/bottom-anchor-controller.test.ts Adds one integration test covering the main path (prepareForStickyContentChange → beginUserScroll → content/near-bottom change → detach); the hasPendingRequest: true, isUserScroll: true short-circuit path is not covered by any test.

Sequence Diagram

sequenceDiagram
    participant U as User (finger)
    participant FL as FlatList events
    participant SN as strategy-native
    participant BAC as BottomAnchorController

    Note over FL,BAC: Drag-then-fling gesture
    U->>FL: onScrollBeginDrag
    FL->>SN: handleScrollBeginDrag
    SN->>BAC: beginUserScroll() — cancels pending request/verification

    loop scroll events during drag
        FL->>SN: onScroll
        SN->>BAC: "handleScrollNearBottomChange (isUserScrollActive=true)"
        BAC->>BAC: detachByUser() if not near bottom
    end

    U->>FL: onScrollEndDrag
    FL->>SN: handleScrollEndDrag
    SN->>SN: schedule rAF for endUserScroll

    alt fling — momentum follows
        FL->>SN: onMomentumScrollBegin
        SN->>SN: clearPendingUserScrollEnd() — cancels rAF
        FL->>SN: onMomentumScrollEnd
        SN->>BAC: endUserScroll()
        BAC->>BAC: markVerified if nearBottom, else detachByUser
    else simple drag — no momentum
        Note over SN: rAF fires one frame later
        SN->>BAC: endUserScroll()
        BAC->>BAC: markVerified if nearBottom, else detachByUser
    end

    Note over FL,BAC: Programmatic scroll (agent streaming, no user touch)
    BAC->>SN: "scrollToBottom(animated=false) — sets budget=3"
    FL->>SN: "onScroll (budget>0, y<=8, isUserScrollActive=false)"
    SN->>SN: decrement budget — handleScrollNearBottomChange NOT called
Loading

Reviews (2): Last reviewed commit: "fix(app): preserve scroll ownership thro..." | Re-trigger Greptile

Comment thread packages/app/src/agent-stream/strategy-native.tsx
Comment thread packages/app/src/agent-stream/bottom-anchor-controller.test.ts Outdated
sakurayun pushed a commit to sakurayun/paseo-reclaude that referenced this pull request Jul 23, 2026
Upstream PR: getpaseo#2338

Resolved strategy-native.tsx by keeping both clearScrollToIndexRetry (local)
and clearPendingUserScrollEnd (PR) helpers.
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