fix(mobile): page native remote history until the reader sees an older turn - #3177
Merged
Merged
Conversation
…r turn Host pages are cut by sequence, and a record's sequence is the moment it was last updated, so one long turn owns every record it produced. Reading a single page per request could therefore deliver nothing but more records of the turn already on screen: the timeline reported "history loaded" while the transcript above stayed identical, and the newest turn of a long session can hold several pages by itself. One request now walks up to four pages and stops as soon as it delivers a turn the transcript did not have, the host runs out, or the budget is spent; the next gesture continues from where it stopped. The request reports one settled history state instead of one per page, and the loading indicator covers the whole walk: the buffered Kotlin transport waits for downstream consumption before it reports caught-up or completes a request, so enqueueing records no longer reads as completion. A request is a user gesture waiting outside the flow, so the lane owes every one an answer: a lane that closes now fails queued requests instead of leaving their callers suspended for the life of the app. Wakes coalesce before the lane serves them, so a hint burst costs one catch-up read and a history request that arrived during the burst is served in that same pass instead of behind one refresh per hint. The burst no longer rewrites the transcript per record either. Writes are deferred until a page settles, a write reuses the rows it wrote last time and appends only what changed, and a turn renders once until its records or controls change. Disconnect, session switch and delete drop the cached rows. iOS, Android and HarmonyOS keep the reading position while paging: one automatic page per deliberate drag, no request queued while loading, anchors preserved on prepend, and no request from layout, anchor correction or released-finger overscroll. iOS also gains the composer dismiss gesture, and subagent children that arrive nested and again flat are folded once instead of drawn twice. A running-input acknowledgement no longer consumes the newly submitted bubble through turn-based deduplication. docs/architecture/peer-device-mode.md records the record-page, delivery and timeline boundaries. These are client-internal boundaries, not additions to the read_stream wire format.
The timeline carried the design token `timeline_top_padding` (22) as its own top padding, but the top overlay is already reserved through `safeAreaInset`, and that inset ends with the 28pt header edge fade. The first message therefore began 50pt below the header band at rest, while Android's `contentPadding(top = topInset)` and HarmonyOS's `contentStartOffset(topInset)` start it at 28pt. Dropping the extra padding gives the three clients one content start and leaves the fade doing what it is for: softening text that scrolls under the band. The token stays in the contract, where only the two design galleries consume it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Native mobile clients (iOS, Android, HarmonyOS) and the mobile-web client page
older remote session history through
read_stream. This change makes onehistory request walk far enough to actually reveal an older turn, keeps the
reading position while it does, and stops the page burst from rewriting the
transcript once per record.
Fixes: no tracked issue.
Type and Areas
Type: bug fix / regression fix, with the performance and UI work that the same
code path requires.
Areas: native mobile (
src/apps/mobile/{ios,android,harmonyos}), shared Kotlinmobile core (
core-transport,core-feature), mobile web, shared relaytransport (
src/shared/relay-transport), docs.Motivation / Impact
Host pages are cut by sequence, and a record's sequence is the moment it was
last updated, so one long turn owns every record it produced. A request that
read exactly one page could therefore deliver nothing but more records of the
turn already on screen: the client reported "history loaded" while the
transcript above stayed identical, and the newest turn of a long session can
hold several pages by itself.
What changes for users:
delivers a turn the transcript did not already have, the host runs out, or the
budget is spent. The next gesture continues from where it stopped.
buffered Kotlin transport waits for downstream consumption before it reports
caught-up or completes a request, so enqueueing records no longer reads as
completion.
twice: one automatic page per deliberate drag, nothing queued while loading,
and no request from layout, anchor correction, or released-finger overscroll.
so a session's first message sits where Android and HarmonyOS already put it.
input row dismisses the keyboard, matching the transcript's own scroll view).
are folded once instead of drawn twice.
through turn-based deduplication.
What changes for developers:
queued requests instead of leaving their callers suspended for the life of the
app, and wakes coalesce before the lane serves them, so a hint burst costs one
catch-up read and a history request queued during the burst is served in that
same pass instead of behind one refresh per hint.
until the page settles, a write reuses the rows it wrote last time and appends
only what changed, and a turn renders once until its records or controls
change. Disconnect, session switch, and delete drop the cached rows.
docs/architecture/peer-device-mode.mdrecords the record-page, delivery, andtimeline boundaries. These stay client-internal: nothing was added to the
read_streamwire format, andMAX_HISTORY_PAGES_PER_REQUESTis mirroredbetween the Kotlin core transport and
src/shared/relay-transport/HostStream.ts.Verification
Commands run on this branch (65 files, +2483/-444):
node --test src/apps/mobile/harmonyos/tools/tests/{host-stream,session-record,history-page-arrival}.test.cjscd src/apps/mobile/shared && ./gradlew :core-feature:jvmTest :core-transport:jvmTestcd src/apps/mobile/android && ./gradlew :app:testDebugUnitTestHistoryPageArrivalTrackerTestcd src/apps/mobile/ios && ./Testing/run-pure-swift-tests.shcd src/apps/mobile/ios && xcodebuild -project OpenBitFun.xcodeproj -scheme OpenBitFun -configuration Debug -destination 'generic/platform=iOS' CODE_SIGNING_ALLOWED=NO build** BUILD SUCCEEDED **cd src/mobile-web && pnpm run test:host-streamcd src/mobile-web && npx tsc --noEmitpnpm run mobile:ui:checkNot run, and not claimed:
pnpm run mobile:architecturefails on this branch and identically on thebase commit (
d06151243, verified in a clean worktree):sharedReachesPlatformTreesflags
core-transport/src/jvmTest/.../ClientBuildContractTest.ktanddefaultArgsInFeatureApiflagsAccountUiState.kt,RemoteSidebarPresentation.kt,ConversationModels.kt. None of those files are touched here; this is apre-existing base failure.
ConversationViewTest,ChatMessageBubbleTest)and the iOS simulator UI tests need a device/simulator and were not run here,
so the prepend anchor, overscroll, and keyboard-dismiss behavior is covered by
unit tests only on this branch.
no simulator or on-device capture of the new spacing was taken on this branch.
controllers over the host stream), at the protocol/reducer/unit level. The
other three scenarios (remote workspace, peer device mode, detached dispatch)
are not touched by this change.
Reviewer Notes
a user gesture, not an unbounded download. Reviewers may want to argue about
the value 4 and about whether the "stops when it shows an older turn" test —
turn membership derived from
turn.turnIdonsession-recordpayloads — isthe right progress signal.
RemoteSessionStoreis the only writer of the remote transcript table, whichis what makes the reuse-unchanged-rows write legal. If that ever stops being
true,
forgetWrittenTranscriptand the append path need a rethink.every mutation that can change what a turn renders to; the deletion path
falls back to clearing the whole cache when the record id does not name a
whole turn.
removed, or repurposed, and the SQLite path only appends rows it previously
wrote.
Checklist
MiniApp/Demo/pocket-island/directory was left untracked and is not part of this branch.)docs/architecture/peer-device-mode.md; the removed Android strings were unused entries, and the iOSLocalizable.xcstringsentries were the matching unused copies.)