From 3489f6cc801eaa897254c226ef5a136a642d3e2a Mon Sep 17 00:00:00 2001 From: Aamer Akhter Date: Thu, 22 Oct 2026 13:26:04 -0400 Subject: [PATCH] fix(tmux): send prompts that start with a hyphen literally `send-keys -l ` keeps parsing options after `-l`, so text starting with `-` was read as a flag ("unknown flag -x", "invalid flag --") and never typed. End option parsing with `--` at every send-keys -l site in sendInput() and sendInputToPane(), as set-buffer already does. --- src/tmux-manager.ts | 8 ++++---- test/tmux-submit-paste.test.ts | 16 +++++++++++++--- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/tmux-manager.ts b/src/tmux-manager.ts index d3a2d174..73bae4e2 100644 --- a/src/tmux-manager.ts +++ b/src/tmux-manager.ts @@ -3869,7 +3869,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer { // highlighted option (see submitsAsKeys). if (submitsAsKeys(textPart)) { await execAsync( - `${this.tmux()} send-keys -t "${session.muxName}" -l ${shellescape(escapeTmuxTrailingSemicolon(textPart))}`, + `${this.tmux()} send-keys -t "${session.muxName}" -l -- ${shellescape(escapeTmuxTrailingSemicolon(textPart))}`, { timeout: EXEC_TIMEOUT_MS } ); } else { @@ -3885,7 +3885,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer { } else if (textPart) { // Text only, no Enter await execAsync( - `${this.tmux()} send-keys -t "${session.muxName}" -l ${shellescape(escapeTmuxTrailingSemicolon(textPart))}`, + `${this.tmux()} send-keys -t "${session.muxName}" -l -- ${shellescape(escapeTmuxTrailingSemicolon(textPart))}`, { timeout: EXEC_TIMEOUT_MS } ); } else if (hasCarriageReturn) { @@ -4030,7 +4030,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer { if (textPart && hasCarriageReturn) { execSync( - `${tmux} send-keys -t ${shellescape(target)} -l ${shellescape(escapeTmuxTrailingSemicolon(textPart))}`, + `${tmux} send-keys -t ${shellescape(target)} -l -- ${shellescape(escapeTmuxTrailingSemicolon(textPart))}`, { encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS, @@ -4042,7 +4042,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer { }); } else if (textPart) { execSync( - `${tmux} send-keys -t ${shellescape(target)} -l ${shellescape(escapeTmuxTrailingSemicolon(textPart))}`, + `${tmux} send-keys -t ${shellescape(target)} -l -- ${shellescape(escapeTmuxTrailingSemicolon(textPart))}`, { encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS, diff --git a/test/tmux-submit-paste.test.ts b/test/tmux-submit-paste.test.ts index ba529f18..8d0756c1 100644 --- a/test/tmux-submit-paste.test.ts +++ b/test/tmux-submit-paste.test.ts @@ -3,7 +3,8 @@ * submit (`sendInput`) goes in as ONE bracketed paste. Also `submitsAsKeys()`: a * single character is typed instead, since a selection dialog ignores a pasted `3` * and the Enter then confirms the highlighted option. And - * `escapeTmuxTrailingSemicolon()`, which every literal text path runs through. + * `escapeTmuxTrailingSemicolon()`, which every literal text path runs through, + * and the `--` that ends tmux's option parsing before every `send-keys -l` text. * * Why: `send-keys -l` typed the text as an unmarked burst, and Codex's paste-burst * heuristic folded the Enter that followed 50 ms later into the paste as a newline, @@ -163,13 +164,13 @@ describe('sendInput wiring', () => { it('types a single-character submit with send-keys -l, so a dialog answer selects', async () => { const body = await sendInputBody(); const keysBranch = body.slice(body.indexOf('if (submitsAsKeys(textPart))'), body.indexOf('} else {')); - expect(keysBranch).toMatch(/send-keys[^`]*-l \$\{shellescape\(escapeTmuxTrailingSemicolon\(textPart\)\)\}/); + expect(keysBranch).toMatch(/send-keys[^`]*-l -- \$\{shellescape\(escapeTmuxTrailingSemicolon\(textPart\)\)\}/); expect(keysBranch).not.toContain('pasteText('); }); it('escapes a trailing ";" on every send-keys -l in sendInput and sendInputToPane', async () => { const src = await source(); - const typed = src.match(/send-keys[^`\n]*-l \$\{[^`\n]*\}/g) ?? []; + const typed = src.match(/send-keys[^`\n]*-l -- \$\{[^`\n]*\}/g) ?? []; const inSend = [await sendInputBody(), src.slice(src.indexOf('sendInputToPane(muxName'))] .map((b) => b.slice(0, b.indexOf('\n }\n'))) .join('\n'); @@ -177,4 +178,13 @@ describe('sendInput wiring', () => { expect(typedHere.length).toBe(4); for (const cmd of typedHere) expect(cmd).toContain('escapeTmuxTrailingSemicolon(textPart)'); }); + // tmux parses an argument after `-l` that starts with `-` as an option, so a + // prompt like `-x hello` or `--help` was refused instead of typed. `--` ends + // option parsing; set-buffer already gets one (buildPasteTextCommands). + it('ends option parsing with -- before the text on every send-keys -l', async () => { + const src = await source(); + const literal = src.match(/send-keys -t [^`\n]*\s-l\s[^`\n]*/g) ?? []; + expect(literal.length).toBe(4); + for (const cmd of literal) expect(cmd).toMatch(/\s-l -- \$\{shellescape\(/); + }); });