Skip to content

fix(mobile): keep a form sheet open while its content scrolls - #8249

Merged
iscekic merged 12 commits into
mainfrom
drain/keep-sheet-open-while-scrolling
Oct 21, 2026
Merged

iscekic merged 12 commits into
mainfrom
drain/keep-sheet-open-while-scrolling

Conversation

@iscekic

@iscekic iscekic commented Oct 20, 2026

Copy link
Copy Markdown
Collaborator

What changed

A full-window sheet no longer dismisses when the user drags the content of its form. Before, on iOS the native sheet claimed every downward drag that started over the form's scroll view, so scrolling the form back up dismissed the sheet and dropped its values. Now the shared sheet answer is:

  • Sheet gains dismissGestures (off for the full-window SessionPageSheet, so the drag belongs to the content) and onRequestDismiss, which lets a form answer a dismissal the sheet received by itself (Android Back) before it happens.
  • @expo/ui patch: the bottom sheet separates its drag gesture from its back/scrim dismissal. Because Material3 hides the sheet by itself before JavaScript hears the request (measured on the emulator), the Kotlin side now owns Back: it passes shouldDismissOnBackPress = false to Material3 and forwards the press to JavaScript through a BackHandler, so the sheet hides only when JavaScript drops it. apps/mobile/package.json compiles expo-ui from its Android sources (its Gradle project name) so the Kotlin patch is in the build.
  • useSheetDismissGuard: the shared Keep editing / Discard confirmation. A form reads its own unsaved state at the moment of the dismissal and only closes after Discard.
  • Every form sheet uses it: the profile agent, skill, slash command and MCP sheets; Save as profile; the chat remote-MCP form; the chat backend form; and the todo list settings and invite member route sheets. The invite sheet gained a header with Cancel and Send invite, because its only exit was the swipe.
  • apps/mobile/src/i18n/locales/en.json plus every locale catalog: common.discardChangesTitle and common.discardChangesMessage.

End-to-end evidence

Emulator (omp36, local backend, Metro, fresh dev client built from this branch's native inputs):

  • Add Agent sheet: changed visibility to All, scrolled down to the tools, then dragged the form back up three times — the sheet stayed open at the top with the typed values.
  • Cancel with edits showed Discard changes? / Your changes will be lost. / Discard / Keep editing; Keep editing kept the sheet and its scroll position; Discard closed it to the agents list.
  • Android Back on a dirty form showed the same confirmation; on a clean form it closed the sheet and left the agents list in place.
  • Same flow in the Skills sheet: Back on a dirty form showed the confirmation.

iOS simulator (lane, dev client of this branch's fingerprint, Metro on the lease's offset):

  • The reported gesture was reproduced on the same build by switching the sheet's drag gesture back on: three downward drags dismissed the Add Agent sheet and returned to the agents list.
  • With the change, the same three drags left the sheet open with alpha still in Agent name, and Cancel showed Discard changes? / Your changes will be lost. with Discard and Keep editing; Keep editing returned to the sheet intact.
  • The simulator screenshot endpoint does not include the native confirmation sheet, so the confirmation is evidenced by the accessibility snapshot text, not an image.

Checks

  • pnpm --filter kilo-app typecheck, lint, check:i18n, check:plurals
  • pnpm --filter kilo-app test (whole suite) and node tools/i18n/check-catalogs.mjs

Visual proof

Screenshots are attached in a follow-up comment on this pull request.

Proxy answers

The owner's proxy gave these answers in the owner's place.

  1. Question: Shared change: How widely should the dismissal fix apply? The brief says fix the shared form-sheet presentation, and the scroll-to-dismiss happens through Sheet/SessionPageSheet used by ~15 surfaces (forms plus read-only detail sheets).
    Answer: Shared + all profile form sheets
    Confidence: 0.82 · Precedent: Fix the root cause and the class: session 75f8833fe8c1, "fix this for all shared services model pickers on all surfaces"
  2. Question: Dirty confirm: Requirement 2 says the sheet dismisses only by explicit close or a drag on grabber/header, and requirement 3 asks for a Keep editing / Discard confirm when dismissing with unsaved changes. With pan-down disabled, on these full-window sheets the only dismissal paths left are explicit close and Android Back. Should the confirm gate those dismissal paths when the form is dirty?
    Answer: Yes, confirm on dirty dismissal
    Confidence: 0.82 · Precedent: Fix the root cause and the class: session 00eb2a1d3e4b, "if anything repeats the same mistake over and over again, fix the class"
  3. Question: Android back: Disabling pan-down on Android also disables Back-button dismissal and outside-tap for the sheet (library maps one prop to all three). Android Back must keep closing the sheet, routed through the dirty gate when the form is dirty. Acceptable?
    Answer: BackHandler routes to confirm
    Confidence: 0.85 · Precedent: UX best-in-class standard: session 00eb2a1d3e4b, "design and UX MUST BE BEST-IN-CLASS"
  4. Question: Read-only sheets: Non-form SessionPageSheet surfaces (session details, file preview, tool runs) also lose the pan-down dismiss gesture with the shared change. Their dismissal stays via the header Done control, which is how they are designed to close. Accept that, or scope pan-down-disabling to the four form sheets only?
    Answer: Accept
    Confidence: 0.60 · Precedent: Keep similar elements consistent across surfaces: session a247f3c19b1e, the thought row should match the tool call row
  5. Question: Android Back: With swipe-to-dismiss off, Android Back must still close the sheet and go through the dirty confirm. @expo/ui ties gesture, back and scrim dismissal to one prop, so how should Android Back be kept?
    Answer: Patch @expo/ui JS (Recommended)
    Confidence: 0.65 · Precedent: Fix the root cause and the class of the issue: "fix class of issue preventing progress"; Choose maximum performance and full automation even if this adds a dependency: "I don't mind an additional dep if performance is legit better"
  6. Question: Dirty confirm scope: Which form sheets get the Keep editing / Discard confirm in this PR?
    Answer: Every form sheet
    Confidence: 0.60 · Precedent: Fix the root cause and the class of the issue on every affected surface: "fix this for all shared services model pickers on all surfaces (web, app)"
  7. Question: Confirm choices: Should the dirty confirm offer a third Save action, or only Keep editing / Discard?
    Answer: Keep editing / Discard only (Recommended)
    Confidence: 0.75 · Precedent: Do the least work possible with no drop in quality: "skip work that doesn't need to be done"
  8. Question: Route form sheets: Two of the named forms are navigation formSheet routes, not the shared Sheet: todo list settings (has Cancel) and invite member (no close control at all — its only way out is the native swipe/back). Disabling the swipe there without adding a control would trap the user. How should those two be handled?
    Answer: Disable swipe + add close control (Recommended)
    Confidence: 0.75 · Precedent: Fix the root cause and the class of the issue on every affected surface: session 75f8833fe8c1, "fix this for all shared services model pickers on all surfaces (web, app)"

A full-window sheet no longer dismisses on a downward drag, so scrolling a
form back up keeps the sheet and the values typed into it. Every form sheet
asks Keep editing / Discard before a close, or Android Back, drops unsaved
edits.

The @expo/ui patch separates a bottom sheet's drag gesture from its back and
scrim dismissal, and lets the sheet own the Back press so a caller can answer
it first.
The sheet patch carries Kotlin, and expo-ui otherwise resolves to its
published artifact. Its Gradle project name is expo-ui.
The invite sheet now opens a Keep editing / Discard confirm and answers
Android Back, so its mounted suite needs those modules stubbed.
The host stubs expose props untyped, so one small narrowing helper per
suite replaces the inline casts.
@iscekic iscekic added drain Work item from the findings drain explorer:ios Finding surfaced by the iOS explorer labels Oct 20, 2026
@iscekic

iscekic commented Oct 20, 2026

Copy link
Copy Markdown
Collaborator Author

bot: Verified on the Android emulator (local backend and Metro, dev client built from this branch's native inputs) and on an iOS simulator lane (dev client of this branch's fingerprint, Metro on the lease's offset). Fake local account, no production data.

Android, profile agent sheet:

  1. The Add agent sheet open on a fresh profile agent.
  2. After selecting All, scrolling down to the tools, and three drags back up inside the form: the sheet is still open at the top, values intact.
  3. Cancel with edits: the Keep editing / Discard confirmation.
  4. Keep editing: the sheet stays with its scroll position and values. Android Back shows the same confirmation on a dirty form and closes a clean one.

iOS, same sheet: after three downward drags inside the form the sheet stays open (the reported gesture dismissed it before this change; the same build with the old gesture reproduced that dismissal). The simulator screenshot endpoint does not include the native confirmation sheet, so the confirmation itself is evidenced by the accessibility snapshot text, not an image.

Add agent sheet open on Android

Sheet still open after scrolling the form back up

Keep editing and Discard confirmation

Sheet kept after choosing Keep editing

iOS sheet still open after the drag gesture

expo-ui only reached androidx.activity.compose transitively, so compiling
the patched module failed on CI. Declare it in the patched module.
@kilo-code-bot

kilo-code-bot Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Executive Summary

The only change since the last reviewed commit (e8cbf5ce) is a documentation comment in use-backend-form-actions.ts that describes the hook/form split; it carries no behavioral change, and the previously raised findings are fixed in current code.

Verification

  • Incremental diff (e8cbf5ce..66e7c2e) touches one file, +3/−1, replacing a "lives outside the form because the JSX is long" comment with a responsibility split between fields/layout and requests/state. No code path changed.
  • Targeted re-checks of earlier findings: remote-mcp-server-form.tsx:119 now compares the token only for bearer, and the new test at remote-mcp-server-form.mounted.test.tsx:279 covers the OAuth → bearer → OAuth round trip; invite-member-sheet.tsx and todo-list-settings-sheet.tsx use doneDisabled plus focus-scoped BackHandler; the profile form sheets and SaveProfileSheet pass doneDisabled instead of disabled.
  • I did not execute the mobile test suite (read-only review), so this is a static assessment.
Files Reviewed (1 file)
  • apps/mobile/src/components/chat/use-backend-form-actions.ts - no issues
Previous Review Summaries (6 snapshots, latest commit e8cbf5c)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit e8cbf5c)

Status: No Issues Found | Recommendation: Merge

Executive Summary

The only change since the last reviewed commit (bf62c03) is a new mounted test in remote-mcp-server-form.mounted.test.tsx, and it correctly pins the OAuth → bearer → OAuth dirty-check regression fixed by the bearer-only token comparison.

Verification

  • Incremental diff (bf62c03..e8cbf5ce) touches one file, +25 lines: a test that mounts an oauth server, switches to bearer, types a token, switches back to oauth, and asserts dirty.at(-1) is false.
  • That assertion fails under the previous next.method !== 'none' / unconditional token comparison, so it genuinely covers the fix at remote-mcp-server-form.tsx:119 rather than passing vacuously.
  • The previously active suggestion on this test is now resolved; no other files changed, and the earlier doneDisabled / useFocusEffect fixes were spot-checked as present in current code.
  • I did not execute the test suite (read-only review), so this is a static correctness assessment of the added case.
Files Reviewed (1 file)
  • apps/mobile/src/components/chat/remote-mcp-server-form.mounted.test.tsx - no issues

Previous review (commit bf62c03)

Status: 1 Issue Found | Recommendation: Address before merge

Executive Summary

No code changed since the last reviewed commit (40948d2), so the only unresolved item is the still-valid test-coverage gap for the OAuth → bearer → OAuth dirty-check regression.

Overview

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

SUGGESTION

File Line Issue
apps/mobile/src/components/chat/remote-mcp-server-form.mounted.test.tsx 230 The dirty-report test mounts a bearer server, so it also passes under the previous next.method !== 'none' condition. Add an oauth → bearer (type a token) → oauth case asserting dirty.at(-1) is false; that is the exact regression the next.method === 'bearer' guard at remote-mcp-server-form.tsx:119 fixes.
Files Reviewed (1 file)
  • apps/mobile/src/components/chat/remote-mcp-server-form.mounted.test.tsx - 1 suggestion (carried forward; unchanged since the previous review, whitespace case at lines 253-279 remains correct)

Fix these issues in Kilo Cloud

Previous review (commit 40948d2)

Status: 1 Issue Found | Recommendation: Address before merge

Executive Summary

The new whitespace test is correct, but the OAuth-to-bearer-and-back regression fixed in remote-mcp-server-form.tsx still has no test that fails on the previous condition.

Overview

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

SUGGESTION

File Line Issue
apps/mobile/src/components/chat/remote-mcp-server-form.mounted.test.tsx 230 The dirty-report tests mount a bearer server, so they pass on the old next.method !== 'none' condition as well. Add an oauth → bearer (type a token) → oauth case asserting dirty.at(-1) is false, which is the exact regression the next.method === 'bearer' guard fixes.
Files Reviewed (1 file)
  • apps/mobile/src/components/chat/remote-mcp-server-form.mounted.test.tsx - 1 suggestion (carried forward; the new whitespace case at lines 253-279 is correct and adds useful coverage)

Fix these issues in Kilo Cloud

Previous review (commit b366a0d)

Status: 1 Issue Found | Recommendation: Address before merge

Executive Summary

The incremental fix correctly stops counting a typed token when the auth method is not bearer; the remaining gap is that the new test does not cover the OAuth-to-bearer-and-back case the change fixes.

Overview

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

SUGGESTION

File Line Issue
apps/mobile/src/components/chat/remote-mcp-server-form.mounted.test.tsx 230 The added test mounts a bearer server and would pass on the old condition, so the OAuth token regression this commit fixes is not pinned. Add an oauth → bearer (type token) → oauth case asserting dirty.at(-1) is false.
Files Reviewed (2 files)
  • apps/mobile/src/components/chat/remote-mcp-server-form.tsx - no issues (condition now compares the token only for bearer)
  • apps/mobile/src/components/chat/remote-mcp-server-form.mounted.test.tsx - 1 suggestion

Fix these issues in Kilo Cloud

Previous review (commit 1e07de7)

Status: 1 Issue Found | Recommendation: Address before merge

Executive Summary

The incremental fix keeps Cancel usable during saves and scopes Back handling to focus, but the remote MCP dirty check still reports an edit when an OAuth server's typed token is dropped by the saved draft.

Overview

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

SUGGESTION

File Line Issue
apps/mobile/src/components/chat/remote-mcp-server-form.tsx 119 next.method !== 'none' still counts a typed token for oauth, which authOf discards, so an unchanged OAuth server can prompt "Discard changes?" after switching to bearer, typing a token, and switching back.
Files Reviewed (12 files)
  • apps/mobile/src/components/organization/invite-member-sheet.tsx - no new issues (prior Cancel trap fixed; Back now focus-scoped)
  • apps/mobile/src/components/todos/todo-list-settings-sheet.tsx - no new issues (prior Cancel and Back-scope issues fixed)
  • apps/mobile/src/components/agents/save-profile-sheet.tsx - no new issues
  • apps/mobile/src/components/profiles/agent-form-sheet.tsx - no new issues
  • apps/mobile/src/components/profiles/skill-form-sheet.tsx - no new issues
  • apps/mobile/src/components/profiles/mcp-form-sheet.tsx - no new issues
  • apps/mobile/src/components/profiles/kilo-command-form-sheet.tsx - no new issues
  • apps/mobile/src/components/sheet-header.tsx - no issues
  • apps/mobile/src/components/sheet-header.mounted.test.tsx - no issues
  • apps/mobile/src/components/picker-sheet.tsx - no issues
  • apps/mobile/src/components/chat/remote-mcp-server-form.tsx - 1 suggestion
  • apps/mobile/src/components/organization/member-limit-sheet.mounted.test.tsx - no issues

Fix these issues in Kilo Cloud

Previous review (commit ad8403d)

Status: 5 Issues Found | Recommendation: Address before merge

Executive Summary

The invite-member sheet becomes impossible to close on iOS until a valid email is typed, because its header Cancel is coupled to validation while this PR removes the sheet's swipe dismissal.

Overview

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

CRITICAL

File Line Issue
apps/mobile/src/components/organization/invite-member-sheet.tsx 96 SheetHeader disables Cancel together with Done, so with the route's swipe disabled on iOS there is no way to leave the invite sheet until a valid email is typed.

WARNING

File Line Issue
apps/mobile/src/components/todos/todo-list-settings-sheet.tsx 118 New-list flow: Cancel is disabled while the name is empty, and the route's swipe is disabled by this PR, trapping iOS users.
apps/mobile/src/components/todos/todo-list-settings-sheet.tsx 73 The BackHandler listener is not focus-scoped and steals Back from the modal picker routes this sheet pushes, showing a discard prompt that pops only the picker.
apps/mobile/src/components/agents/session-page-sheet.tsx 63 With drag dismissal removed for every SessionPageSheet, form sheets that disable both header controls while saving have no iOS exit until the save settles.

SUGGESTION

File Line Issue
apps/mobile/src/components/chat/remote-mcp-server-form.tsx 117 Dirty check includes a token that the saved draft ignores when the auth method is not bearer, producing a spurious discard prompt.
Files Reviewed (119 files)
  • apps/mobile/src/components/ui/sheet.tsx - no issues
  • apps/mobile/src/components/ui/use-sheet-dismiss-guard.ts - no issues
  • apps/mobile/src/components/ui/sheet.mounted.test.tsx - no issues
  • apps/mobile/src/components/ui/use-sheet-dismiss-guard.mounted.test.tsx - no issues
  • apps/mobile/src/components/agents/session-page-sheet.tsx - 1 warning
  • apps/mobile/src/components/agents/session-page-sheet.mounted.test.tsx - no issues
  • apps/mobile/src/components/agents/save-profile-sheet.tsx - no issues (class-level note on SessionPageSheet)
  • apps/mobile/src/components/agents/save-profile-sheet.mounted.test.tsx - no issues
  • apps/mobile/src/components/profiles/agent-form-sheet.tsx - no issues
  • apps/mobile/src/components/profiles/skill-form-sheet.tsx - no issues
  • apps/mobile/src/components/profiles/mcp-form-sheet.tsx - no issues
  • apps/mobile/src/components/profiles/kilo-command-form-sheet.tsx - no issues
  • apps/mobile/src/components/profiles/*.mounted.test.tsx - no issues
  • apps/mobile/src/components/chat/backend-form.tsx - no issues
  • apps/mobile/src/components/chat/backend-form-dirty.ts - no issues
  • apps/mobile/src/components/chat/backend-model-list.tsx - no issues
  • apps/mobile/src/components/chat/backend-settings-sheet.tsx - no issues
  • apps/mobile/src/components/chat/use-backend-form-actions.ts - no issues
  • apps/mobile/src/components/chat/mcp-server-form-sheet.tsx - no issues
  • apps/mobile/src/components/chat/remote-mcp-server-form.tsx - 1 suggestion
  • apps/mobile/src/components/chat/remote-mcp-server-form.mounted.test.tsx - no issues
  • apps/mobile/src/components/organization/invite-member-sheet.tsx - 1 critical
  • apps/mobile/src/components/organization/member-limit-sheet.mounted.test.tsx - no issues
  • apps/mobile/src/components/todos/todo-list-settings-sheet.tsx - 2 warnings
  • apps/mobile/src/app/(app)/_layout.tsx - no issues
  • apps/mobile/src/app/(app)/(tabs)/(3_profile)/organization/_layout.tsx - no issues
  • patches/@expo+ui@57.0.21.patch - no issues
  • apps/mobile/package.json, pnpm-workspace.yaml - no issues
  • apps/mobile/src/i18n/locales/*.json (87 catalogs) - both new keys present, valid JSON, no placeholder mismatches

Fix these issues in Kilo Cloud


Reviewed by deepseek-v4.1-flash-1017 · Input: 61.7K · Output: 4.9K · Cached: 385.8K

Review guidance: REVIEW.md from base branch main

The header can disable Done alone, so Cancel still closes a sheet whose
commit is unavailable or waiting: the invite form with an empty address,
a new list without a name, and any form while its save is in flight.

The two route-sheet Back listeners now run only while their screen is
focused, so the pickers they push keep their own Back.

The remote MCP form compares what the saved draft keeps, so whitespace
alone no longer asks to discard.
The saved draft keeps a token only for the bearer method, so the dirty
check follows the same rule.
@iscekic iscekic added merge:human Substantive functional risk requires human review. merge:bot Bot-owned review; merge when ready once readiness passes and a review approves. and removed merge:human Substantive functional risk requires human review. labels Oct 20, 2026
…-while-scrolling

# Conflicts:
#	apps/mobile/src/components/picker-sheet.tsx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

drain Work item from the findings drain explorer:ios Finding surfaced by the iOS explorer merge:bot Bot-owned review; merge when ready once readiness passes and a review approves.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants