Skip to content

Backspace deletes two characters instead of one in Kitty terminal #704

@skyscribe-yf

Description

@skyscribe-yf

Bug Description

In Kitty terminal, pressing Backspace once deletes two characters instead of one. This does NOT happen in other terminal emulators or in pi-coding-agent (which uses the same TUI library).

Environment

  • Terminal: Kitty 0.32.2
  • kimi-code: 0.14.2 (npm global install)
  • OS: Linux
  • Shell: Bash
  • Node: v24.16.0

Reproduction

  1. Open Kitty terminal
  2. Run kimi
  3. Type some text in the input editor
  4. Press Backspace once
  5. Expected: One character deleted
  6. Actual: Two characters deleted

Root Cause

kimi-code ships @earendil-works/pi-tui@0.74.2, which has a buggy Kitty keyboard protocol negotiation in terminal.js. The newer pi-tui (0.79.1, used by pi-coding-agent) fixes this.

Key differences between pi-tui 0.74.2 (kimi-code) and 0.79.1 (pi-coding-agent)

1. Wrong negotiation order (most likely cause)

Old (0.74.2):

1. Send CSI ? u          (query current flags)
2. Terminal responds CSI ? 0 u   (flags=0, nothing pushed yet)
3. Set _kittyProtocolActive = true   ← BUG: protocol not actually active yet!
4. Send CSI > 7 u        (push flags 7)
5. Terminal enables flags 7

New (0.79.1):

1. Send CSI > 7 u CSI ? u CSI c   (push → query → DA sentinel)
2. Terminal responds CSI ? 7 u     (flags=7, already pushed)
3. Check flags !== 0 → set _kittyProtocolActive = true

The old code sets _kittyProtocolActive=true when the protocol flags are still 0 (not yet active). This creates a window where the code thinks the Kitty protocol is active but the terminal is still sending legacy-format key events (raw 0x7F for backspace). When the protocol does become active, the same keypress can be interpreted in both legacy and Kitty-protocol formats, causing double processing.

2. No negotiation buffering

The old code uses a simple regex to match the Kitty protocol response. If the response arrives split across stdin chunks, it can be missed or misinterpreted. The new code has a dedicated keyboardProtocolNegotiationBuffer with a 150ms fragment timeout.

3. No disableModifyOtherKeys()

The old code can end up with both Kitty keyboard protocol AND xterm modifyOtherKeys mode 2 active simultaneously (race condition with the 150ms fallback timeout). The new code explicitly disables modifyOtherKeys when Kitty protocol is confirmed active. Having both protocols active can cause key events to be encoded and processed twice.

4. Accepts flags=0 as "protocol active"

Old code sets _kittyProtocolActive=true on ANY Kitty response, including flags=0. New code checks flags !== 0 before enabling the protocol.

Evidence

Both apps import pi-tui as an external module (not bundled):

// kimi-code dist/main.mjs line 46:
import { ..., isKeyRelease, matchesKey, ... } from "@earendil-works/pi-tui";

Version check:

kimi-code/node_modules/@earendil-works/pi-tui: 0.74.2  ← buggy
pi-coding-agent/node_modules/@earendil-works/pi-tui: 0.79.1  ← fixed

The keys.js, stdin-buffer.js, and keybindings.js files are identical between versions — all key matching and release-event filtering logic is the same. The bug is exclusively in terminal.js protocol negotiation.

Suggested Fix

Bump the @earendil-works/pi-tui dependency from ^0.74.0 to ^0.79.0 (or latest) in apps/kimi-code/package.json.

The new version is backward-compatible — the API surface is a superset of 0.74.2 (adds MarkdownOptions, OverlayUnfocusOptions, word-navigation module, native-modifiers module). No breaking changes to the imports used by kimi-code.

Workaround

Users can manually replace the old pi-tui with a newer version:

# Replace kimi-code's pi-tui with pi-coding-agent's version
KIMI_DIR=$(npm root -g)/@moonshot-ai/kimi-code/node_modules/@earendil-works/pi-tui
PI_DIR=$(npm root -g)/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-tui

# Backup old version
mv "$KIMI_DIR" "${KIMI_DIR}.bak"

# Symlink to the newer version
ln -s "$PI_DIR" "$KIMI_DIR"

⚠️ This workaround will be overwritten on the next kimi-code update.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions