Skip to content

fix(mobile): agent session screen UX — keep-awake, sheets, composer, costs - #4884

Merged
iscekic merged 18 commits into
mainfrom
session-page-ux-8225
Jul 31, 2026
Merged

fix(mobile): agent session screen UX — keep-awake, sheets, composer, costs#4884
iscekic merged 18 commits into
mainfrom
session-page-ux-8225

Conversation

@iscekic

@iscekic iscekic commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

This fixes ten agent session-screen defects in the mobile app.

  • Message long-press and copy (item 13): disables native selection inside message bubbles so it cannot overlap the details sheet, while preserving full message copy for text, reasoning, and tool output.
  • Composer reliability (items 19, 22, 26): restores focus after app foregrounding, dismisses the Android keyboard from composer-row drags without breaking text scrolling, and uses the proven mirror-Text measurement path for multiline growth, clamp, clear, and slash insertion.
  • Session sheet lifecycle (item 21): keeps a child sheet mounted through dismissal, releases it from iOS Modal.onDismiss after the native animation, and retains a non-iOS fallback. This removes the fixed-delay unmount race behind the iOS white flash.
  • Session UX and cost display (items 15, 23–25): scopes keep-awake to an active focused connected session (including transport-disconnect cleanup), suppresses display-zero Older activity residuals, improves slash separators, and bounds sheet titles so Done/Cancel stay usable.
  • Mobile guidance (item 12): links the app guide to the Kilo design repository.

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

  • Added expo-keep-awake and activates it only while a focused session visibly works.
  • Uses child-sheet mount state plus Modal.onDismiss on iOS; Android/non-iOS retains a bounded fallback.
  • Restored useTextHeight mirror-Text measurement after device evidence showed Fabric iOS onContentSizeChange cannot drive a fixed-height multiline input.
  • Added Android touch-identifier tracking to avoid multi-touch re-originating or cancelling keyboard dismissal.
  • Added unit coverage for child-sheet mount state, cost reconciliation, copy serialization, transcript selection context, and composer height helpers.

Verification

  • pnpm format && pnpm typecheck && pnpm lint && pnpm check:unused && pnpm test from apps/mobile before the final repair; repository CI is green on the current head.
  • Final child-sheet repair: pnpm test -- child-session-sheet-state.test.ts (2,529 tests), pnpm typecheck, and pnpm lint.
  • Final keep-awake disconnect repair: mobile format, typecheck, lint, and focused Vitest; Kilobot reviewed the current commit with No Issues Found.
  • Fresh cumulative impl-reviewer review of origin/main...524c31bd8: no findings.
  • Current-head iOS: child-session fixture opened, long title rendered with Done visible, Done dismissed the native sheet, and the same sheet reopened and dismissed again. Rapid dismissal frames contained no all-white frame.
  • Current-head Android verifier completed composer lifecycle, Android swipe-dismiss, multiline composer, and slash-menu checks; baseline restore passed.
  • Environment-limited by design: idle timer state (item 15), persisted-greater-than-live cost fixture (item 23), and voice draft input sizing (item 26) are covered by code inspection/unit tests rather than Appium.
  • pnpx expo-doctor: 19/20. The one failure is 13 pre-existing Expo patch-version mismatches already present on origin/main; this change adds only expo-keep-awake.

Visual Changes

