fix(mobile): hardware Enter keeps the Android terminal focused#24928

merged2
main
GitHub fork: fix/i8140-android-pressing-enter-in-terminal-with
opened6d agobyOonly21mil
View on GitHubTwo-way sync enabledPR last imported 19h ago

Problem

On Android with a physical keyboard, Enter in the native terminal sends the command, then moves focus off the terminal. The next keys go nowhere until you tap the terminal again. Fixes #8140, reported on a Pixel 10a with a physical keyboard.

The terminal's hidden input is a single-line EditText. On a hardware Enter, Android calls the editor action listener on key-down and again on key-up. Our listener sent CR on key-down and returned false on key-up. A single-line TextView treats an unconsumed Enter key-up as "advance focus", so it moved focus to the next focusable view on every Enter. Numpad Enter was never handled at all: no CR, and focus moved the same way.

Change

  • The listener now consumes both halves of Enter and numpad Enter. It sends CR once, on key-down, as before. Key-up returns true and does nothing else, so TextView no longer advances focus.
  • To test this without the Expo view and the native terminal library, the listener moved out of T3TerminalView into a small TerminalEnterListener class. That is its own commit with no behavior change, so the fix commit shows only the key-up change.
  • The module gets the same Robolectric test setup the composer, review-diff, notifications and widget modules already use.

The soft keyboard's Send action is unchanged: it still arrives as IME_ACTION_SEND without a key event and sends CR once. Read-only terminals still swallow Enter.

Scope and approval

Bug fix for #8140. The maintainer's triage note found no duplicate or existing fix and requested Android verification with a physical keyboard. This PR adds before/after Android emulator evidence using injected keyboard-source Enter events; a physical keyboard remains untested. One choice for maintainers to confirm: numpad Enter now sends CR like the main Enter key. Before this change it sent nothing and also moved focus away. This awaits maintainer review.

Verification

  • A standalone Gradle JVM harness compiled the production TerminalEnterListener.kt and TerminalEnterListenerTest.kt and ran gradle --no-daemon test --rerun-tasks with Robolectric SDK 36. The test puts the listener on a focused single-line EditText with a focusable control below it, then dispatches key-down and key-up for Enter and numpad Enter. It asserts focus stays on the input after each key and that CR went out once per key, twice in total.
    • With the listener at the refactor commit, which keeps main's logic: fails, focus left the input after key 66.
    • On this branch: passes.
  • ktlint 1.8.0 and detekt 1.23.8 with apps/mobile/detekt.yml on the three changed Kotlin files: clean.
  • Android 16 emulator, Debug builds of main and this branch, a real T3 server and a shell in the native terminal. Keys came from adb shell input text and input keyevent 66, which inject keyboard-source key events, the same path a physical keyboard takes. Each run typed echo first, Enter, echo second, Enter, then a bare Enter, and read the focused view from the accessibility tree after each Enter.
    • main: the first command runs, then the terminal input loses focus and the terminal turns grey. echo second and both later Enters never reach the shell.
    • This branch: both commands and the bare Enter reach the shell, and the terminal input is the focused view after every Enter.

Not checked:

  • A physical Android device or a real Bluetooth/USB keyboard, including the reporter's Pixel 10a. The emulator runs had no hardware keyboard attached, so the on-screen keyboard stayed up beside the injected key events.
  • iOS. Its terminal input is a separate native view and this PR does not touch it.
  • Third-party soft keyboards that send Enter as key events instead of an editor action. They now take the same path as a hardware Enter key; no third-party keyboard was tested.
  • The module's :t3-terminal:testDebugUnitTest task in the app's Gradle build. The regression test ran through the standalone JVM harness described above.

Prior PRs

  • #14465 by yqecea moved this listener into a new input class while reworking deletion and composition, and kept the Enter logic as it was. It was closed for missing UI verification. This PR does not build on it.
  • The listener dates from #3579 (Android mobile support).

Evidence

Same three hardware Enters on an Android 16 emulator: main sends the first command, then the terminal turns grey and the rest go nowhere; this branch runs both commands and the extra Enter.

Side by side: main shows only echo first with a grey terminal; this branch shows echo first, echo second and two prompts

Main: after the first Enter the terminal turns grey, the keyboard's action key changes from Send to Return, and "echo second" never reaches the shell.

Recording on main: focus leaves the terminal after the first Enter Full recording (mp4)

This branch: "echo first", "echo second" and a bare Enter all reach the shell, and the terminal input keeps focus throughout.

Recording on this branch: every Enter reaches the shell Full recording (mp4)

Focused Robolectric test with main's Enter logic: focus left the input after key 66 (Enter).

Gradle output: TerminalEnterListenerTest fails with focus left the input after key 66

The same test on this branch passes.

Gradle output: TerminalEnterListenerTest passes

Production fix, tests, PR description and images by Claude Opus 5.5 in T3 Code Nightly.

🤖 Generated with Claude Opus 5.5 in T3 Code Nightly

Checks

23 successful · 11 skipped
All checks passed
41adb7b

Added 2 commits

  • refactor(mobile): move Android terminal Enter handling into its own listener39561c2
  • fix(mobile): hardware Enter keeps the Android terminal focused41adb7b
Synced from GitHub

Approvability

Verdict: Approved at 41adb7b

Macroscope's review found this PR approvable — This is a localized Android terminal bug fix that preserves existing input behavior while consuming hardware Enter key-up events to prevent focus loss, with a focused Robolectric regression test. Build-file changes are test-only and do not alter product defaults or static-analysis policy.

You can add or adjust custom eligibility rules. Learn more.

Synced from GitHub

Review in Change Stack →

No actionable comments were generated in the recent review.

External image Recent review info
External image Run configuration
External image Commits

Reviewing files that changed from the base of the PR and between 5b35402623d7d1d88eaa472807272a6c2e78012a and 41adb7b23a9ac0688a027c6b9360a011bd4e4844.

External image Files selected for processing: 4
  • apps/mobile/modules/t3-terminal/android/build.gradle
  • apps/mobile/modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalView.kt
  • apps/mobile/modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/TerminalEnterListener.kt
  • apps/mobile/modules/t3-terminal/android/src/test/java/expo/modules/t3terminal/TerminalEnterListenerTest.kt

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.

Walkthrough

Android terminal input now uses a dedicated listener for IME and hardware Enter events. A Robolectric test checks that hardware Enter keeps focus on the terminal input.

Changes: Android terminal Enter handling

Layer / File(s)Summary
Enter event handling
apps/mobile/modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalView.kt, apps/mobile/modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/TerminalEnterListener.kt
The terminal view delegates editor actions to TerminalEnterListener. The listener sends a carriage return for IME Send and hardware Enter key-down events when the terminal is not read-only. It consumes hardware Enter key-up events and all actions when the terminal is read-only.
Robolectric focus test
apps/mobile/modules/t3-terminal/android/build.gradle, apps/mobile/modules/t3-terminal/android/src/test/java/expo/modules/t3terminal/TerminalEnterListenerTest.kt
The module enables Android resources for unit tests, adds JUnit and Robolectric, and configures test tasks to use Java 21. The test dispatches Enter and numpad Enter events and checks that focus remains on the input and the callback runs twice.

Priority: External image Normal

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix · Severity of issue fixed: Low

Merge Risk: External image Minimal · up to 41adb

The change appears mergeable after normal checks. Physical-keyboard behavior on a device and the app Gradle test task remain unverified.

Pre-merge checks: Passed 4
Check nameStatusExplanation
Title checkPassedThe title clearly and concisely describes the primary change: keeping the Android terminal focused after a hardware Enter.
Description checkPassedThe description includes Problem, Change, Scope and approval, and Verification sections. It explains the bug, implementation, linked issue, test results, emulator evidence, screenshots, and untested s…
Linked Issues checkPassedIssue #8140 requires repeated Android terminal Enter input without losing focus. TerminalEnterListener handles KEYCODE_ENTER and KEYCODE_NUMPAD_ENTER, sends CR once on key-down, and consumes key…
Out of Scope Changes checkPassedThe changes stay within issue #8140. The listener extraction enables isolated testing. The Robolectric dependencies and test configuration support the regression test. Numpad Enter handling addresses …
Full details: Description check

Explanation

The description includes Problem, Change, Scope and approval, and Verification sections. It explains the bug, implementation, linked issue, test results, emulator evidence, screenshots, and untested scenarios.

Full details: Linked Issues check

Explanation

Issue #8140 requires repeated Android terminal Enter input without losing focus. TerminalEnterListener handles KEYCODE_ENTER and KEYCODE_NUMPAD_ENTER, sends CR once on key-down, and consumes key-up. T3TerminalView uses the listener. TerminalEnterListenerTest dispatches both key phases for both key codes and verifies focus remains on the input and CR is sent twice. The added Robolectric setup supports this automated regression test.

Full details: Out of Scope Changes check

Explanation

The changes stay within issue #8140. The listener extraction enables isolated testing. The Robolectric dependencies and test configuration support the regression test. Numpad Enter handling addresses the same focus-loss path. No unrelated product behavior or unrelated files are changed.

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

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

Note

Written by gpt-6.1-sol on behalf of Maria

Verified in a real dev environment

No changed behavior could be verified in the running app on this VM. This is an unverifiable result, not a successful verification.

Read the description, discussion, inline comments and complete four-file diff at 41adb7b23a9ac0688a027c6b9360a011bd4e4844. The PR is open; neither commit is authored by juliusmarminge or t3dotgg. GitCafe mirrors it as https://git.cafe/pingdotgg/t3code/pulls/24928. Its status reports 27 successful checks, zero failing and zero pending. Those checks are not runtime proof.

The change is Android-native: consume both hardware Enter/numpad Enter phases, send CR on key-down, and keep the single-line terminal input focused. The required user path is an Android native terminal connected to a real server, followed by consecutive shell commands without tapping to refocus.

Device preflight returned:

text
1mcp__t3_code__device_list({})
2Agent device access is turned off for this environment.

The repository mobile verification skill explicitly says: "If T3 device tools or the selected device are unavailable, report the blocker and stop verification. Do not install or switch to another automation system."

No dev server was started, no checkout or source was changed, and no runtime screenshots or recordings were captured. Existing author evidence was not treated as independently reproduced evidence.

Not verified

  • Original focus-loss reproduction on github/main and the fix on this exact head: Android device access is disabled, so neither could be exercised.
  • Consecutive hardware Enter commands, numpad Enter, soft-keyboard Send, read-only input, and focus retention in the real Android client.
  • Real Codex gpt-6.1-sol turns through a newly created project/thread. A web turn cannot exercise the changed Kotlin input listener.
  • Physical USB/Bluetooth keyboards, third-party Android keyboards, and native iOS behavior.

To complete verification, enable Android device access and run main then this head against one isolated dev server at a time. Type two distinguishable shell commands with hardware Enter, then a bare Enter, without tapping to refocus; check that each submits exactly once and focus stays in the terminal. Repeat with numpad Enter, then check soft-keyboard Send and read-only input. Capture the main/PR interaction and inspect it before uploading.

Note

Written by gpt-6.1-sol on behalf of Maria

Verified on Android emulator (API 35) against a real dev server

No changed app behavior was verified. This attempt is unverifiable, not a passing Android verification.

Read the complete diff, description, discussion, reviews and commit authors at 41adb7b23a9ac0688a027c6b9360a011bd4e4844. The PR remains open and neither commit is by juliusmarminge or t3dotgg. The change is Android-native.

Booted the t3 AVD with KVM and the requested emulator flags. adb shell getprop sys.boot_completed returned 1; adb shell getprop ro.build.version.sdk returned 35. Opened the emulator in T3's Device panel and obtained a working AgentDevice snapshot. Device access is no longer the blocker.

Checked out the exact head and completed vp i successfully. Started a real isolated dev server with vp run dev --home-dir /home/user/t3code/.t3-verify24928: the server logged Listening on http://127.0.0.1:13773, and Metro answered packager-status:running on port 8081. Configured adb reverse for both ports.

This head predates the mobile:install package command. Ran the current main branch's cache-aware native installer as a temporary helper against this head's native sources. It found no shared cached APK for android/development-x86_64/b9f18c18d056e12b3e63219b482f7cde3c4ce385 and completed Expo prebuild, then stalled restoring the Android build cache before Gradle could start. Kernel inspection showed the worker blocked in fuse_rename_common -> fuse_rename2 -> vfs_rename -> __x64_sys_rename. A clean sibling worktree at the same SHA also stalled opening files, with fuse_do_getattr -> fuse_permission -> path_openat. This is a VM filesystem blocker, not evidence that the PR is broken.

No candidate native client was installed, so no terminal screenshots or recordings can honestly demonstrate this change. Existing author media and CI were not substituted for independent runtime proof. GitCafe reports 27 successful checks, zero failing and zero pending. No source fix or push was made.

Not verified

  • Consecutive hardware Enter commands, bare Enter, focus retention, and exactly-once submission through the real Android terminal.
  • Numpad Enter, soft-keyboard Send, and read-only terminal behavior.
  • Original reproduction on github/main.
  • Pairing the native app, creating real project/thread data, and real Codex gpt-6.1-sol turns.
  • Physical USB/Bluetooth keyboards and third-party Android keyboards.
  • iOS: this Linux VM cannot run iOS simulators.

Verification needs a functioning filesystem and an APK built from this exact native fingerprint, then the requested app interactions and independently captured evidence.