Skip to content

fix(mobile): guard hardware sends during composition and focus changes - #2840

Merged
DoodleBears merged 12 commits into
mainfrom
codex/2820-composition-guard
Oct 25, 2026
Merged

DoodleBears merged 12 commits into
mainfrom
codex/2820-composition-guard

Conversation

@DoodleBears

@DoodleBears DoodleBears commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

#2820 | Agent claim

Agent-Task: codex/local/01a173f0-3f45-7f02-87a0-b1afa3b17ae0

Why the change

Prevent hardware-keyboard shortcuts from sending a mobile draft while confirming composed text or after input focus changes.

Special things to note

  • Requires new iOS and Android native builds. React Native retains the editor, draft storage and send owner. Physical CJK input methods, hardware keyboards, compact/expanded selection, streaming input latency and Human acceptance remain open under Mobile I1: 流式回复时长草稿输入跟手,保护中日文组合与光标 #2820; UIKit marked text and Robolectric replay do not establish those results.
  • Merge risk: two-way door — revert this slice and rebuild; medium blast radius — composer hardware sends can be suppressed or duplicated if native event ordering is wrong. Review focus: pre-IME intent, delayed echoes, current editable-field focus, marked text, cached actions and the next legitimate key.
  • The iOS test host is a default-off CocoaPods app/test specification selected only by BOTHARNESS_HARDWARE_SEND_IOS_TESTS=1. The manual CI input is also off by default, uses a standard read-only Mac runner and needs no account/signing credentials. Normal app builds exclude test and test-host sources.

Change outline

 Android hardware Enter → pre-IME → key/newline echo
-  remember only modifiers and a short echo window
+  remember focused field and composition intent before the input method handles Enter
+  invalidate echoes on focus change, cancellation, release and consumption
 iOS Command+Return → advertised command or cached action
-  check only whether sending is enabled
+  also require a supported editable input inside this view to remain first responder
+  reject marked text at both entrances; allow the next ordinary command
 native regression result → CI
+  require exact method identities and passing counts; reject empty/skipped/crash placeholders

The Android regression uses a real HardwareSendView, focused EditText, InputConnection, pre-IME/key dispatch and TextWatcher; only Expo's outbound event bridge is observed. The iOS regression uses a real scene/window, UITextView, UIKeyCommand, responder-chain dispatch and final Expo onSend events. Neither substitutes a private policy helper.

Evidence Result
Android composition red, b7c6c8555 13 tests: 3 failures at final send assertions; ordinary Enter passes.
Android composition green, 59baf515c Same 13 tests pass, including the next legitimate Enter.
Consumed key-up red, 7ce655726 15 tests: only the pre-IME-only key-up regression fails; cancellation/next-key controls pass.
Android release green, 6a2ebf2ef Same 15 tests pass, including 6 real-View cases; full workflow passes.
UIKit behavioral red, d32dafc36 Exact ordinary method passes; exact marked-text and focus methods fail with 4 assertions. Their native focus/marked-text preconditions pass. Independent xcresult inspection confirms 1 + 2 actual methods.
UIKit green, fa2c58c29 Exact ordinary method 1/1 and cached-command methods 2/2 pass with zero failures, skips or expected failures. Both strict result gates pass; independent raw xcresult extraction matches the CI JSON. Test sources, host and podspec are unchanged from the behavioral red.
PR merge CI All checks pass for the PR combined with the newer main, including full test shards, static checks, Android native tests, mobile export, docs and isolated Host smoke.
Local checks 105 tests in 8 files, root/mobile typing, scoped lint/format and source/feature-map/ledger/CI-plan checks pass. Guard-only future changes select the guard tests under existing CI rules. Independent real-result audit accepts the ordinary result and rejects failed regressions, a crashed-host placeholder and malformed case sets.

The three earlier UIKit attempts failed in test-fixture compilation, C++ linking and legacy test-host startup. They are harness diagnostics, not behavioral red evidence. The working target uses a dedicated scene-based test host; its sources remain outside the production module.

Exact candidate: fa2c58c29be1923fa27ba5e42120db765d235110; merge base: a344c131ee778ba856557aa8490de537e93055c0. Independent Standards and Spec reviews have zero unresolved findings; Spec independently passed 33 tests. UIKit native execution, its exact-result gates and the full manual verification workflow 40235150028 all pass. Main subsequently added #2839 without overlapping changed files; PR merge CI 40235372862 passes for that combination separately.

Capture blocker and review path: native keyboard events cannot be exercised by Expo Web, and the assigned physical-keyboard/CJK device journey has not run this candidate. Follow the composer journey steps 9–10 with rebuilt native apps: confirm a candidate without sending, send once with the next shortcut, then change focus with a pending shortcut. Capture matched native recordings or key-state evidence there. The automated result bundles do not replace device or Human acceptance.

Exercise HardwareSendView with a real EditText/InputConnection and observe only the Expo event bridge. Keep production input code unchanged for the authorized red run.

Issue: #2820

Agent-Task: codex/local/01a173f0-3f45-7f02-87a0-b1afa3b17ae0
Record the focused field and composition state before the IME receives Enter; clear stale echoes at focus, cancellation, release and send boundaries.

Issue: #2820

Agent-Task: codex/local/01a173f0-3f45-7f02-87a0-b1afa3b17ae0
Keep XCTest sources out of the normal pod. Gate the test specification behind an explicit CNG plugin opt-in; config boundary tests pass, while native compilation and behavior remain pending the assigned Mac QA window.

Issue: #2820

Agent-Task: codex/local/01a173f0-3f45-7f02-87a0-b1afa3b17ae0
Use explicit fixture setup and deferred cleanup instead of actor-isolated overrides of synchronous XCTest lifecycle methods.

Issue: #2820

Agent-Task: codex/local/01a173f0-3f45-7f02-87a0-b1afa3b17ae0
Drive pre-IME key release and cancellation through the native View, then deliver a late newline and verify the next fresh hardware key.

Issue: #2820

Agent-Task: codex/local/01a173f0-3f45-7f02-87a0-b1afa3b17ae0
An IME can consume physical key-up before ordinary dispatch. Clear the pending echo at that boundary so a later newline cannot submit the draft.

Issue: #2820

Agent-Task: codex/local/01a173f0-3f45-7f02-87a0-b1afa3b17ae0
Keep the standard Mac job disabled by default. Build only the CocoaPods test host, prove ordinary command delivery first, and retain scoped synthetic test evidence without credentials or signing.

Issue: #2820

Agent-Task: codex/local/01a173f0-3f45-7f02-87a0-b1afa3b17ae0
Xcode 27 imports UIKeyCommand.action as optional. Assert that the cached and next commands expose real selectors before sending their actions through UIKit.

Issue: #2820

Agent-Task: codex/local/01a173f0-3f45-7f02-87a0-b1afa3b17ae0
The isolated CocoaPods test bundle links ReactCodegen without an app target providing libc++; declare the standard library only on the HardwareSend test specification.

Issue: #2820

Agent-Task: codex/local/01a173f0-3f45-7f02-87a0-b1afa3b17ae0
Preserve both Expo Widgets and the opt-in HardwareSend test plugin.

Issue: #2820

Agent-Task: codex/local/01a173f0-3f45-7f02-87a0-b1afa3b17ae0
Use the public CocoaPods app specification only for the opt-in HardwareSend tests; bind fixture windows to the connected scene.

Issue: #2820

Agent-Task: codex/local/01a173f0-3f45-7f02-87a0-b1afa3b17ae0
…anges

Use one native eligibility check for advertised and cached shortcuts, and require exact UIKit method results in the manual verification job.

Issue: #2820

Agent-Task: codex/local/01a173f0-3f45-7f02-87a0-b1afa3b17ae0
@coderabbitai

coderabbitai Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Caution

Review failed

The pull request is closed.

Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@DoodleBears
DoodleBears marked this pull request as ready for review October 25, 2026 21:58
@DoodleBears
DoodleBears merged commit d488ae9 into main Oct 25, 2026
24 of 25 checks passed
@DoodleBears

Copy link
Copy Markdown
Contributor Author

已完成以下中文、日文输入功能场景:产品 d488ae9b8f074311c741eb636b935d028268f755,iOS 27 Simulator,native Debug app + production JavaScript。结果仅覆盖下列功能场景。

场景 结果 全文与消息检查
短中文草稿 + 固定流式回复 PASS 确认后保留 QA 中文,新组合可取消;canonical 120→122
短日文草稿 + 固定流式回复 PASS 确认后保留 QA 日本語,新组合可取消;canonical 120→122
10k 中文草稿,空闲会话 PASS 原 10,000 code points 前缀完整保留,追加 中文 后为 10,002 code points;canonical 保持 0
10k 中文草稿 + 固定流式回复 PASS 完整读回等于固定前缀 + 中文,共 10,002 code points;canonical 124→126
10k 日文草稿 + 固定流式回复 PASS 完整读回等于固定前缀 + 日本語,共 10,003 code points;canonical 126→128

每次流式运行只新增 fixture 触发和固定回复,没有组合确认或取消导致的额外消息。长草稿两轮的确认与新组合发生在 producer 区间内;中文 Delete 取消跨越 producer 结束,日文 Delete 发生在结束后、界面仍播放至约第 41 段。不能概括为所有取消动作都发生在上游输出期间。

完整验收矩阵与原始截图。原 5 张截图未改;long.png 仅表示成功预载,不是长草稿流式终态截图。

这些是 Simulator 功能结果,不是实机或 Human QA,也不构成触摸时延、FPS、A/B 性能或完整视觉播放结论。长草稿流式场景复用了预留会话,不能当作等价历史的性能样本。

滚动跟随的补充工作单独归属 #2477:来源已确认的新 8b08f4491b7c S3 自动末尾场景通过;打开键盘后另有遮挡待处理。早先 56pt 运行仍为 INVALID_SOURCE_PROVENANCE,不计作该候选通过或失败。该补充工作不扩大本 PR 的输入法验收范围。

Task: codex/local/01a173f0-3f45-7f02-87a0-b1afa3b17ae0; qa/batch-90, sole native runner mobile_ios_composition_qa.

@DoodleBears

Copy link
Copy Markdown
Contributor Author

Physical iPhone outcome — real composition and draft correctness

Signed frozen8b Release on XS Max/iOS18.1.1: actual WeChat Pinyin and existing system Japanese Kana candidate confirmation/cancellation passed the bounded short-draft cases. Confirmation during fixed streaming was verified; cancellation completed after or crossed the producer end, so active-production cancellation is not fully qualified. A 10k Chinese case preserved the entire draft except the intended single character at codepoint 65. A/B full-draft switching, expanded-editor return and exactly one normal send passed.

This is not full hardware-keyboard, long-draft Japanese, caret/background or latency acceptance. First 10k setup timed out; full readback qualified only after one controlled driver recovery. Physical profiling failed to attach by PID and name, so performance remains UNMEASURED. The exact tested scope and original failures are retained.

Full physical acceptance matrix, timing limits and cleanup: verified result. Task codex/local/01a173f0-3f45-7f02-87a0-b1afa3b17ae0; state handed-off for remaining work. The native lane was released on 2026-10-26 at 09:26:52 UTC. Upgraded Dev app and original user data/pairings retained; only owned test resources released. Keep acceptance gaps open; no merge or release was performed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant