Skip to content

tui: keypad PageUp/PageDown/Home/End ignored by the error screen, error details, /btw and mini footers #58946

Open

Description

@argszero

Description

NumPad PageUp / PageDown / Home / End do nothing in several TUI views, while the regular keys work. Scaffolding sections that hardcode a comparison against event.name compare against pageup, pagedown, home, end, but under the kitty keyboard protocol a keypad press reports its own key name.

Measured on dev (a41d09bc78) by driving the real input path (app.renderer.stdin → KeyHandler → useKeyboard):

key sent name the handler receives
keypad End (CSI 57424 u) kpend
keypad Home (CSI 57423 u) kphome
keypad PageUp (CSI 57421 u) kppageup
keypad PageDown (CSI 57422 u) kppagedown
keypad Up / Down / Left / Right kpup / kpdown / kpleft / kpright
regular End / CSI 5 ~ PageUp end / pageup

So a guard like if (event.name === "end") fires 1 time for the regular key and 0 times for the keypad key.

Affected comparisons (all reached through useKeyboard, i.e. raw events):

  • packages/tui/src/component/error-component.tsx:94-97 — the error screen; PageUp/PageDown/Home/End cannot scroll the stack trace.
  • packages/tui/src/component/dialog-error-details.tsx:74-77 — the error details dialog.
  • packages/tui/src/feature-plugins/prompt/btw.tsx:120-123 — the /btw answer panel.
  • packages/tui/src/mini/footer.form.tsx:252-253
  • packages/tui/src/mini/footer.permission.tsx:257-258
  • packages/tui/src/mini/footer.subagent.tsx:144-146

The keypad Up/Down/Left/Right comparisons in the same handlers miss for the same reason.

Why the cli.json workaround does not cover this

#55533 documents that adding kp* names to cli.json restores the affected bindings. These six sites never consult the keymap: each subscribes to renderer.keyInput via useKeyboard and compares the raw event name, so no binding and no alias table is involved. The keypad keys stay dead there even with cli.json configured.

@opentui/core already knows the equivalence (defaultKeyAliases maps kpend → end, kppageup → pageup, …), but it applies it to binding lookups only, not to the events a raw handler receives.

Expected behaviour

Keypad navigation keys behave like their regular equivalents in these views, so a keyboard with NumLock off can scroll the error screen, the error details dialog, /btw and the mini footers.

Related

#55533 is the binding-side counterpart and stays open; this covers the raw-handler sites it cannot reach.

Activity

  1. opencode-agent commented on Oct 25, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report! I could reproduce this in the /btw answer panel.

    Steps: I opened a session, asked a /btw question whose simulated answer was 80 lines long, then sent the kitty keypad sequences CSI 57424 u (End), CSI 57422 u (PageDown) and CSI 57423 u (Home). None of them scrolled the panel, while the regular End and Home keys did. It happens on 2.1.4 and on the latest dev (a41d09bc78). The recording and screenshots below show it, and the script below reproduces it.

    I only ran the /btw panel. The error screen, the error details dialog and the mini footers use the same event.name checks in the code, so they are very likely affected the same way. Your PR #58947 targets exactly this, and #55533 remains the separate issue for keypad keys in keybindings.

    recording.mp4

    Screenshot

    Screenshot

    Reproduction script (drive.ts)
    // Repro for #58946: keypad End/Home (kitty CSI-u) do nothing in the /btw answer panel,
    // while the regular End/Home keys scroll it.
    import { Effect, Stream } from "effect"
    import { Llm, OpenCodeDriver } from "opencode-drive"
    
    const lines = Array.from({ length: 80 }, (_, i) => `answer line ${i + 1}`)
    const answer = lines.join("\n\n")
    
    const KP_END = "\x1b[57424u"
    const KP_HOME = "\x1b[57423u"
    const KP_PAGE_DOWN = "\x1b[57422u"
    
    export default OpenCodeDriver.use(
      {
        ...(process.env.OC_DEV ? { opencode: { dev: process.env.OC_DEV } } : {}),
        tui: { recording: true, keypressOverlay: true },
      },
      ({ ui, llm }) =>
        Effect.gen(function* () {
          yield* llm.serve((request) =>
            Stream.make(JSON.stringify(request).includes("what are the lines") ? Llm.text(answer) : Llm.text("Hello.")),
          )
          yield* ui.submit("hi")
          yield* Effect.sleep(4000)
    
          yield* ui.submit("/btw what are the lines?")
          yield* Effect.sleep(6000)
          yield* Effect.sleep(800)
          yield* ui.screenshot("1-btw-open")
    
          // Keypad End: should jump to the bottom
          yield* ui.press(KP_END)
          yield* Effect.sleep(800)
          const kpEnd = yield* ui.matches("answer line 80")
          yield* ui.screenshot("2-after-keypad-end")
    
          // Keypad PageDown: should scroll down a page
          yield* ui.press(KP_PAGE_DOWN)
          yield* Effect.sleep(800)
          const kpPgDn = !(yield* ui.matches("answer line 12"))
          yield* ui.screenshot("3-after-keypad-pagedown")
    
          // Regular End: jumps to the bottom
          yield* ui.press("end")
          yield* Effect.sleep(800)
          const end = yield* ui.matches("answer line 80")
          yield* ui.screenshot("4-after-regular-end")
    
          // Keypad Home: should jump back to top
          yield* ui.press(KP_HOME)
          yield* Effect.sleep(800)
          const kpHome = !(yield* ui.matches("answer line 80"))
          yield* ui.screenshot("5-after-keypad-home")
    
          // Regular Home
          yield* ui.press("home")
          yield* Effect.sleep(800)
          const home = !(yield* ui.matches("answer line 80"))
          yield* ui.screenshot("6-after-regular-home")
    
          yield* Effect.log(
            JSON.stringify({ keypadEndReachedBottom: kpEnd, keypadPageDownScrolled: kpPgDn, regularEndReachedBottom: end, keypadHomeReachedTop: kpHome, regularHomeReachedTop: home }),
          )
        }),
    )

    Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH; set OC_DEV to the path of an OpenCode checkout to run that from source instead.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions