v0.1.1: key-strip taps no longer dismiss the keyboard a TUI is driven with - #13
Merged
Conversation
On a phone, tapping up/down/left/right/esc/tab on the terminal's key strip hid the keyboard — every arrow press cost a re-summon, which makes a TUI menu undrivable one keystroke at a time. Return did NOT hide it, and that asymmetry was the tell once the resign was traced: the keyboard's own keys live in another window, but strip taps land in the app, where `KeyboardFloatingHost` installs a global tap-to-dismiss recognizer that resigns on any tap outside a text input. The strip's buttons are buttons, not text inputs, so driving the program read as "tap outside" and dismissed. Traced, not guessed: a temporary override on the prompt field logged the resign's callstack on the simulator, and it named the dismiss coordinator's gesture action directly. Fix: while a program is running, the terminal container registers its frame in `KeyboardHoldRegions`, and the dismiss recognizer skips taps that land inside a registered region. Everything else about dismissal stands — taps outside the container (another lane, the gap) still resign, which is the half of the rule worth keeping, and the region unregisters when the program ends. An edge-preview terminal registers an off-screen rect no tap can land in. A first attempt re-claimed the keyboard on `keyboardDidHide` while a program owned the screen. It is not in this diff, and the reason is recorded: with the region in place, the only dismissals it could still catch are the ones the user MEANT — it would have turned "tap off the edge to dismiss" into a fight the user loses. Removed in favor of not causing the hide at all. Verified on the simulator, both directions: `less` running, tap `down` on the strip — the prompt keeps its caret (before the fix this produced the logged resign); tap the margin outside the container — the caret drops. Build clean. The gesture law is upheld, not amended: taps remain content, and the one global exception (tap-outside-dismisses) now excludes the region where taps ARE the content's input. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both apps move to 0.1.1 (Android versionCode 2), per RELEASING.md: the changelog's two entries are the graph-mount status freeze (merged in #12) and the key-strip keyboard dismissal (this branch). Tagging v0.1.1 on main after merge publishes the release. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
What & why
Two things, one release:
The fix. On a phone, tapping
up/down/left/right/esc/tabon the terminal's key strip hid the keyboard — every arrow press cost a re-summon, making a TUI menu undrivable one keystroke at a time. Return did not hide it, and that asymmetry was the tell once traced: the keyboard's own keys live in another window, but strip taps land in the app, whereKeyboardFloatingHostinstalls a global tap-to-dismiss recognizer that resigns on any tap outside a text input. The strip's buttons are buttons, not text inputs, so driving the program read as "tap outside" and dismissed.Traced, not guessed: a temporary override on the prompt field logged the resign's callstack in the simulator, and it named the dismiss coordinator's gesture action directly.
While a program runs, the terminal container now registers its frame in
KeyboardHoldRegions, and the dismiss recognizer skips taps that land inside a registered region. Taps outside the container — another lane, the gap — still dismiss, and the region unregisters when the program ends. An edge-preview terminal registers an off-screen rect no tap can land in.A first attempt re-claimed the keyboard on
keyboardDidHide; it is deliberately NOT in this diff — with the region in place, the only dismissals it could catch are the ones the user meant.The release. This PR also cuts v0.1.1 per RELEASING.md: both apps bumped (iOS
MARKETING_VERSION0.1.1, AndroidversionName0.1.1 /versionCode2), changelog[Unreleased]moved to[0.1.1] - 2026-08-06covering this fix and the graph-mount status freeze (#12, already on main). After merge:git tag v0.1.1 && git push origin v0.1.1— the Release workflow builds and publishes with artifacts.Gesture-law impact
Upheld, not amended: taps remain content, and the one global exception (tap-outside-dismisses) now excludes the region where taps ARE the content's input — a terminal being driven by its key strip.
Verified — function and feel
Simulator, both directions:
lessrunning, tapdownon the strip → the prompt keeps its caret (before the fix this exact tap produced the logged resign); tap the margin outside the container → the caret drops (dismissal preserved).iOS simulator build green after
xcodegen generate; AndroidassembleDebuggreen with the version bump.Builds clean (
xcodegen generatere-run for the version bump)Feel-tested on a real device — owner: arrows through a create-vite menu should leave the keyboard planted; a tap off the container edge should still dismiss
Screenshot exercised in-session (caret held / caret dropped; the fix is the absence of a disappearance)
Docs updated: CHANGELOG.md carries 0.1.1; behavior change is the fix itself
No diagnostics or demo scaffolding left in the merge (the callstack logger was removed before commit)
🤖 Generated with Claude Code