Skip to content

Terminal / TUI: Escape key is captured and not forwarded to programs in the pane (vim, nano, fzf, CLI agent TUIs) #2440

Closed as not planned

Description

@kjohnsen

What happened

  1. Open a terminal pane in Ovis (or drop into the terminal lens from an agent session).
  2. Launch any interactive terminal application or TUI (e.g., vim, nano, htop, fzf, less, or an interactive agent prompt).
  3. Press Escape (for instance, to exit insert mode in Vim, cancel search in fzf or less, or navigate TUI menus).
  4. Actual behavior: The Escape key gets captured / swallowed by Ovis or the browser environment and does not reach the running TUI program (\x1b is not delivered). This makes it impossible to exit modes or cancel operations via keyboard in TUIs.
  5. Expected behavior: In the terminal lens with terminal focus, pressing Escape should immediately send \x1b (ESC) through the active pty/WebSocket connection to the running program, without being intercepted or swallowed.

How often

Every time

Ovis version

0.4.21 (latest main)

How it runs

herdr plugin / bun run start / web client

Server PC

Linux x64, herdr 0.9.3

Device and browser

Desktop browser (Chrome / Firefox / Edge / Safari) and installed PWA

How the device reaches the server

This PC (localhost) / LAN address

How it signs in

Nothing (this PC) / Token

Agent and PC

Any terminal / shell pane or CLI agent (e.g. bash/zsh, Claude Code, Codex), local PC

Technical Context & Suspected Areas

Note: The following technical analysis was generated via automated codebase inspection and has not been human-verified:

  • xterm custom key handler: In src/lib/useTerminalMount.ts, term.attachCustomKeyEventHandler inspects key events. When Escape is pressed without modifiers, it returns true without calling preventDefault(), which may allow window/browser capture handlers to intercept or cancel the key.
  • Window capture handlers: Several components attach capture-phase listeners on window for Escape (e.g., src/lib/useSidebarLayout.ts, src/components/CommandPalette.tsx, src/lib/useFocusTrap.ts). If focus state or modal detection has any discrepancy, event.stopPropagation() in capture listeners may swallow Escape before it reaches xterm's hidden textarea.
  • Browser Fullscreen / Keyboard Lock: In Chromium-based browsers, full-screen and web apps require navigator.keyboard.lock(["Escape"]) to prevent the browser from consuming Escape for browser-level navigation/exit actions.
  • herdr ESC delay & PTY serialization: In server/core/input.ts, herdr buffers/holds lone ESC (~150ms) to disambiguate Alt+key combinations, which could also contribute to dropped or stalled escape sequences in fast interactive TUIs.

Steps to Reproduce

# In an Ovis terminal pane:
vim
# Press 'i' to enter insert mode
# Type some text
# Press 'Escape' to return to normal mode
# Note: Remains in insert mode; Escape is not registered by Vim

Activity

  1. devswha commented on Oct 31, 2026

    @devswha
    Owner

    I tried to reproduce this on the latest main (38686a2, 0.4.21) and could not: a lone Escape reached the program in the pane every time.

    Setup. An isolated herdr 0.9.5 test session (no real workspace) and a fresh Ovis server on loopback, built from that commit. The pane ran a small raw-mode recorder (process.stdin.setRawMode(true)) that writes down every byte it reads. Each check counts as passed only if \x1b shows up in both places: the browser's WebSocket input frame and the bytes the program read from its pty.

    What passed (30 checks, 0 failed):

    • Desktop Chrome 156 and Firefox 155 (Playwright), 1280×800, terminal lens, after a click into the grid. Each browser ran with navigator.platform set to Linux and again set to macOS:
      • a single Escape
      • iabc then Escape (the vim insert-mode case); the program read 69 61 62 63 1b
      • two Escapes in a row; the program read both
      • Escape after opening the command palette (Ctrl/Cmd+Shift+K) and closing it with Escape
    • The same pane reported as a Claude agent, both working and idle, in the terminal lens: a single Escape and a double Escape both arrived.
    • Phone width (390×844, touch): the key bar's Esc button sends \x1b and the program reads it.
    • Both PTY hosts: the Node fallback and the Rust host.

    Not covered: real Safari, and an installed PWA window. WebKit could not run on this test machine.

    About the suspected areas in the report: xterm's key handler returning true for Escape is how xterm sends the key itself (it calls preventDefault on its own). The window-level Escape listeners act only while their dialog, menu or drawer is open. Neither one dropped the key in any of the cases above.

    To track this down, could you share:

    1. The exact Ovis version (Settings → About) and how it is installed: the herdr plugin, bun run start, or another way.
    2. The exact browser and version. Does it also happen in a normal browser tab, not only the installed app?
    3. Whether anything was open when Escape failed: a dialog, the sidebar drawer on a narrow window, the find bar, or a peek over a chat. Or was the cursor simply in the grid?
    4. Whether an input method (IME) or a non-US keyboard layout is active, and whether a key remapper (for example keyd, Karabiner or AutoHotkey) is running.
    5. In the failing pane, run od -c, press Escape and then Enter, and paste what it prints. If 033 shows up, the key reached the pane and the program is the next suspect.

    Update: WebKit and the Mac app (macOS 27.0.1 test Mac, served by the same isolated test server, all on the same main build)

    • Safari 27.0.1, driven by safaridriver with real key events. A single Escape, iabc then Escape, two Escapes in a row, and Escape after opening the palette (Cmd+Shift+K) and closing it with Escape: every check passed. The pty read 1b each time, and the Escape that closed the palette did not also reach the pane.
    • Ovis for Mac 0.4.21 (the signed release Ovis.dmg, a WKWebView window), with OS-level key presses (System Events key code 53). A single Escape, iabc then Escape, two Escapes, Escape after the palette, and Escape with the window in native full screen all reached the pane. In full screen, Escape went to the terminal and did not leave full screen.
    • iOS 27 Simulator Safari: the pane and the key bar (Esc, Tab, ^C, Ctrl, Alt) load. I could not press a key there from this setup, so iOS is still unchecked.

    Desktop WebKit and the Mac app deliver Escape the same way Chrome and Firefox do. The questions above still apply.

  2. devswha commented on Nov 3, 2026

    @devswha
    Owner

    I tested this again on the current main (4bc043d, 0.4.22). I still could not reproduce it.

    Setup. I used an isolated herdr 0.9.5 test session and a fresh Ovis server on loopback with an empty state directory. The browser was headless Chrome, driven by Playwright. The pane ran a raw-mode recorder that writes down every byte the program reads. A check passed only if the browser sent an input frame containing \x1b and the program read 1b.

    What passed (15 checks):

    • Desktop at 1280×800, in the terminal lens, with navigator.platform set first to Linux and then to macOS:
      • a single Escape
      • iabc then Escape (the program read 69 61 62 63 1b)
      • two Escapes in a row
      • Escape after opening and closing the command palette. The Escape that closed the palette did not reach the pane.
    • The same pane reported as a Claude agent, both working and idle: a single Escape and a double Escape.
    • Real vi (vi -u NONE): i, some text, Escape, then :wq. The file was written, so Escape left insert mode.
    • Phone width (390×844): the key bar's Esc button.

    One thing I did see: a key pressed right after the terminal opens, before its connection is ready (well under a second here), is not sent. This happens to a letter as well as to Escape. After that, every Escape arrived. If your Escape fails only right after you open or switch to a pane, please tell us. That would be a different and narrower bug.

    The questions in my earlier comment still apply: the version and how Ovis is installed, the browser, anything open when Escape fails, any IME, keyboard layout or key remapper, and the output of od -c in the failing pane. Without one of these, we have no case where Escape is dropped that we can track down.

  3. devswha commented on Nov 5, 2026

    @devswha
    Owner

    Closing this, since it does not reproduce on main. Thanks again for the report, @kjohnsen.

    • Two isolated runs (on 38686a2 and on 4bc043d, see the comments above) delivered a lone Escape to the program in the pane every time: single and double Escape, iabc + Escape (69 61 62 63 1b), Escape after the command palette, Claude panes working and idle, real vi leaving insert mode, and the phone key bar's Esc.
    • The one miss those runs found, a key pressed before the terminal's connection is ready (a letter as well as Escape), was fixed by fix(terminal): never lose keys typed before the terminal takes input #2689 (merged 2026-11-04): keys typed before the terminal takes input are no longer lost.

    If Escape still fails for you on a current version, please reopen with the version, browser, what is open when it fails (IME, keyboard layout or remapper), and the output of od -c in the failing pane after pressing Escape, and we will look again.

  4. kjohnsen commented on Nov 5, 2026

    @kjohnsen
    Author

    Sorry for the delay. Ok, it looks like this is a result of something in my browser extensions (actually, narrowed it down to Vimium). It doesn't reproduce when I open in an incognito window. Thanks!

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions