fix(mobile): keep the one-shot Ctrl armed through terminal-generated reports

Review of #268 turned up two defects, both verified against a real shell
session on an isolated instance.

1. A tap spent the modifier. The onData hook consumed every chunk while
   armed, but not every chunk is a keystroke: a shell session keeps the
   narrow scrollback strip, so mouse DECSETs reach the browser, and with
   vim/htop running a tap arrives as `\x1b[<0;31;23M`. Measured in the
   real app: armed, one tap, disarmed, and the Ctrl button read as dead.
   The hook now skips mouse and focus reports via a new
   `CodemanTerminalInput.isTerminalFocusOrMouseReport()`; they still reach
   the PTY, they just no longer stand in for the next key. Focus reports
   are covered for the same reason even though FOCUS_ESCAPE_FILTER in
   session.ts strips DECSET 1004 today, since the bar refocuses the
   terminal after every key and would spend the modifier on its own
   `\x1b[I` the moment that filter changed.

2. The armed style did not land on the four light skins. The competing
   rule is (0,3,1), not (0,2,1) as the comments claimed: `:is()` takes the
   specificity of its most specific argument and that list holds
   `.btn-toolbar.btn-shell`, so it outranked the (0,3,0) armed rules in
   both stylesheets. Measured across all seven skins at 390px, armed and
   resting backgrounds were byte-identical on paper-gray, solarized-light,
   catppuccin-latte and rose-pine-dawn. The light-skin rule now excludes
   the state as `.accessory-btn:not(.armed)`, which fixes phone and tablet
   at once; adding another class to the armed rules would only have moved
   the tie.

Tests: 20 more cases in test/mobile-shell-keyboard.test.ts (the report
classifier, the gate's effect on the modifier, and a static guard on the
light-skin selector, since the existing E2E background assertion passes on
a light skin and the browser suite runs the dark default), plus a browser
regression that taps the terminal with mouse reporting on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-08-10 04:07:19 +02:00
parent 9b9f2c21e9
commit 7c2a49d432
6 changed files with 229 additions and 9 deletions
+6 -4
View File
@@ -12081,10 +12081,12 @@ body.touch-device.cjk-input-visible .main {
color: var(--yellow);
}
/* Armed one-shot Ctrl (shell bar, issue #262). Three classes on purpose: the
light-skin compatibility rule in mobile.css repaints every .accessory-btn at
specificity (0,2,1), so a two-class rule here would lose and the modifier
would look unarmed on the light skins. */
/* Armed one-shot Ctrl (shell bar, issue #262). Three classes on purpose, to beat
the plain .accessory-btn rules. It still cannot outrank the light-skin
compatibility rule in mobile.css, which repaints every .accessory-btn at
(0,3,1) — `:is()` inherits its most specific argument — so that rule excludes
`.armed` by hand. Without the exclusion the modifier looks unarmed on the four
light skins, which is worse than having no armed style at all. */
.accessory-btn.accessory-btn-ctrl.armed {
background: var(--accent);
border-color: var(--accent);