feat(keyboard-guard): in-runner keyboard-occlusion guard for live device_* taps, iOS + Android (#370)#380
Merged
Merged
Conversation
…iewed + amended) TDD plan for the in-runner live-tap keyboard-occlusion guard (iOS + Android), amended per the Claude+Codex plan review: pure predicate in a dependency-free file, guard at the command handler (not tapAt), Android FLAG_RETRIEVE_INTERACTIVE_WINDOWS, containment predicate, destructive-back safety, off-device tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…the TS opt-out reaches iOS (#370)
…ns for KeyboardGuard (#370) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…on guard for live taps (#370)
… on press/longpress (#370)
…ead of lying (#370) applyKeyboardGuard reported 'dismissed' without checking the result, and dismissKeyboard's swipeDown corrupts the focused field via QuickPath slide-typing on iPhone QWERTY. Guard path now uses only the safe dismiss-control tap, re-verifies visibility, and returns dismiss_failed; the command handler refuses the tap with KEYBOARD_OCCLUDED instead of tapping the keyboard.
…ded taps fit the client timeout (#370)
…p occlusion guard (#370)
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.
Closes #370 (Phase 2 of #356; Phase 1 was PR #367).
What
Adds a default-ON, in-runner keyboard-occlusion guard for live
device_press/device_longpresstaps on iOS and Android: before a guarded coordinate tap, the runner checks whether a visible software keyboard's sane rect (non-empty, ≥120pt iOS / ≥150px Android — accessory bars excluded) contains the tap point, and auto-dismisses only when genuinely occluded (KeyboardAvoidingView-lifted controls are left alone).pressBack+ boundedwaitForIdle(1500), gated on a realTYPE_INPUT_METHODwindow with sane bounds (destructive-back safe; device-verified route-unchanged). RequiresFLAG_RETRIEVE_INTERACTIVE_WINDOWS, enabled at dispatcher init (flag-only mutation, null-guarded) — without itgetWindows()is silently empty (the B223 no-op class the plan review flagged).KEYBOARD_OCCLUDED … keyboardGuard=dismiss_failedrather than tapping the keyboard. Device-proven rationale: XCTestswipeDown()on the keyboard triggers QuickPath slide-typing and corrupts the focused field.meta.keyboardGuard:off | no_keyboard | not_occluded | dismissed(+dismiss_failedin the refusal error).RN_KEYBOARD_GUARD=0, resolved TS-side per command (guardKeyboardon the wire; absent → guard ON, with an iOS Codable decode regression test guarding exactly that).tapSeries, by-text taps, type/fill focus-taps, swipes,doubleTapdeliberately unguarded.Note: the plan's 2a (TS+iOS) / 2b (Android) stacked-PR split was collapsed into this single branch — the tasks interleaved naturally and each platform was device-verified independently.
Verification
node --test), 6/6 XCTest (incl. thevar-not-letdecode regression), 6/6 JVM Kotlin (incl. exclusive-untilboundary pins); oxlint + oxfmt clean; committeddist/confirmed byte-identical to a fresh rebuild.rn-dev-agent-workspace/docs/proof/370-keyboard-occlusion-phase2/):no_keyboard✓,not_occluded✓ (real keyboard frame; high tap spared),KEYBOARD_OCCLUDEDrefusal ✓ side-effect-free (keyboard stays, field unchanged). Live-measured KAV case: the wizard's Next button lifts y=790→571 and half-overlaps the keyboard — the refusal correctly prevents a post-dismiss miss-tap.not_occludedrequires window visibility),no_keyboard/not_occludedend-to-end withmeta.keyboardGuard✓,dismissed✓ (timed 3.6s incl. bounded idle;mInputShowntrue→false), destructive-back safety ✓ (route unchanged across all low taps).swipeDownQuickPath-corrupted the field (fixed: verify-or-refuse,ebb35874), and Android's unboundedwaitForIdleblew the client timeout (fixed: bounded 1500ms,321b2970).Process
Multi-LLM-reviewed plan (6 blockers fixed pre-code) → subagent-driven TDD (spec + quality review gate per task) → consolidated device pass → final whole-branch review (verdict: ready to merge; all deferred minors triaged, follow-ups filed).
Follow-ups (filed)
surfaceKeyboardGuardnull-hardening, structuredcodeon the refusal payload, guard-stepmeta.timings_ms, CLAUDE.md troubleshooting row)foreground()pre-flight ~10s stall exposed during verification (not introduced here)🤖 Generated with Claude Code