fix(mobile): close audit findings — desktop focus, claim wiring, CJK setting, ESC passthrough

Adversarial post-rebase audit (11 agents) confirmed four real issues;
all fixed:

- Desktop tab clicks stopped focusing the terminal: handleSessionTabClick
  passed preserveKeyboard:false on desktop (KeyboardHandler.keyboardVisible
  is mobile-only state) and selectSession's ternary mapped explicit false
  to 'never focus', skipping the gesture-stack focus master relies on.
  Focus policy now lives solely in _shouldFocusTerminalForTabSwitch()
  (desktop: always; touch: only while the keyboard is open).

- Desktop sizing claims were almost never registered: selectSession's
  resizes run before _connectWs, so they went over HTTP (which never
  claims), leaving the arbitration inert in the canonical desktop+phone
  scenario. ws.onopen now sends a typed resize over the fresh socket —
  registering the claim and syncing PTY dims after (re)connects.

- throttledResize (the main window-resize path) sent untyped HTTP
  resizes: a rotating phone bypassed a desktop claim, and a desktop
  narrowing past the tablet breakpoint never released its stale claim.
  It now sends typed resizes, WS-first, like sendResize.

- The cjkInputEnabled App Settings toggle was silently ignored on touch
  phones/tablets (composer only reachable via the server inputCjkForm
  override, while the checkbox stayed visible and saveable). The user
  setting is honored everywhere again; mobile keeps native-input-by-
  default via the cjkInputEnabled:false mobile default.

- _handleCjkInput appended multi-byte ESC sequences (hardware-keyboard
  arrows/Home/End on the composer) to local-echo pending text, typing
  raw ESC bytes into the prompt on Enter; they are now forwarded to the
  PTY like the onData path. Its backspace path also syncs the
  per-session flushed Maps the way onData does, so tab-switch restore
  no longer resurrects deleted characters.

Defensive: Session.stop() clears desktop sizing claims (a hung client's
socket close can lag teardown by a ping cycle), and the claims docblock
documents the WS-only tradeoff explicitly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
arkon
2026-06-10 04:31:45 +02:00
co-authored by Claude Opus 4.8
parent e77df131b8
commit 7a39fd9a77
3 changed files with 79 additions and 21 deletions
+12
View File
@@ -2060,6 +2060,12 @@ export class Session extends EventEmitter {
* registers them on a desktop-typed resize and releases them on socket
* close, so a mobile-only session (no desktop connected) keeps full control
* of its own size — including narrowing below the spawn default.
*
* Deliberate tradeoff: claims are WS-only because only a socket has a
* liveness signal. A desktop degraded to the stateless HTTP resize fallback
* still applies its typed resizes but holds no claim, so a concurrent phone
* can reflow it. This is cooperative UX arbitration, not a security
* boundary — untyped (legacy/API) resizes bypass claims by design.
*/
private _desktopSizeClaims = new Set<symbol>();
@@ -2188,6 +2194,12 @@ export class Session extends EventEmitter {
this._clearAllTimers();
// Drop desktop sizing claims defensively. Sockets normally release their
// own claim on close, but a hung client's close event can lag the session
// teardown by up to a ping cycle — don't let a stale claim suppress
// mobile resizes if this Session object sees any further use.
this._desktopSizeClaims.clear();
// Immediately cleanup Promise callbacks to prevent orphaned references
// during the rest of stop() processing (e.g., if mux kill times out)
if (this.rejectPromise && !this._promptResolved) {