fix(mobile): agent session screen UX — keep-awake, sheets, composer, costs - #4884
Conversation
…eet dismiss Install expo-keep-awake and mount a conditional useKeepAwake host while the session is streaming or has pending messages, so the idle timer stays off exactly while the UI shows the agent as working (item 15). Mount the child session sheet behind a separate visible flag instead of unmounting it on close, so iOS plays the native slide-down animation instead of flashing the white window behind (item 21).
Reconcile the persisted page total against the live cost breakdown when history is unpaginated: an Older activity row carries the residual exactly when it exceeds the 1 microdollar epsilon, so Total always matches the visible rows (item 23).
Gate selectable behind InMessageBubbleContext so a long-press anywhere in a message opens the details sheet without the iOS selection callout floating over it (item 13). Preparation group and child-sheet texts keep native selection as their only copy path; details-sheet copy and the a11y rotor path are unchanged.
Use the border-border theme token between slash-command rows and drop the trailing separator (item 24). Bound sheet titles in a flex-1 px-24 wrapper with numberOfLines=1 so long titles ellipsize without covering the Done/Cancel controls, which render after the title to stay on top (item 25).
…growth Move message-input-app-state to src/lib as the shared home and reuse its blur-on-background / delayed-refocus pattern in the agents composer (item 19). Replace the hidden mirror-Text measurement with the TextInput's own onContentSizeChange plus pure, unit-tested reducers, so the composer reliably grows to its max height then scrolls (item 26). Add a downward-pan GestureDetector scoped to the composer input row so a swipe down dismisses the keyboard when the input is not scrollable (item 22).
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Executive SummaryThe incremental commit closes the last outstanding finding by gating keep-awake on a non-disconnected agent status, and the single changed line is correct against the SDK status lifecycle. Files Reviewed this pass (1 file, 1 new commit)Incremental scope:
Verification notes on the new code:
Resolved since the previous pass
Assumptions
Previous Review Summaries (6 snapshots, latest commit 524c31b)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit 524c31b)Status: 1 Issue Found | Recommendation: Address before merge Executive SummaryThis pass resolves the child-sheet release timing by switching iOS to React Native's native Overview
Issue Details (click to expand)WARNING
Files Reviewed this pass (2 files, 1 new commit)Incremental scope:
Verification notes on the new code:
Resolved since the previous pass
Assumptions
Fix these issues in Kilo Cloud Previous review (commit 44bcf5a)Status: 2 Issues Found | Recommendation: Address before merge Executive SummaryThis pass fixed four of the previous findings; the remaining risk is the keep-awake latch that still holds the idle timer on a focused screen whose transport dropped with a queued message, plus a new duration-guessed child-sheet unmount that can race the native dismiss animation. Overview
Issue Details (click to expand)WARNING
SUGGESTION
Files Reviewed this pass (9 files, 4 new commits)Incremental scope:
Verification notes on the new code:
Resolved since the previous pass
Assumptions
Fix these issues in Kilo Cloud Previous review (commit feaae22)Status: 7 Issues Found | Recommendation: Address before merge Executive SummaryThis pass contains no new branch code — only a Overview
Issue Details (click to expand)WARNING
SUGGESTION
Files Reviewed this pass (0 new code files)Incremental scope: 2 commits since Merge-integration checks performed instead:
Resolved since the previous pass
Assumptions
Fix these issues in Kilo Cloud Previous review (commit e442877)Status: 7 Issues Found | Recommendation: Address before merge Executive SummaryThe new commit correctly removes the AppState re-subscription hazard in the composer and introduces no new defects; the highest-risk open item remains the item-13 selection gating that leaves in-bubble tool output, tool errors, and reasoning text with no copy path. Overview
Issue Details (click to expand)WARNING
SUGGESTION
Files Reviewed this pass (1 file)Incremental scope: 1 commit since
Resolved since the previous pass
Assumptions
Fix these issues in Kilo Cloud Previous review (commit e43025a)Status: 8 Issues Found | Recommendation: Address before merge Executive SummaryThe new commit adds an Android-only JS touch fallback for swipe-to-dismiss in the composer whose tracking ref is not touch-identifier scoped; the highest-risk open item remains the item-13 selection gating that leaves in-bubble tool output, tool errors, and reasoning text with no copy path. Overview
Issue Details (click to expand)WARNING
SUGGESTION
Files Reviewed this pass (1 file)Incremental scope: 1 commit since
Assumptions
Fix these issues in Kilo Cloud Previous review (commit 7c6a8bb)Status: 7 Issues Found | Recommendation: Address before merge Executive SummaryThe new commit replaces the composer's Overview
Issue Details (click to expand)WARNING
SUGGESTION| File | Line | Issue | [Snapshot truncated.] Additional previous summary content was truncated to keep this comment within platform limits. Reviewed by claude-opus-5 · Input: 40 · Output: 5.7K · Cached: 841.4K Review guidance: REVIEW.md from base branch |
iOS Fabric emits TextInput onContentSizeChange only from updateLayoutMetrics, so a fixed-height uncontrolled input never reports growth past the initial layout — the onContentSizeChange reducers could never grow the composer. Restore the mirror-Text measurement (useTextHeight), which follows JS text state deterministically and was iOS-proven on this composer. Keep the pure scroll-threshold helper; AppState focus restore (item 19) and swipe-down dismiss (item 22) unchanged.
ReactEditText requests disallow-intercept on every ACTION_DOWN, so the wrapper's RNGH pan never sees the stream start from the focused input. Track bubbled JS touch moves on the composer host View and dismiss at the same 24px threshold, claiming the responder there. Android-only; the iOS RNGH path is unchanged.
…em 19) The effect's [disabled, isSending] deps let any flip re-run the effect and clear the pending 100ms focus restore after 'active' had already consumed the restore flag — Android intermittently (5/16 cycles) never restored the composer keyboard. Subscribe once, schedule unconditionally on shouldFocus, and gate on live refs at fire time.
Main deleted all of .kilo_workflow in #4909; the workflow now lives in a private repo. Remove the branch learning that survived the merge (already ported there) so the PR carries zero workflow diffs.
In-bubble native selection was disabled so the iOS callout cannot float over the details sheet, but both remaining copy paths (details-sheet Copy message, a11y rotor) collect only text parts — tool commands/outputs/ errors and reasoning lost their only copy route. Extend collectCopyableText to serialize reasoning and tool parts; parts with no payload yet stay excluded. Addresses Kilobot thread on mono-scroll-block.tsx.
Child sheet (item 21): the mounted-during-close pattern exists only to let the native dismiss animation play, but the sheet identity lived on for the rest of the screen, running child-transcript scans on every streaming render. Release it 350ms after close (covers the animation; the white flash is iOS-specific); a reopen before the release wins and a visible sheet is never released. Keep-awake (item 15): gate on useIsFocused so a screen covered by a pushed route stops holding the OS idle timer, and scope the expo tag by session id so stacked session screens cannot release each other's wake lock. Addresses two Kilobot threads on session-detail-content.tsx + child-session-sheet-state.ts.
The 1µ$ reconciliation epsilon let 2-49µ$ residuals render an "Older activity $0.0000" row. Gate on what formatCost actually displays, mirroring formatSessionTotalCost's suppression, and reword the subtitle — the residual also absorbs ingest gaps when the full history is loaded. Addresses Kilobot thread on session-cost-breakdown.ts.
… (item 22) topTouch* events fire per changed touch: a second finger overwrote the tracked gesture origin or reset it mid-drag, breaking the primary finger's swipe-to-dismiss. Track nativeEvent.identifier and ignore other identifiers, matching the RNGH pan's identifier awareness. Addresses Kilobot thread on chat-composer.tsx.
Summary
This fixes ten agent session-screen defects in the mobile app.
Modal.onDismissafter the native animation, and retains a non-iOS fallback. This removes the fixed-delay unmount race behind the iOS white flash.Why
The session page had reproducible mobile regressions around composer lifecycle, native keyboard gestures, copy affordances, child-session dismissal, long titles, and cost reconciliation. The fixes preserve platform-native behavior while keeping each change scoped to the session surface.
How
expo-keep-awakeand activates it only while a focused session visibly works.Modal.onDismisson iOS; Android/non-iOS retains a bounded fallback.useTextHeightmirror-Text measurement after device evidence showed Fabric iOSonContentSizeChangecannot drive a fixed-height multiline input.Verification
pnpm format && pnpm typecheck && pnpm lint && pnpm check:unused && pnpm testfromapps/mobilebefore the final repair; repository CI is green on the current head.pnpm test -- child-session-sheet-state.test.ts(2,529 tests),pnpm typecheck, andpnpm lint.impl-reviewerreview oforigin/main...524c31bd8: no findings.pnpx expo-doctor: 19/20. The one failure is 13 pre-existing Expo patch-version mismatches already present onorigin/main; this change adds onlyexpo-keep-awake.Visual Changes
The iOS dismissal defect was transient (an all-white animation frame), so the final evidence shows the final mounted child sheet and its successful post-dismiss reopen rather than a stable static “before” state.
Reviewer Notes
expo-keep-awakeimporter entry; its package snapshot already existed transitively.kilo-chatchanges are limited to movingmessage-input-app-stateand updating its import..kilo_workflow/**path is included in this PR.