Termix-SSH / Termix-SSH/Support
[BUG] Local echo rollback emits CSI K inside vim / alternate-screen apps, erasing the rest of the line
Nobody has claimed this yet.
- 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.
How to Reproduce
- 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.)
- Open an SSH terminal to that host and run vim ~/.ssh/authorized_keys — any file with lines longer than a few dozen characters works.
- Put the cursor on a long line and press $.
- 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
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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