fix(mobile): stable active-session order, unified list scroll, reachable mono blocks - #4778
Conversation
Replace the per-instance opacity pulse with a soft horizontal gradient highlight driven by one refcounted module-level Reanimated clock shared across all Skeleton instances. Reduced motion keeps the static muted block; the component API is unchanged.
… load The transient loading skeleton in the list header collapsed in the same update that prepended an older page; FlashList's content-position maintenance compensates prepended items but not the header height change, producing a visible jump at the page boundary. Suppress the placeholder at the render-model layer (loading maps to the omitted banner when one is visible, else hidden) and start the older-page fetch two viewports before the boundary so the prepend lands before the user reaches it. Mirror the fix in the kilo-chat message list and remove the now-unused isFetchingOlder prop end to end.
Match category and master-row placeholders to the settled row geometry, and gate the master Switch and Enable-notifications CTA on a new pure deriveGateSettled predicate so a fully enabled user never sees a transient Enable card or a Switch with a transient wrong value while the push-token queries settle.
|
(bot) E2E interim result (iPhone SE, iOS 26.5, local stack, 3 live CLI sessions): Item 1 (ordering) ACCEPT — A1-A5/A12 pass on both surfaces (order stable across pull-to-refresh ×3, websocket status changes, and the enrichment window; new sessions appear at top and stay). Item 3 (unified scroll) ACCEPT — C1-C7 pass (scroll restoration identical offsets, search/tab-re-tap to top, continuous tray+history scroll, expand survives refresh, pagination works expanded). Item 4 (mono blocks) REJECT on D2 — wide mono content still lays out at viewport width (hierarchy shows no node wider than 375pt), so horizontal pans reveal no line tails; D1/D5/D6/D8/D9/D10 pass. Diagnosis: on Fabric the ScrollView content container stretches the text to viewport width so |
|
(bot) E2E final result (iPhone SE, iOS 26.5, local stack, 3 live CLI sessions) — head Item 1 (deterministic active-session order): ACCEPT — A1–A5, A12 pass. The interim D2 rejection (horizontal pans never reached the inner scroller — RN 0.83 Fabric nested-scroll negotiation failure inside the session FlashList) is resolved by |
|
(bot) E2E follow-up at Reproduction (pre-fix, Fix ( On-device verification (this head, same 3s delay in place, fresh verifier agent, iPhone SE iOS 26.5 simulator):
Verdict: VERIFIED. No manual post-merge actions. |
|
(bot) Standin code review at Verdict: NO VALID FINDINGS. Reviewed all six own commits:
Verified: latency harness is inert when env knobs are unset ( This was the final review pass; no further review rounds are queued unless the head changes. |
Three mobile fixes, stacked on
fix/mobile-loading-polish(#4772).Base note: this branch also carries a snapshot merge of
feat/mobile-session-list-ux(#4773, open) at0affde149, because the unified-scroll item rebuilds the container its scroll behaviors attach to. The PR diff transiently shows #4773's commits (including its two smallapps/webschema/SDK lines) until the bases merge; GitHub collapses them. This branch's own changes are underapps/mobile/plus oneapps/webfix incloud-agent-sdk/session-manager.ts(item 4).1. Stable "Active now" ordering (
d8bc2405f)The Agents "Active now" tray and Home "Agent sessions" section reshuffled as data refreshed: nothing in the pipeline (ingest worker, router, live-sync updater, hook, consumers) imposed an order, and any refetch or websocket
setQueryDatacould permute the array. Fixed once at the shared read boundary (useAgentSessions), which both surfaces inherit.New pure
sortActiveSessionsByCreatedAt:createdAtsort first, tie-broken byidascending.createdAtdescending (newest first).createdAt→idascending — a total order, so output never depends on input order (no reliance onArray.prototype.sortstability; input array never mutated).The precise stability claim (do not overstate): a row lacking
createdAtis placed at the top provisionally. When enrichment suppliescreatedAt(~1–2s later), the row moves to its true position — at most one reposition, within one enrichment cycle of the row appearing. For a brand-new session that position is already the top, so it does not move; a reactivated or orphaned session may move down once. No row that has had acreatedAtfor longer than one enrichment cycle ever moves. The reported defect (rows reshuffling on refresh) is that last sentence, and it is fully fixed; the bounded one-time reposition of a just-appeared row is an accepted residual, pinned by unit tests (A12/A13). A session the worker reports but that has nocli_sessions_v2row never gets acreatedAtand pins to the top indefinitely — accepted as a deviation: it is perfectly stable, and burying a live session at the bottom is strictly worse UX. Reconciling worker/DB divergence is out of scope.The comparator keys strictly on a parseable
createdAt(viaparseTimestamp; unparseable is treated as missing, never epoch 0) and deliberately not onisEnriched's any-of-three-fields heuristic. Sorting at the client read boundary rather than the router is required because the live-sync owner mutates the query cache out-of-band and would overwrite any server-sideORDER BY.Unit tests cover tie determinism, reversed-input identity, non-mutation, the unenriched buckets, and the two permitted one-time moves.
2. One continuous scroll on the sessions list page (
d3f30c526)The "Active now" tray was pinned above the history list, permanently consuming viewport. It now renders as the history
SectionList'sListHeaderComponentand scrolls away with it in one continuous gesture.ScreenHeader, filter chips, and the search header stay pinned above the list.Single render site (mandatory): the
isLoadingearly return is gone — loading renders the sameSectionListwith empty sections and skeletons asListEmptyComponent, so the tray's local+N moreexpanded state survives the loading→rows transition. The remaining early returns (full-screen error, first-use empty) are gated on!isLoadingso a cold open with an empty cache cannot flash "No sessions yet";ListEmptyComponentisisLoading ? skeletons : body-emptyso a tray-first load shows skeletons under the tray instead of "No past sessions". The gating decision lives in a pureselectSessionListContentSurfaceselector with focused tests.Preserves #4773's behaviors: scroll restoration on back-navigation (baseline captured RESTORED pre-change), committed-search reset-to-top, tab re-tap scroll-to-top, pull-to-refresh and pagination — re-verified on device in this PR's E2E.
3. Reachable monospace blocks (
f674ff734)Long lines in tool-card mono blocks were cut at the right edge and unreachable: Yoga shrank the mono
Textto the card width, so the horizontalScrollView's content equaled its viewport and there was nothing to scroll (drags did nothing). All 13 horizontal-scroll sites (plus the preparation output tail) are deduped into oneMonoScrollBlock:shrink-0mono text keeps intrinsic width so the scroll has real content to deliver (the actual fix, not an affordance-only patch).directionalLockEnabled(iOS),nestedScrollEnabled(Android),canCancelContentTouches— a mostly-vertical drag keeps scrolling the conversation; a horizontal pan scrolls the block.Command text (bash
$ …, preparation setup command) intentionally stays soft-wrapped; markdown code fences are untouched (out of scope, they wrap intentionally);kilo-chat/**has zero diff.4. Session-open empty flash — reproduced and fixed (
bc44e50ea,a6bc40301)A fourth reported defect — opening an in-progress session flashing an empty state — initially did not reproduce on the unmodified baseline and was dropped. That conclusion was wrong: the race only becomes visible when the messages page fetch is slower than the first CLI heartbeat. A committed E2E latency harness (
bc44e50ea;E2E_LATENCY_*env knobs delaying tRPC queries / WS events, inert unless set) reproduces it on demand at 3s messages delay: skeleton (~0.5s) → "No messages yet" (~3s) → transcript, captured frame-level on video.Root cause in
cloud-agent-sdk/session-manager.ts(a6bc40301):onFirstActivityclearedisLoadingon the first CLI heartbeat while the snapshot page fetch was still in flight. Fix: while a remote session's initial history replay is pending,onFirstActivitykeepsisLoading;onReplayComplete(fired by the transport on every terminal snapshot outcome, success or failure) owns the clear. Regression tests cover the race plus the fallback and non-remote paths (171/171 green). This is the oneapps/webchange on this branch.Device verification on this head with the same 3s delay in place (fresh verifier agent): non-empty session opens skeleton → transcript directly with zero "No messages yet" frames; a genuinely-empty session still reaches "No messages yet" (~3.5s, no skeleton hang). Evidence in the comments.
Verification
pnpm format && pnpm typecheck && pnpm lint && pnpm check:unused && pnpm test— green (230 files / 1924 tests).a6bc40301with 3s messages latency in place: skeleton → transcript directly (zero empty-state frames) for a non-empty session; genuine-empty session still shows "No messages yet" — results in the comments.