From ab8f063814089c17b2a457e3f4041a89e45b042e Mon Sep 17 00:00:00 2001 From: fyzanshaik Date: Fri, 19 Jun 2026 15:18:29 +0530 Subject: [PATCH 1/2] fix(tui): disable fast-echo bypass inside tmux to prevent cursor drift --- .../src/__tests__/textInputFastEcho.test.ts | 20 +++++++++++++++++++ ui-tui/src/components/textInput.tsx | 7 +++++++ 2 files changed, 27 insertions(+) diff --git a/ui-tui/src/__tests__/textInputFastEcho.test.ts b/ui-tui/src/__tests__/textInputFastEcho.test.ts index 6221314a062..03805aa3886 100644 --- a/ui-tui/src/__tests__/textInputFastEcho.test.ts +++ b/ui-tui/src/__tests__/textInputFastEcho.test.ts @@ -178,6 +178,26 @@ describe('supportsFastEchoTerminal', () => { expect(supportsFastEchoTerminal({ TERM_PROGRAM: 'Apple_Terminal' } as NodeJS.ProcessEnv)).toBe(false) }) + it('disables fast-echo inside tmux', () => { + expect(supportsFastEchoTerminal({ TMUX: '/tmp/tmux-1000/default,1234,0' } as NodeJS.ProcessEnv)).toBe(false) + expect(supportsFastEchoTerminal({ TMUX: '/private/tmp/tmux-501/default' } as NodeJS.ProcessEnv)).toBe(false) + }) + + it('tmux wins over Termux fast-echo opt-in', () => { + expect( + supportsFastEchoTerminal({ + TMUX: '/tmp/tmux-1000/default,1234,0', + HERMES_TUI_TERMUX_FAST_ECHO: '1', + TERMUX_VERSION: '0.118.0' + } as NodeJS.ProcessEnv) + ).toBe(false) + }) + + it('keeps fast-echo enabled when TMUX is empty or unset', () => { + expect(supportsFastEchoTerminal({ TMUX: '' } as NodeJS.ProcessEnv)).toBe(true) + expect(supportsFastEchoTerminal({ TERM_PROGRAM: 'vscode' } as NodeJS.ProcessEnv)).toBe(true) + }) + it('disables fast-echo by default in Termux mode', () => { expect( supportsFastEchoTerminal({ TERMUX_VERSION: '0.118.0', PREFIX: '/data/data/com.termux/files/usr' } as NodeJS.ProcessEnv) diff --git a/ui-tui/src/components/textInput.tsx b/ui-tui/src/components/textInput.tsx index 564484999f6..ff6c9dad7b3 100644 --- a/ui-tui/src/components/textInput.tsx +++ b/ui-tui/src/components/textInput.tsx @@ -359,6 +359,13 @@ export function supportsFastEchoTerminal(env: NodeJS.ProcessEnv = process.env): return false } + // tmux adds a PTY multiplexing layer that desyncs stdout.write() cursor + // advances from its internal cursor model, causing cursor drift and ghost + // whitespace under the fast-echo bypass path. + if ((env.TMUX ?? '').trim().length > 0) { + return false + } + // Termux terminals are especially sensitive to bypass-path cursor drift and // stale paints at soft-wrap boundaries on tall/narrow viewports. Keep this // off by default in Termux mode; allow explicit opt-in for local debugging. From e52fffb607fe560604d5645f57d84d71d6c8b51e Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Fri, 19 Jun 2026 16:09:33 +0530 Subject: [PATCH 2/2] harden(tui): also disable fast-echo for tmux-flavored TERM (SSH-from-tmux) TMUX is not forwarded over SSH, so a TUI launched on a remote host from inside local tmux only sees TERM=tmux/tmux-256color with no TMUX var -- the cursor-drift bug still applies there. Extend supportsFastEchoTerminal() to also fall back when TERM is tmux-flavored. Deliberately scoped to tmux* only, NOT screen*: GNU screen sets the same screen/screen-256color TERM and has no reported drift, so widening to screen would disable the optimization for those users with no evidence of a bug (matching the original PR's stated out-of-scope note). Adds tests for tmux-flavored TERM (disabled) and screen/xterm TERM (stays enabled) to guard against accidental widening. --- ui-tui/src/__tests__/textInputFastEcho.test.ts | 17 +++++++++++++++++ ui-tui/src/components/textInput.tsx | 11 ++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/ui-tui/src/__tests__/textInputFastEcho.test.ts b/ui-tui/src/__tests__/textInputFastEcho.test.ts index 03805aa3886..98928d1baf1 100644 --- a/ui-tui/src/__tests__/textInputFastEcho.test.ts +++ b/ui-tui/src/__tests__/textInputFastEcho.test.ts @@ -198,6 +198,23 @@ describe('supportsFastEchoTerminal', () => { expect(supportsFastEchoTerminal({ TERM_PROGRAM: 'vscode' } as NodeJS.ProcessEnv)).toBe(true) }) + it('disables fast-echo when only a tmux-flavored TERM is present (SSH from tmux, no TMUX forwarded)', () => { + // OpenSSH forwards TERM but not TMUX, so a TUI on a remote host launched + // from inside local tmux sees TERM=tmux-256color with no TMUX var. The + // cursor-drift bug still applies, so fast-echo must stay off. + expect(supportsFastEchoTerminal({ TERM: 'tmux' } as NodeJS.ProcessEnv)).toBe(false) + expect(supportsFastEchoTerminal({ TERM: 'tmux-256color' } as NodeJS.ProcessEnv)).toBe(false) + }) + + it('does NOT disable fast-echo for screen-flavored TERM (GNU screen out of scope, no reported drift)', () => { + // GNU screen sets TERM=screen/screen-256color and has no reported drift. + // We must not widen the tmux guard to screen* and regress its perf. + expect(supportsFastEchoTerminal({ TERM: 'screen' } as NodeJS.ProcessEnv)).toBe(true) + expect(supportsFastEchoTerminal({ TERM: 'screen-256color' } as NodeJS.ProcessEnv)).toBe(true) + // And an unrelated 256color TERM must stay enabled. + expect(supportsFastEchoTerminal({ TERM: 'xterm-256color' } as NodeJS.ProcessEnv)).toBe(true) + }) + it('disables fast-echo by default in Termux mode', () => { expect( supportsFastEchoTerminal({ TERMUX_VERSION: '0.118.0', PREFIX: '/data/data/com.termux/files/usr' } as NodeJS.ProcessEnv) diff --git a/ui-tui/src/components/textInput.tsx b/ui-tui/src/components/textInput.tsx index ff6c9dad7b3..deb22914695 100644 --- a/ui-tui/src/components/textInput.tsx +++ b/ui-tui/src/components/textInput.tsx @@ -362,7 +362,16 @@ export function supportsFastEchoTerminal(env: NodeJS.ProcessEnv = process.env): // tmux adds a PTY multiplexing layer that desyncs stdout.write() cursor // advances from its internal cursor model, causing cursor drift and ghost // whitespace under the fast-echo bypass path. - if ((env.TMUX ?? '').trim().length > 0) { + // + // `TMUX` catches the local case. It is NOT forwarded over SSH, so when the + // TUI runs on a remote host launched from inside local tmux we only see a + // tmux-flavored `TERM` (tmux sets `tmux`/`tmux-256color`); match that too so + // remote-over-tmux sessions still fall back to the safe render path. We + // deliberately do NOT match `screen*`: GNU screen sets the same TERM and has + // no reported drift, so widening to screen would disable the optimization for + // those users with no evidence of a bug. + const term = (env.TERM ?? '').trim().toLowerCase() + if ((env.TMUX ?? '').trim().length > 0 || term === 'tmux' || term.startsWith('tmux-')) { return false }