Reliably refocus the terminal after a channel switch (#35) #41

Merged
pedromcaraujo merged 1 commit from fix/terminal-focus into main 2026-09-02 18:09:30 +01:00

Follow-up to #35 / #36 — the earlier fix (focusGen + a wider retry window)
still left focus off the shell for one class of device, because two independent
causes were in play:

  1. focusTerm() bailed on IS_TOUCH. IS_TOUCH is a device capability
    (maxTouchPoints > 0 / pointer: coarse), so it's true for touchscreen
    laptops, 2-in-1s and Chromebooks driven with a keyboard — and on those the
    focus code never ran at all. Now gated on a live input-modality flag
    (lastInputWasTouch): only a genuine finger tap suppresses auto-focus (which
    is what pops the Android soft keyboard); any keyboard/mouse/stylus input
    focuses the shell, even on touch-capable hardware. suppressTermKeyboard is
    gated on the same flag, keeping the two paths mutually exclusive.

  2. The "focus stuck" check could pass falsely. It read only
    frame.contentDocument.activeElement === ta, which can be true inside the
    iframe while keystrokes still land on the top document. Now requires both
    the top document to hold the iframe and the iframe to hold its textarea
    before it stops retrying.

Retained from #36: the focusGen counter that supersedes the earlier in-flight
attempt from closePalette. The wide fixed-schedule retry is replaced by a
self-scheduling poll (~3 s, guarded by focusGen) that also bails if the user
has meanwhile focused another launcher control.

Verify: on a desktop/keyboard device, switch channels from the palette
(Alt+K) and via the sidebar; the shell should take focus without a click. On a
phone, the soft keyboard should still stay down on switch.

Follow-up to #35 / #36 — the earlier fix (`focusGen` + a wider retry window) still left focus off the shell for one class of device, because two independent causes were in play: 1. **`focusTerm()` bailed on `IS_TOUCH`.** `IS_TOUCH` is a device *capability* (`maxTouchPoints > 0` / `pointer: coarse`), so it's true for touchscreen laptops, 2-in-1s and Chromebooks driven with a keyboard — and on those the focus code never ran at all. Now gated on a live input-modality flag (`lastInputWasTouch`): only a genuine finger tap suppresses auto-focus (which is what pops the Android soft keyboard); any keyboard/mouse/stylus input focuses the shell, even on touch-capable hardware. `suppressTermKeyboard` is gated on the same flag, keeping the two paths mutually exclusive. 2. **The "focus stuck" check could pass falsely.** It read only `frame.contentDocument.activeElement === ta`, which can be true inside the iframe while keystrokes still land on the top document. Now requires *both* the top document to hold the iframe *and* the iframe to hold its textarea before it stops retrying. Retained from #36: the `focusGen` counter that supersedes the earlier in-flight attempt from `closePalette`. The wide fixed-schedule retry is replaced by a self-scheduling poll (~3 s, guarded by `focusGen`) that also bails if the user has meanwhile focused another launcher control. **Verify:** on a desktop/keyboard device, switch channels from the palette (Alt+K) and via the sidebar; the shell should take focus without a click. On a phone, the soft keyboard should still stay down on switch.
Reliably refocus the terminal after a channel switch (#35)
All checks were successful
CI / check (pull_request) Successful in 22s
375aab7dfd
Two independent causes left focus off the shell after a palette channel
switch, so a keyboard user had to click the terminal to type.

- focusTerm() bailed on IS_TOUCH, a device *capability* that is true for
  touchscreen laptops, 2-in-1s and Chromebooks driven with a keyboard.
  On those it never focused the terminal at all. Gate on the live input
  modality (lastInputWasTouch) instead: only a genuine finger tap
  suppresses auto-focus (which is what pops the Android soft keyboard);
  any keyboard/mouse/stylus input focuses the shell, even on
  touch-capable hardware. suppressTermKeyboard is gated on the same flag,
  keeping the two paths mutually exclusive.

- The retry window ([0,60,200,500]ms) was too short: a fresh channel load
  rebuilds xterm's hidden textarea only after the ttyd bundle boots and
  the websocket reconnects, often past 500ms, so every tick fired before
  the textarea existed and it gave up. Poll ~3s until it appears (matching
  applyTermThemeWhenReady), and require both the top document to hold the
  iframe *and* the iframe to hold its textarea before declaring focus
  stuck — the inner-only check was a false positive.
pedromcaraujo deleted branch fix/terminal-focus 2026-09-02 18:09:30 +01:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
pedromcaraujo/forge!41
No description provided.