Termix-SSH / Termix-SSH/Support

[BUG] Local echo rollback emits CSI K inside vim / alternate-screen apps, erasing the rest of the line

Open
#1,230 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug good-first-issue platform-desktop platform-macos ssh ui
Dominant language
No language data
Stars
28
Forks
4
PR merge metrics
No merged PRs in 30d

Description

Title

Local echo rollback emits CSI K inside vim / alternate-screen apps, erasing the rest of the line

Platform

Desktop App - macOS

Server Installation Method

N/A

Version

2.7.1

CLI Installation Method

None

CLI Version

No response

Troubleshooting
  • I have examined logs and tried to find the issue
  • I have reviewed opened and closed issues
  • I have tried restarting the application
  • I have checked open issues and ensured this is not a duplicate
The Problem

TerminalLocalEcho (src/ui/lib/terminal-local-echo.ts, added in #1249, shipped in 2.7.0) has no notion of the alternate screen buffer, so it corrupts the display of every full-screen TUI run over SSH. The most visible case is vim.

What happens, step by step:

handleInput() queues every printable ASCII keystroke as a prediction — the only filter is SAFE_INPUT = /^[\x20-\x7e]$/. So vim normal-mode keys ($, w, b, G, 0, …) all end up in pending.
The server's reply to such a key is never the literal character; it is a cursor movement escape sequence starting with \x1b. The match loop in handleOutput() therefore consumes 0 characters and falls through to rollback().
rollback() injects \x1b[D\x1b[K into the data stream, ahead of vim's own output. CSI K erases from the cursor to the end of the line.
vim does not know that region was cleared, so it never repaints it. The tail of the line stays blank until something forces a full redraw — Ctrl-L, :redraw!, or the SIGWINCH you get by resizing the window.

// src/ui/lib/terminal-local-echo.ts
private rollback() {
  const count = this.pending.filter((item) => item.predicted).length;
  this.pending = [];
  return count > 0 ? `\x1b[${count}D\x1b[K` : "";   // <-- CSI K = erase to end of line
}

With mode: "on" this reproduces on the very first keypress. With the default mode: "auto" it is latent until slowSamples reaches 2 (two echoes measured at ≥120 ms), which happens within seconds on any WAN / VPS connection — and once it flips, predicted stays true for the rest of the session, because Terminal.tsx never calls reset().

The same rollback fires inside any alternate-screen program, not just vim: less, htop, nano, mc, tmux copy-mode are all affected.

Workaround for users: set Local Echo to "Off" in User Profile -> Terminal, or per host in the host editor.

Image
How to Reproduce
  1. Sidebar -> User Profile -> Terminal -> Local Echo -> "Always on". (Or leave it on "Auto" and connect to any host with ~120 ms+ RTT; prediction turns itself on after a couple of keystrokes.)
  2. Open an SSH terminal to that host and run vim ~/.ssh/authorized_keys — any file with lines longer than a few dozen characters works.
  3. Put the cursor on a long line and press $.
  4. Everything between the cursor and the end of that screen row disappears. Press Ctrl-L, or resize the Termix window, and the text comes back.
Additional Context

Suggested fix — 1: make local echo a no-op on the alternate screen.

Terminal.tsx already tracks this (ALTERNATE_SCREEN_SEQUENCE / updateAlternateScreenMode, line 136), but the flag only gates the syntax highlighter, not the local echo. Note that handleOutput() runs before formatTerminalOutput(), which is where alternateScreenModeRef is updated — so gating on that ref alone would leave the chunk containing \x1b[?1049h unguarded. Reading xterm's own buffer type avoids the chunk-boundary problem entirely:

// src/ui/features/terminal/Terminal.tsx — onData handler
const inAltScreen = terminal.buffer.active.type === "alternate";
const predicted = inAltScreen ? "" : localEchoRef.current?.handleInput(data);

// src/ui/features/terminal/Terminal.tsx — websocket "data" handler (~line 1417/1429)
const output = inAltScreen
  ? msg.data
  : (localEchoRef.current?.handleOutput(msg.data) ?? msg.data);

localEchoRef.current?.reset() should also be called on the transition into the alternate screen, so no stale pending entries survive to be rolled back later.

Suggested fix — 2: rollback() should not use CSI K.

Erasing to end of line is destructive any time something was already drawn to the right of the cursor. Beyond TUIs, this also clobbers zsh-autosuggestions, fish autosuggestions, and any right-hand prompt (RPROMPT). Erasing exactly as many cells as were predicted is sufficient and safe:

return count > 0 ? `\x1b[${count}D\x1b[${count}X` : "";   // CSI n X = ECH, erase n chars

Environment: Termix desktop 2.7.1 (arm64), macOS 15 / Darwin 25.6.0, embedded desktop backend, remote hosts are Linux VPSes over WAN.

Related: #985 (the feature request), #1249 (the PR that added it), released in 2.7.0.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with src/ui/lib/terminal-local-echo.ts and the onData and websocket data handlers in src/ui/features/terminal/Terminal.tsx, then inspect xterm's active buffer type and the existing alternate-screen tracking. Reproduce the issue with vim or another alternate-screen app using Local Echo set to Always on. Done means alternate-screen output is not corrupted, pending predictions do not leak across transitions, and normal local-echo rollback no longer erases unrelated line content.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
desktop
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.