Skip to content

tui: ctrl+j and ctrl+return insert a newline even when unbound from input.newline #50684

Open

Description

@dmitri-trofimov

Summary

Ctrl+J and Ctrl+Return insert a newline into the prompt input regardless of cli.json keybind configuration. Even when explicitly removed from input.newline and rebound to an unrelated command, they still insert a newline and never trigger the newly bound command — indicating they bypass the configurable keybind dispatch entirely for this purpose.

Environment

  • opencode version: 2.0.14
  • OS: Linux 6.18.33.2-microsoft-standard-WSL2 (WSL2 Ubuntu on Windows 11), x86_64
  • Terminal: Windows Terminal (host), TERM=xterm-256color inside WSL
  • Shell: /bin/bash
  • Install/channel: latest
  • Active plugins: none found in config

Reproduction

  1. Set ~/.config/opencode/cli.json to:
    {
      "keybinds": {
        "input.newline": "shift+return",
        "app.debug": ["ctrl+j", "ctrl+return"]
      }
    }
    This replaces the default input.newline list (shift+return,ctrl+return,ctrl+j) so it no longer includes ctrl+j/ctrl+return, and instead binds those two combinations to app.debug (Toggle debug panel).
  2. Launch the TUI (opencode) and start typing in the prompt.
  3. Press Ctrl+J.
  4. Press Ctrl+Return.

Expected Behavior

Since input.newline no longer lists these keys, and they are now bound to app.debug, pressing them should toggle the debug panel and not insert a newline.

Actual Behavior

Both Ctrl+J and Ctrl+Return insert a newline into the prompt exactly as before. The debug panel never appears. The rebinding has no observable effect for either key.

Additional Context

Activity

  1. github-actions commented on Sep 22, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

    For keybind-related issues, please also check our pinned keybinds documentation: #4997

  2. opencodejr commented on Oct 28, 2026

    @opencodejr
    Contributor

    Thanks for the detailed report. I can reproduce this on 2.0.14, and it still happens on the latest dev (fa1260d).

    Steps: I used your cli.json (input.newline set to shift+return, app.debug bound to ctrl+j and ctrl+return), typed in the prompt, then sent Ctrl+J the way a terminal without the Kitty keyboard protocol sends it: as a raw line-feed byte (0x0a). A newline was inserted and the debug panel did not open. The recording and screenshots below show it, and the script below reproduces it.

    For comparison, when the same keys arrive Kitty-encoded (CSI-u), your bindings work: Ctrl+J opens the debug panel, Ctrl+Return closes it, and no newline is inserted. So the problem looks limited to the legacy line-feed byte, which Windows Terminal sends for Ctrl+J (and, as far as I know, for Ctrl+Enter too, though I couldn't check that in a real Windows Terminal).

    Likely cause: the underlying terminal UI library (OpenTUI) reads that byte as a linefeed key rather than ctrl+j. Its textarea has a built-in linefeed → newline binding, which runs no matter what input.newline is set to, so your ctrl+j binding never matches. I've passed this to the maintainers.

    recording.mp4

    Screenshot

    Screenshot

    Screenshot

    Reproduction script (drive.ts)
    // Repro for anomalyco/opencode#50684: with Ctrl+J / Ctrl+Return removed from
    // input.newline and bound to app.debug, a terminal without the Kitty keyboard
    // protocol (e.g. Windows Terminal) still gets a newline and never toggles debug.
    //
    // Drive's simulated terminal speaks the Kitty protocol, so ui.press("j", {ctrl})
    // arrives as CSI-u "ctrl+j" and the custom binding works (control case).
    // A legacy terminal sends Ctrl+J (and Windows Terminal's Ctrl+Enter) as a raw
    // LF byte 0x0a; ui.type("\n") writes exactly that byte to stdin.
    import { Effect } from "effect"
    import { OpenCodeDriver } from "opencode-drive"
    
    export default OpenCodeDriver.use(
      {
        ...(process.env.OC_DEV ? { opencode: { dev: process.env.OC_DEV } } : {}),
        tui: { recording: true, keypressOverlay: true },
        tuiConfig: {
          keybinds: {
            "input.newline": "shift+return",
            "app.debug": ["ctrl+j", "ctrl+return"],
          },
        },
      },
      ({ ui }) =>
        Effect.gen(function* () {
          yield* Effect.sleep("1500 millis")
          yield* ui.type("first")
          // Control: Kitty-encoded ctrl+j -> debug panel opens, no newline
          yield* ui.press("j", { ctrl: true })
          yield* Effect.sleep("700 millis")
          yield* ui.screenshot("1-kitty-ctrl-j-opens-debug")
          // Kitty-encoded ctrl+return -> debug panel closes, no newline
          yield* ui.press("return", { ctrl: true })
          yield* Effect.sleep("700 millis")
          yield* ui.screenshot("2-kitty-ctrl-return-closes-debug")
          // Bug: legacy Ctrl+J = raw LF -> newline inserted, debug panel does not open
          yield* ui.type("second\n")
          yield* Effect.sleep("700 millis")
          yield* ui.type("third")
          yield* Effect.sleep("1000 millis")
          yield* ui.screenshot("3-legacy-lf-inserts-newline")
        }),
    )

    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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions