Skip to content

Input line: keep a sent line cleared against a late keyboard edit - #63

Open
vadi2 wants to merge 4 commits into
mainfrom
ccr-14671218-tf96z6-input-clear-race
Open

vadi2 wants to merge 4 commits into
mainfrom
ccr-14671218-tf96z6-input-clear-race

Conversation

@vadi2

@vadi2 vadi2 commented Nov 15, 2026 •

Copy link
Copy Markdown
Member

Brief overview of PR changes/additions

  • Fixes a sent command coming back on the input line: after Send, a keyboard edit that arrived before the next frame (for example LatinIME marking the word before the cursor) was applied to the old text and undid the clear. Pressing Send right after a history recall or a completion could trigger it.
  • The input line and the masked password field now use Compose's state-based text fields (TextFieldState, OutlinedTextField(state = …), OutlinedSecureTextField). The keyboard edits that state directly, so clearText() stays cleared. History, completion, a picked command and a script's line set it with edit(); suggestions follow it through snapshotFlow.
  • The player's own edits clear a notice through an InputTransformation, at once, as onValueChange did. This way a "not sent" notice from a send right after typing is not lost.
  • Ghost text is now an OutputTransformation. The state, the keyboard and TalkBack see only what was typed. The accept button and the hardware Right arrow still accept it; Tab, Up/Down and Ctrl+R are unchanged.
  • Autocorrect stays off with no capitalisation. The password stays out of saved state and is never logged.
  • Tests:
    • GameScreenTest.anEditTheKeyboardSendsJustAfterASendLeavesTheLineEmpty sends and calls InputConnection.setComposingRegion in one main-thread task. No frame can come between them, so the race happens on every run: the test is deterministic, not timing-dependent, and failed the same way on both emulators in bite 1.
    • The history test now checks that the line is empty after each send.
    • The completion test checks that typing clears its notice.

Motivation for adding to Mudlet

A command the player has just sent must not come back and get glued to the next one ("northdraft").

Other info (issues closed, discussion etc)

Part of Mudlet/Mudlet#9011
Stacked on #61 (on #59, #49)

  • Checked locally: ktlint, shellcheck, actionlint, clang-format, and a compile check of the game UI and the new test's calls against Compose 1.10 desktop jars.
  • CI, green on API 28 and 36: https://github.com/Mudlet/android/actions/runs/43667185541
  • Bite 1, the new tests on the old TextFieldValue field: https://github.com/Mudlet/android/actions/runs/43666862978
    • Red on API 28 and 36, with only the race test failing: the line still held "north" after the send.
    • The old field passes the history test's new checks, because a test without a keyboard edit never hits the race.
  • Bite 2, this head with the send's clearText() and the notice transformation removed: https://github.com/Mudlet/android/actions/runs/43667342309
    • Red on API 28 and 36: 14 of 30 UI tests fail, among them the race test, the history test's empty-line checks and the completion test's "no notice once the player types".
  • Device-only: typing on a real keyboard.

🤖 Generated with Claude Code

https://claude.ai/code/session_016ujaeGtuaeaa9oac3KmLFo

A plain Kotlin module the input line will use to offer commands as the player types: history matched as a
subsequence and ranked by frecency, the command that usually follows the previous one or the latest game lines,
and words from recent output completing the word at the cursor. One suggester per profile, bounded, saved as a
line-based text; hidden input is never stored. Its tests run in apks.yml and with kotlinc alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ujaeGtuaeaa9oac3KmLFo
The input line now offers what :suggest ranks as the player types: up to
five buttons directly above the line, updated 120 ms after the typing
pauses and computed off the main thread, and the rest of the best
command greyed after the cursor (a VisualTransformation, so the text and
what TalkBack reads stay as typed). A button puts its suggestion on the
line without sending; an accept button, or a hardware right arrow at the
end of the line, takes the ghost text. An empty line after a send offers
what usually came next.

Each profile's suggestions are a SuggestionSession, loaded from
files/suggest/<profile>.txt when the game screen first shows the
profile, fed every send and every game line but the echo of a send, and
saved a few seconds after a send and when the app leaves the front. An
I/O error is logged, without any command, and the suggestions go on in
memory. While the game hides input there is neither strip nor ghost, and
nothing typed in the masked field reaches the session.

Autocorrection and the capital first letter are off on the command line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ujaeGtuaeaa9oac3KmLFo
A History button below the input line, or Ctrl+R on a hardware keyboard,
opens a sheet that searches the profile's history as the suggestions
match it (CommandSuggester.search). A row puts its command on the line
without sending; its buttons send, pin or unpin, and forget it. Up to 12
pinned commands per profile show as buttons below the line that send at
a tap, saved beside the suggestions as files/suggest/<profile>.pins.
Neither the quick bar nor the sheet shows while the game hides input.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ujaeGtuaeaa9oac3KmLFo
The input line took a TextFieldValue and onValueChange. The text field's
copy of the text, which the keyboard edits, catches up with the value
only when it recomposes, so a keyboard edit that landed between a send's
clear and the next frame was applied to the old text and onValueChange
put the sent line back (GameScreenTest, once on API 28: "northdraft").
Pressing Send right after a history recall or a completion does the same.

Both fields now use TextFieldState, which the keyboard edits directly:
a send clears it with clearText(); history, completion, a picked command
and a script's line set it with edit(). Suggestions follow the state
through snapshotFlow. The player's own edits clear a notice through an
InputTransformation, at once as onValueChange did, so a notice from a
send right after typing is not lost. The ghost text is an
OutputTransformation, so the state and the keyboard keep only what was
typed, and the masked field is an OutlinedSecureTextField on a
TextFieldState that is not saved.

A new test sends and makes a keyboard edit in one turn of the main
thread; the history test checks the line is empty after each send, and
the completion test that typing clears its notice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ujaeGtuaeaa9oac3KmLFo
@vadi2
vadi2 force-pushed the ccr-14671218-tf96z6-input-clear-race branch from e2abfb2 to 831de68 Compare November 15, 2026 02:18
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.

2 participants