Child session title and Done Child sheet reopened after native dismiss Android slash separators
25-child-sheet-open.png 21-reopen.png 11_item24_slash_separators.png

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

  • The lockfile change is limited to the expo-keep-awake importer entry; its package snapshot already existed transitively.
  • kilo-chat changes are limited to moving message-input-app-state and updating its import.
  • No .kilo_workflow/** path is included in this PR.

iscekic added 6 commits July 30, 2026 13:15
…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).
@iscekic iscekic self-assigned this Jul 30, 2026
Comment thread apps/mobile/src/components/agents/mono-scroll-block.tsx
Comment thread apps/mobile/src/components/agents/child-session-sheet-state.ts
Comment thread apps/mobile/src/components/agents/session-cost-breakdown.ts Outdated
Comment thread apps/mobile/src/components/agents/chat-composer.tsx Outdated
Comment thread apps/mobile/src/components/agents/session-detail-content.tsx Outdated
Comment thread apps/mobile/src/components/agents/session-detail-content.tsx Outdated
Comment thread apps/mobile/src/components/agents/chat-composer.tsx Outdated
Comment thread apps/mobile/src/components/sheet-header.tsx
@kilo-code-bot

kilo-code-bot Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Executive Summary

The 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: 524c31bd..39d33ef039d33ef0c (stop keep-awake after disconnect).

  • apps/mobile/src/components/agents/session-detail-content.tsx - 0 issues

Verification notes on the new code:

  • manager.atoms.agentStatus exists on the public atoms surface (packages/cloud-agent-sdk/src/session-manager.ts:249) and is kept in sync from session.state.getStatus() on every service-state notification (session-manager.ts:842), so the new subscription observes the same status the bottom bar uses.
  • AgentStatus is a discriminated union with a literal 'disconnected' member (packages/cloud-agent-sdk/src/types.ts:99-104), so agentStatus.type !== 'disconnected' is type-safe and cannot silently never match.
  • Both disconnect paths that leave a queued message behind set status = { type: 'disconnected' } (service-state.ts:189 for wrapper, service-state.ts:205 for transport-disconnected, which deliberately preserves pendingMessages), so the latch identified in the previous pass is now released.
  • The gate is not permanently sticky: reconnect clears the status back to idle via processSessionStatus (service-state.ts:126-129) and the snapshot path (service-state.ts:656-662), and session switches reset the atom (session-manager.ts:606), so keep-awake resumes for a genuinely working session.
  • Non-disconnected terminal statuses (error, interrupted) intentionally remain outside the gate; those paths keep a live socket, and cloud.message.failed / queue.changed still drain pendingMessages, so no new indefinite latch is introduced.

Resolved since the previous pass

  • session-detail-content.tsx:722-723 — keep-awake no longer stays latched on a focused screen whose cloud-agent socket dropped with a message still queued.

Assumptions

  • Read-only review: no builds, unit tests, typecheck, simulators, or emulators were run.
  • Findings in files outside this incremental diff are excluded per incremental scope; no previously reported finding remains unresolved on current code.
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 Summary

This pass resolves the child-sheet release timing by switching iOS to React Native's native Modal onDismiss; the only remaining finding is the pre-existing keep-awake latch that can hold the idle timer on a focused screen whose transport dropped with a message still queued.

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
apps/mobile/src/components/agents/session-detail-content.tsx 721 Carried: keepScreenAwake = isFocused && (isStreaming || pendingMessages.size > 0). pendingMessages is deliberately not cleared on transport-disconnected (packages/cloud-agent-sdk/src/service-state.ts:192-204), so a focused session screen with a permanently dropped socket and a queued message keeps useKeepAwake active indefinitely
Files Reviewed this pass (2 files, 1 new commit)

Incremental scope: 44bcf5af..524c31bd524c31bd8 (release child sheet after iOS dismiss).

  • apps/mobile/src/components/agents/session-detail-content.tsx - 0 new issues (1 carried finding in unchanged lines)
  • apps/mobile/src/components/agents/child-session-sheet.tsx - 0 issues

Verification notes on the new code:

  • onDismiss is optional on ChildSessionSheetProps and forwarded straight to RN Modal, which only invokes it on iOS, matching the Platform.OS !== 'ios' fallback gate in handleCloseChildSession; Platform was already imported (line 12), so no missing-import risk.
  • handleChildSheetDismiss clears the fallback timer before releasing, and releaseChildSessionSheet no-ops on a visible sheet, so a dismiss callback arriving after a reopen cannot unmount the newly presented sheet.
  • The unmount effect still clears childSheetReleaseTimeoutRef, and on iOS no timer is scheduled at all, so there is no leaked timer or post-unmount setState on either platform path.
  • On iOS the child-sheet identity now survives until the native pageSheet dismissal completes rather than a guessed 350 ms, and the identity is still released afterwards, so the bounded-mount fix from cfcb4f2bd is preserved (no return to permanent per-render child-session scans).

Resolved since the previous pass

  • session-detail-content.tsx:373 — the fixed 350 ms release no longer races the iOS dismiss animation; iOS uses Modal's onDismiss and the timer is kept only as the non-iOS path, where presentationStyle (and the white flash) do not apply.

Assumptions

  • Read-only review: no builds, unit tests, typecheck, simulators, or emulators were run.
  • Findings in files outside this incremental diff are excluded per incremental scope; the carried keep-awake finding already has an active inline thread, so it is reported summary-only to avoid a duplicate comment.

Fix these issues in Kilo Cloud

Previous review (commit 44bcf5a)

Status: 2 Issues Found | Recommendation: Address before merge

Executive Summary

This 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

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
apps/mobile/src/components/agents/session-detail-content.tsx 714 Partially fixed: isFocused now bounds the awake window to the visible screen, but pendingMessages is still deliberately not cleared on transport-disconnected (packages/cloud-agent-sdk/src/service-state.ts:192), so a focused screen with a permanently dropped socket and a queued message holds useKeepAwake indefinitely

SUGGESTION

File Line Issue
apps/mobile/src/components/agents/session-detail-content.tsx 373 CHILD_SHEET_RELEASE_DELAY_MS = 350 guesses the iOS pageSheet dismissal duration; if the timer wins the race the Modal unmounts mid-animation, reintroducing the item-21 white flash. RN Modal's iOS onDismiss gives a deterministic release point
Files Reviewed this pass (9 files, 4 new commits)

Incremental scope: feaae222..44bcf5af2b7cfa90a (item 13 copy path), cfcb4f2bd (child-sheet release + keep-awake scoping/focus gate), 27982652e (item 23 sub-display residuals), 44bcf5af4 (item 22 touch-identifier scoping).

  • apps/mobile/src/components/agents/session-detail-content.tsx - 2 issues (1 carried, 1 new)
  • apps/mobile/src/components/agents/collect-copyable-text.ts - 0 issues
  • apps/mobile/src/components/agents/child-session-sheet-state.ts - 0 issues
  • apps/mobile/src/components/agents/session-cost-breakdown.ts - 0 issues
  • apps/mobile/src/components/agents/chat-composer.tsx - 0 issues
  • apps/mobile/src/components/agents/session-context-sheet.tsx - 0 issues
  • apps/mobile/src/components/agents/message-copy-text.test.ts - 0 issues
  • apps/mobile/src/components/agents/child-session-sheet-state.test.ts - 0 issues
  • apps/mobile/src/components/agents/session-cost-breakdown.test.ts - 0 issues

Verification notes on the new code:

  • collectCopyableText now walks text / reasoning / tool parts with runtime guards; the direct state.input access matches the existing tool-card pattern (tool-cards/*.tsx), and the loosely typed test fixtures resolve to the { type: string } union arm, so the guards, not the declared shapes, drive behavior.
  • useIsFocused is imported from expo-router (same pattern as components/home/home-screen.tsx) and is called unconditionally before the component's single return, so hook ordering is intact; SessionDetailContent is only mounted from app/(app)/agent-chat/[session-id].tsx, so a navigation context always exists.
  • The child-sheet release timer is cleared on reopen and on unmount, and releaseChildSessionSheet is a no-op on a visible sheet, so no post-unmount setState or leaked timer.
  • getOlderActivityCostUsd now suppresses residuals that render as $0.0000, which also removes them from getModelsSectionCount; the pre-existing subagentCostUsd > 0 arm of that count is unchanged code and out of scope.
  • Android touch tracking is now identifier-scoped on start/move/end/cancel, so a second finger no longer re-origins or aborts the primary drag.

Resolved since the previous pass

  • mono-scroll-block.tsx:74 — in-bubble tool output / tool errors / reasoning now have a copy path again through collectCopyableText (details-sheet copy and the a11y rotor).
  • child-session-sheet-state.ts:46releaseChildSessionSheet plus the scheduled release bound the subagent sheet's mount to its lifetime, ending the permanent per-render child-session scans.
  • session-cost-breakdown.ts:211 — the residual gate is display-aligned, so sub-display deltas no longer render an "Older activity $0.0000" row or inflate Models (N).
  • session-detail-content.tsx:945 — keep-awake uses a session-scoped tag, so stacked session screens no longer fight over one activation.
  • chat-composer.tsx:322 — Android swipe-dismiss tracking is touch-identifier aware.

Assumptions

  • Read-only review: no builds, unit tests, typecheck, simulators, or emulators were run.
  • Findings on files outside this incremental diff (mono-scroll-block.tsx, sheet-header.tsx, chat-composer height/AppState threads) are excluded per incremental scope.

Fix these issues in Kilo Cloud

Previous review (commit feaae22)

Status: 7 Issues Found | Recommendation: Address before merge

Executive Summary

This pass contains no new branch code — only a main merge and workflow-file cleanup — so the highest-risk open item is unchanged: the item-13 selection gating that leaves in-bubble tool output, tool errors, and reasoning text with no copy path.

Overview

Severity Count
CRITICAL 0
WARNING 4
SUGGESTION 3
Issue Details (click to expand)

WARNING

File Line Issue
apps/mobile/src/components/agents/mono-scroll-block.tsx 74 In-bubble tool output / tool errors / reasoning lose native selection, and collectCopyableText only collects text parts — no copy path remains (also affects reasoning-part-renderer.tsx:49, tool-cards/bash-tool-card.tsx:32,47, per-card error text)
apps/mobile/src/components/agents/child-session-sheet-state.ts 46 closeChildSessionSheet keeps sheet, so the subagent sheet stays mounted for the screen's life; getChildSessionStreaming / getChildSessionHydrationState / getChildMessages full scans then run on every parent render forever after the first open
apps/mobile/src/components/agents/session-cost-breakdown.ts 211 1 µ$ epsilon vs 4-decimal formatCost renders an "Older activity $0.0000" row and bumps Models (N) for sub-50 µ$ persisted-vs-live deltas
apps/mobile/src/components/agents/session-detail-content.tsx 686 pendingMessages survives transport-disconnected, so a permanently dropped socket with a queued message latches keep-awake indefinitely; also not gated on screen focus

SUGGESTION

File Line Issue
apps/mobile/src/components/agents/session-detail-content.tsx 945 useKeepAwake() uses the shared default tag; two concurrently mounted session screens fight over one activation
apps/mobile/src/components/agents/chat-composer.tsx 317 Android onTouchStart / onTouchEnd overwrite / null androidDismissGestureRef per changed touch, so a second finger re-origins the drag and lifting any finger aborts detection for the still-active primary drag; the iOS RNGH pan is identifier-aware
apps/mobile/src/components/sheet-header.tsx 22 px-24 reserves ~96 pt per side vs ~70 pt of actual button width, ellipsizing titles earlier than necessary on narrow devices
Files Reviewed this pass (0 new code files)

Incremental scope: 2 commits since e442877bee1e04d2 (merge of origin/main @ 0a1e4074) and feaae222 (delete one .kilo_workflow learning file that main had already removed elsewhere). The branch authored no new application code in this range (git diff 0a1e4074..feaae222 matches the previous pass's file set line-for-line), so there are 0 new issues.

Merge-integration checks performed instead:

  • apps/mobile/AGENTS.md and apps/mobile/package.json were the only conflicted paths; both resolutions keep each side's content (branch drops the .kilo_workflow/WORKFLOW.md pointer + adds expo-keep-awake; main adds the Design section + expo-device).
  • pnpm-lock.yaml carries both expo-keep-awake@57.0.1 and main's expo-device under the apps/mobile importer, so the lockfile is consistent with the merged manifest.
  • No apps/mobile/src file changed by main in this range overlaps the PR's changed files, and the moved module @/lib/message-input-app-state has no stale importers at the old components/kilo-chat/ path (chat-composer.tsx:52, message-input-content.tsx:9, plus its colocated test).
  • SessionContextMetrics (changed on main) and SessionContextSheet / SheetHeader (changed here) have no prop-contract drift after the merge.

Resolved since the previous pass

  • The two outdated chat-composer.tsx inline comments (lastContentHeightRef caching at old line 200, AppState deps at old line 288) no longer correspond to live code: growth measurement now lives in useTextHeight / chat-composer-input-height.ts, and the AppState effect has [] deps with fire-time disabledRef / isSendingRef gating. Both are excluded above.

Assumptions

  • Read-only review: no builds, unit tests, typecheck, simulators, or emulators were run.
  • main's already-merged commits in this range were not re-reviewed; only their integration with this branch's changes was checked.
  • All 7 findings above were re-verified line-by-line against current HEAD feaae222 and are still present at the cited lines.

Fix these issues in Kilo Cloud

Previous review (commit e442877)

Status: 7 Issues Found | Recommendation: Address before merge

Executive Summary

The 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

Severity Count
CRITICAL 0
WARNING 4
SUGGESTION 3
Issue Details (click to expand)

WARNING

File Line Issue
apps/mobile/src/components/agents/mono-scroll-block.tsx 74 In-bubble tool output / tool errors / reasoning lose native selection, and collectCopyableText only collects text parts — no copy path remains (also affects reasoning-part-renderer.tsx:49, tool-cards/bash-tool-card.tsx:32,47, per-card error text)
apps/mobile/src/components/agents/child-session-sheet-state.ts 46 closeChildSessionSheet keeps sheet, so the subagent sheet stays mounted for the screen's life; getChildSessionStreaming / getChildSessionHydrationState / getChildMessages full scans then run on every parent render forever after the first open
apps/mobile/src/components/agents/session-cost-breakdown.ts 211 1 µ$ epsilon vs 4-decimal formatCost renders an "Older activity $0.0000" row and bumps Models (N) for sub-50 µ$ persisted-vs-live deltas
apps/mobile/src/components/agents/session-detail-content.tsx 686 pendingMessages survives transport-disconnected, so a permanently dropped socket with a queued message latches keep-awake indefinitely; also not gated on screen focus

SUGGESTION

File Line Issue
apps/mobile/src/components/agents/session-detail-content.tsx 945 useKeepAwake() uses the shared default tag; two concurrently mounted session screens fight over one activation
apps/mobile/src/components/agents/chat-composer.tsx 317 Android onTouchStart / onTouchEnd overwrite / null androidDismissGestureRef per changed touch, so a second finger re-origins the drag and lifting any finger aborts detection for the still-active primary drag; the iOS RNGH pan is identifier-aware
apps/mobile/src/components/sheet-header.tsx 22 px-24 reserves ~96 pt per side vs ~70 pt of actual button width, ellipsizing titles earlier than necessary on narrow devices
Files Reviewed this pass (1 file)

Incremental scope: 1 commit since e43025a3 (e442877b — evaluate composer AppState refocus gate at fire time, item 19).

  • apps/mobile/src/components/agents/chat-composer.tsx — 0 new issues. The focus-restore timeout is now scheduled unconditionally on transition.shouldFocus and gated at fire time through disabledRef / isSendingRef, with [] deps on the AppState effect, so a disabled/isSending flip can no longer tear down the subscription and cancel a pending restore after restoreFocusOnActive was already consumed. clearRestoreFocusTimeout still runs on the blur transition, before each re-schedule, and on unmount, so no timer escapes the component. Render-phase ref assignment matches the existing pattern in this codebase (use-voice-input.ts:64-70, session-detail-content.tsx:260, share-prefill.ts:82-86) and is not flagged.

Resolved since the previous pass

  • chat-composer.tsx — the [disabled, isSending] re-subscription that could silently drop the 100 ms focus restore is fixed by this commit; its inline comment is now outdated on GitHub.

Assumptions

  • Read-only review: no builds, unit tests, typecheck, simulators, or emulators were run; the AppState/refocus path was assessed by inspection of resolveMessageInputAppStateTransition and the composer's timer lifecycle.
  • The 7 findings above come from files untouched by this commit (git diff --name-only e43025a3..e442877b lists chat-composer.tsx only); each was re-verified against current HEAD e442877b and is still present at the cited line.

Fix these issues in Kilo Cloud

Previous review (commit e43025a)

Status: 8 Issues Found | Recommendation: Address before merge

Executive Summary

The 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

Severity Count
CRITICAL 0
WARNING 4
SUGGESTION 4
Issue Details (click to expand)

WARNING

File Line Issue
apps/mobile/src/components/agents/mono-scroll-block.tsx 74 In-bubble tool output / tool errors / reasoning lose native selection, and collectCopyableText only collects text parts — no copy path remains (also affects reasoning-part-renderer.tsx:49, tool-cards/bash-tool-card.tsx:32,47, per-card error text)
apps/mobile/src/components/agents/child-session-sheet-state.ts 46 closeChildSessionSheet keeps sheet, so the subagent sheet stays mounted for the screen's life; getChildSessionStreaming / getChildSessionHydrationState / getChildMessages full scans then run on every parent render forever after the first open
apps/mobile/src/components/agents/session-cost-breakdown.ts 211 1 µ$ epsilon vs 4-decimal formatCost renders an "Older activity $0.0000" row and bumps Models (N) for sub-50 µ$ persisted-vs-live deltas
apps/mobile/src/components/agents/session-detail-content.tsx 686 pendingMessages survives transport-disconnected, so a permanently dropped socket with a queued message latches keep-awake indefinitely; also not gated on screen focus

SUGGESTION

File Line Issue
apps/mobile/src/components/agents/chat-composer.tsx 304 New: Android onTouchStart / onTouchEnd overwrite / null androidDismissGestureRef per changed touch, so a second finger re-origins the drag (missed threshold or double Keyboard.dismiss()) and lifting any finger aborts detection for the still-active primary drag; the iOS RNGH pan is identifier-aware
apps/mobile/src/components/agents/session-detail-content.tsx 945 useKeepAwake() uses the shared default tag; two concurrently mounted session screens fight over one activation
apps/mobile/src/components/agents/chat-composer.tsx 276 [disabled, isSending] deps re-subscribe AppState per send and can cancel the pending 100 ms focus restore with no retry
apps/mobile/src/components/sheet-header.tsx 22 px-24 reserves ~96 pt per side vs ~70 pt of actual button width, ellipsizing titles earlier than necessary on narrow devices
Files Reviewed this pass (1 file)

Incremental scope: 1 commit since 7c6a8bbb (e43025a3 — dismiss keyboard on Android EditText swipe-down, item 22).

  • apps/mobile/src/components/agents/chat-composer.tsx — 1 new suggestion (line 304). The extracted DISMISS_KEYBOARD_ACTIVE_OFFSET_Y / DISMISS_KEYBOARD_FAIL_OFFSET_X constants match the RNGH pan thresholds, the inputScrollable guard mirrors .enabled(!inputScrollable), dx/dy are measured from the recorded origin, the dismissed/failed latches prevent repeat dismissals within one drag, and the props object is undefined on iOS so the RNGH-only path is preserved there.

Assumptions

  • Read-only review: no builds, unit tests, typecheck, simulators, or emulators were run, so the Android-only touch path was assessed by inspection of RN's Android bubbling touch events and RNGH pan semantics.
  • The redundancy between onTouchMove and the side-effecting onMoveShouldSetResponderCapture predicate is intentional per the in-file comment (responder capture additionally cancels in-flight child presses), so it is not raised as a separate finding.
  • The 7 findings above from files untouched by this commit were re-verified as still present at HEAD e43025a3; the previously flagged lastContentHeightRef stale-height defect stays resolved (its inline comment is now outdated on GitHub).

Fix these issues in Kilo Cloud

Previous review (commit 7c6a8bb)

Status: 7 Issues Found | Recommendation: Address before merge

Executive Summary

The new commit replaces the composer's onContentSizeChange height math with the existing mirror-Text useTextHeight hook and resolves the previously flagged stale-height bug; the highest-risk open item is now the item-13 selection gating that removes the only copy path for in-bubble tool output, tool errors, and reasoning text.

Overview

Severity Count
CRITICAL 0
WARNING 4
SUGGESTION 3
Issue Details (click to expand)

WARNING

File Line Issue
apps/mobile/src/components/agents/mono-scroll-block.tsx 74 In-bubble tool output / tool errors / reasoning lose native selection, and collectCopyableText only collects text parts — no copy path remains (also affects reasoning-part-renderer.tsx:49, tool-cards/bash-tool-card.tsx:32,47, per-card error text)
apps/mobile/src/components/agents/child-session-sheet-state.ts 46 closeChildSessionSheet keeps sheet, so the subagent sheet stays mounted for the screen's life; getChildSessionStreaming / getChildSessionHydrationState / getChildMessages full scans then run on every parent render forever after the first open
apps/mobile/src/components/agents/session-cost-breakdown.ts 211 1 µ$ epsilon vs 4-decimal formatCost renders an "Older activity $0.0000" row and bumps Models (N) for sub-50 µ$ persisted-vs-live deltas
apps/mobile/src/components/agents/session-detail-content.tsx 686 pendingMessages survives transport-disconnected, so a permanently dropped socket with a queued message latches keep-awake indefinitely; also not gated on screen focus

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 main

iscekic added 3 commits July 30, 2026 14:32
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.
Comment thread apps/mobile/src/components/agents/chat-composer.tsx
iscekic added 7 commits July 30, 2026 20:20
…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.
Comment thread apps/mobile/src/components/agents/session-detail-content.tsx
@iscekic iscekic added the human-ready The PR is ready for human review. label Jul 31, 2026
@iscekic
iscekic enabled auto-merge (squash) July 31, 2026 11:33
@iscekic
iscekic merged commit 24ab65c into main Jul 31, 2026
72 checks passed
@iscekic
iscekic deleted the session-page-ux-8225 branch July 31, 2026 11:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

human-ready The PR is ready for human review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants