Skip to content

id(web): world keyboard encoder drops or silently degrades modified special keys — Alt+Backspace sends plain Backspace, Alt+Arrow is dropped to the browser #732

Closed

Description

@dezren39

Summary

terminalBytesForKey in pkgs/id/web/src/world-keyboard.ts handles modified special keys inconsistently: some are silently dropped, and the rest are silently degraded to the unmodified key — so a world application receives a plain Backspace / Enter / Delete that the user never pressed, while other modified keys vanish entirely.

Evidence (verified on main 5db60876)

The special-key branch (world-keyboard.ts:36-40):

if (event.key in specialKeys) {
  if ((event.ctrlKey || event.altKey || event.shiftKey) && event.key.startsWith("Arrow")) return null;
  if (event.shiftKey && event.key === "Tab") return null;
  return new TextEncoder().encode(specialKeys[event.key]);
}

Only modified arrows and Shift-Tab are rejected; every other modifier combination falls through to the plain sequence. A temporary vitest probe driving the real function (removed after the run) produced:

Input Result Plain-key result
Alt+Backspace \x7f \x7f (identical)
Ctrl+Backspace \x7f \x7f (identical)
Alt+Enter \r \r (identical)
Alt+Tab \t \t (identical)
Alt+Escape \x1b \x1b (identical)
Ctrl+Delete \x1b[3~ \x1b[3~ (identical)
Alt+Delete \x1b[3~ \x1b[3~ (identical)
Ctrl+Home \x1b[H \x1b[H (identical)
Shift+End \x1b[F \x1b[F (identical)
Alt+PageUp \x1b[5~ \x1b[5~ (identical)
Alt+ArrowUp null (dropped) \x1b[A
Alt+ArrowLeft null (dropped) \x1b[D
Shift+Tab null (dropped) \t

Contrast with printable keys in the same function: Alt+x correctly returns \x1bx — the Alt modifier is preserved (as an ESC prefix) for printable input, just not for special keys.

Why it matters

  • Wrong input delivered: Alt+Backspace is word-delete in most editing contexts; the world app instead receives a single-character Backspace. Ctrl+Delete, Alt+Enter, Shift+End etc. all lose their modifier the same way. The caller in pkgs/id/web/src/world.ts (keydown handler, ~line 240) calls preventDefault() whenever bytes are returned, so the browser's own handling is suppressed and the degraded byte sequence is the only thing that happens.
  • Dropped input leaks to the browser: when the function returns null, the caller does not preventDefault(). Alt+ArrowLeft / Alt+ArrowRight are browser history navigation in several browsers, so a user trying to send a modified arrow to the world can instead navigate away from the world page mid-session.
  • The decoder's design says nothing should be dropped or misreported: the receiving side, Key.parse in pkgs/id/examples/roc-world/Key.roc, has no modified-key variants, but it deliberately surfaces unrecognised escape sequences as Unknown(bytes) ("nothing is dropped") — e.g. its own test pins Key.parse("\u(1b)[1;5C") == [Unknown("\u(1b)[1;5C")] for Ctrl+Right. The web encoder is the side that breaks that contract, in two opposite ways at once.

Suggested directions (one consistent policy)

Pick one and apply it to all special keys, not just arrows/Tab:

  1. Encode the modifier using the standard xterm modifier parameter (;2=Shift, ;3=Alt, ;5=Ctrl, …) for CSI sequences (\x1b[3;5~ Ctrl+Delete, \x1b[1;3A Alt+Up, \x1b[Z for Shift-Tab) and the ESC-prefix convention the function already uses for Alt+printable where a CSI form doesn't exist (ESC+\x7f for Alt+Backspace). Key.parse will surface these as Unknown today — preserved, not misreported — and a later Key extension can name them.
  2. Or return null consistently for every modified special key the Roc Key module cannot represent, so no modified press is ever silently rewritten as the plain key — and document that modified specials are unsupported (accepting that the browser keeps e.g. Alt+Left).

Option 1 matches the decoder's nothing-dropped contract; option 2 is the smaller change. Either is better than the current split.

Acceptance criteria

  • One documented policy covers every (special key × modifier) combination in terminalBytesForKey; no modified special returns the plain key's bytes, and the arrow/Shift-Tab special cases are no longer exceptions to a different rule.
  • Regression tests in world-keyboard.test.ts pin the policy for at least: Alt+Backspace, Ctrl+Delete, Alt+Enter, Shift+Home/End, Alt+PageUp, Alt+Arrow, Shift+Tab — and the existing plain and Alt+printable expectations still pass.
  • If option 1 is chosen, the emitted sequences are the standard xterm forms, and the Key.roc behaviour for them (currently Unknown) is noted in the PR.
  • Unmodified special keys, control letters, and AltGr (Ctrl+Alt printable) behaviour are unchanged.

Activity

  1. dezren39 commented on Oct 24, 2026

    @dezren39
    MemberAuthor

    id train triage (2026-10-24): needs-decision — Muse: assign to Session A (phases 0-10 review); web keyboard encoder drops/degrades modified special keys (world-keyboard.ts:36-40).

  2. added
    task/triageQueue packet kind: verify, reproduce, and route this item
    source/creatorFiled by the new-issue-creator job
    user/reviewNeeds a user-look: Muse reviews first and handles what it can
    on Oct 25, 2026
  3. dezren39 commented on Oct 26, 2026

    @dezren39
    MemberAuthor

    Intake review — fleet monitor, 2026-10-26T08:26:24Z.

    Still live — verified on current main this run: the special-key branch of terminalBytesForKey in pkgs/id/web/src/world-keyboard.ts is byte-for-byte the code the issue quotes — modified arrows and Shift+Tab return null, every other modified special key falls through to the unmodified sequence. No merged PR or commit has touched this behaviour since filing.

    Related: #724 / #761 are the sibling world-input correctness findings in the same web client; this issue already carries user/review, and this intake pass is that review's first step — the label stays until the encoding decision below is taken.

    Suggested approach: decide the modified-special-key encoding once, for the whole matrix: Alt+key as ESC-prefixed sequence (consistent with the printable-key path in the same function), Ctrl+special as the xterm modifier-parameter form (CSI 3;5~ etc.) where one exists, and an explicit null + no-preventDefault leak only where the browser genuinely owns the chord. Add the issue's probe table as unit tests in world-keyboard tests so the matrix cannot silently regress.

    Risks: silently degraded input (Alt+Backspace → plain Backspace) is worse than dropped input — it performs an edit the user did not ask for; that is why this stays priority/normal rather than low despite the small surface.

  4. added
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    effort/sSmall: expected under 2 hours of work
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    meta/enrichedIntake review done: acceptance criteria, links, and normalized labels added
    priority/normalOrdinary priority (the default)
    on Oct 26, 2026
  5. dezren39 commented on Oct 27, 2026

    @dezren39
    MemberAuthor

    Re-verified on current main 95cd41f0 (2026-10-27, new-issue-creator-code-b): verbatim copy of pkgs/id/web/src/world-keyboard.ts (sha256 cd62a6a8…, identical both sides) driven under Bun on the laptop — Alt+Enter \x0d, Alt+Backspace \x7f, Alt+Tab \x09, Alt+Escape \x1b, Alt+Delete \x1b[3~, Alt+Home \x1b[H are all byte-identical to the unmodified keys, Alt+ArrowUp still returns null, and Alt+x still correctly returns \x1bx. The inconsistency is unchanged; no separate issue filed.

  6. 53 remaining items

  7. added
    status/parkedDeliberately paused with a recorded resume condition
    and removed
    queue/claimedQueue worker lifecycle state
    on Oct 31, 2026
  8. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · parked (resume check)

  9. added
    queue/claimedQueue worker lifecycle state
    and removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Oct 31, 2026
  10. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  11. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up (resume → merge)

  12. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/idPackage/app: id (pkgs/ or apps/ in this repo)effort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)meta/enrichedIntake review done: acceptance criteria, links, and normalized labels addedpriority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobtask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions