Skip to content

v1.4.1 TUI unusable: arrow keys echo as ^[[A, mouse scroll broken (incl. Ghostty) #544

@mineralinis

Description

@mineralinis

Summary

Since v1.4.1, launching the agent-deck TUI leaves the terminal in a state where input is broken: arrow keys are not interpreted as navigation, mouse-wheel scroll triggers iTerm2's "mouse reporting was left on" warning, and the TUI is essentially unusable. Affects every terminal tested, including Ghostty — the exact terminal the v1.4.1 change (#535) was supposed to fix.

Versions

Version Status
v1.4.0 and earlier input works
v1.4.1 regression introduced (commit 2afee24, "fix(#535): wire CSIuReader into tea.NewProgram input pipeline")

Symptoms

  1. agent-deck launches; sidebar, sessions, and status bar all render correctly.
  2. Pressing arrow keys to move between sessions does nothing useful — instead, ^[[A / ^[[B characters appear in the visible TUI output.
  3. Mouse-wheel scroll does not navigate or scroll. iTerm2 raises:

    "looks like mouse reporting was left on when ssh session ended unexpectedly or an app misbehaved"

  4. The TUI is unusable — there's no way to switch sessions or scroll content.
  5. Quitting the TUI also leaves the parent shell in a bad state (cooked mode + mouse reporting on); reset is needed to recover.

Reproduction

  1. Install v1.4.1 via brew or agent-deck self-update
  2. From a fresh terminal tab (iTerm2 or Ghostty), run agent-deck
  3. Try to navigate with arrow keys → see ^[[A/^[[B
  4. Try to scroll with mouse wheel → see iTerm2 warning, no scroll

Tested on macOS 15 + iTerm2 and macOS 15 + Ghostty. Both broken in identical ways.

Notable: Ghostty was the terminal #535 was filed against. The v1.4.1 fix doesn't even fix the case it was filed for — it just breaks every terminal equally.

Root cause

Commit 2afee24 added one line to cmd/agent-deck/main.go:

tea.WithInput(ui.NewCSIuReader(os.Stdin)),

Two compounding bugs:

1. Bubble Tea silently skips raw-mode setup

Bubble Tea only puts the TTY into raw mode when its input is the real *os.File for stdin. Wrapping stdin in a *csiuReader makes the type assertion fail; Bubble Tea skips raw-mode setup. The TTY stays in cooked mode (echo on, line-buffered), so:

  • Arrow keys generate ESC [ A/ESC [ B. The kernel tty driver echoes them as ^[[A/^[[B. Bubble Tea never sees them as KeyMsg events.
  • tea.WithMouseCellMotion() still emits \033[?1000h to enable mouse reporting at startup, but cooked mode prevents Bubble Tea from consuming the events. iTerm2 detects the orphaned mouse reporting and shows its leak warning.

2. csiuReader.translate does not recognise SGR mouse terminators

internal/ui/keyboard_compat.go's scan loop uses a hardcoded whitelist of CSI final bytes: u, A, B, C, D, H, F, ~. It is missing M and m, the terminators for SGR mouse events (ESC [ < button ; col ; row M/m). When a mouse event arrives, the scanner walks past it looking for a known terminator, eventually bundles the mouse bytes with whatever later sequence terminates the scan, and passes the whole blob through as one "unknown CSI". Mouse handling is broken even with raw mode forced back on.

The full set of CSI final bytes is @~ (0x40–0x7E). A whitelist of 8 of them is fundamentally not a CSI parser; many other inputs (focus events, status reports, etc.) will also be corrupted.

Workaround

Downgrade to v1.3 (last fully clean release) or v1.4.0 (still has the #531 / #533 regressions but input works):

curl -L https://github.com/asheshgoplani/agent-deck/releases/download/v1.3/agent-deck_1.3_darwin_arm64.tar.gz \
  | tar xz -C /tmp && mv /tmp/agent-deck ~/.local/bin/agent-deck

Note on macOS Sequoia: when copying the binary into place over a running agent-deck process, use cp foo agent-deck.new && mv -f agent-deck.new agent-deck (atomic rename) — direct cp over the file may fail because the running TUI holds the inode.

Proposed fix

Revert commit 2afee24. The pre-existing DisableKittyKeyboard(os.Stdout) call at cmd/agent-deck/main.go:607 already asks every Kitty-protocol-aware terminal (Ghostty, Foot, Alacritty, Kitty) to fall back to legacy reporting, which is what #535 was trying to achieve. The reader was unnecessary belt-and-suspenders that turned out to be a foot-gun.

PR submitted as #543.

Why a "proper fix" is more involved than the original

If the wrapper is to come back, it needs:

  1. Explicit term.MakeRaw(int(os.Stdin.Fd())) + defer term.Restore(...) around tea.NewProgram so raw mode is set regardless of input wrapping.
  2. A real CSI parser in csiuReader.translate instead of an 8-byte terminator whitelist (params + intermediates + final byte across @~).
  3. Manual verification on Ghostty / Foot / Alacritty that the wrapper actually helps a real case the existing DisableKittyKeyboard call doesn't already cover.

Until that work happens, reverting is the right move.

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