mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-20 09:09:35 +02:00
fix(tmux): type a single-character submit so a dialog answer selects
Claude Code's and Codex's selection dialogs ignore a bracketed paste, so after the previous commit a pasted `3` + Enter confirmed the highlighted option instead of option 3. sendInput() now types a single character with `send-keys -l` as master did (submitsAsKeys()). One character cannot form the burst Codex reads as a paste, so the Codex fix is unaffected. The trailing-`;` escape moves into escapeTmuxTrailingSemicolon() and now also covers every `send-keys -l` call in sendInput() and sendInputToPane(); tmux 3.5a types `x;` for `send-keys -l 'x\;'`. Docs: the @fileoverview lines and orchestrator-loop-architecture.md still described `send-keys -l`; CLAUDE.md, architecture-invariants.md and cron-guide.md name the single-character exception. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
344edc942b
commit
7fd76f06c3
6 files changed
+123
-36
No files matched your search
@@ -1,6 +1,9 @@
|
||||
/**
|
||||
* Covers `buildPasteTextCommands()` and `pasteText()`: the text half of a text+Enter
|
||||
* submit (`sendInput`) goes in as ONE bracketed paste.
|
||||
* 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.
|
||||
*
|
||||
* 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,
|
||||
@@ -17,7 +20,7 @@
|
||||
*/
|
||||
import { execSync } from 'node:child_process';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildPasteTextCommands, pasteText } from '../src/tmux-manager.js';
|
||||
import { buildPasteTextCommands, escapeTmuxTrailingSemicolon, pasteText, submitsAsKeys } from '../src/tmux-manager.js';
|
||||
|
||||
const FAKE_TMUX = `${JSON.stringify(process.execPath)} -e 'console.log(JSON.stringify(process.argv.slice(1)))' --`;
|
||||
|
||||
@@ -74,6 +77,27 @@ describe('buildPasteTextCommands', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('submitsAsKeys', () => {
|
||||
it.each(['3', '9', 'y', 'n', ';', 'ä', '👍'])('types a single character: %s', (text) => {
|
||||
expect(submitsAsKeys(text)).toBe(true);
|
||||
});
|
||||
|
||||
it.each(['10', 'yes', '/cost', 'read your inbox', 'x'.repeat(1500)])('pastes anything longer: %s', (text) => {
|
||||
expect(submitsAsKeys(text)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('escapeTmuxTrailingSemicolon', () => {
|
||||
it.each([
|
||||
['x;', 'x\\;'],
|
||||
[';', '\\;'],
|
||||
['a; b', 'a; b'],
|
||||
['plain', 'plain'],
|
||||
])('%s -> %s', (text, expected) => {
|
||||
expect(escapeTmuxTrailingSemicolon(text)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pasteText', () => {
|
||||
const cmds = { set: 'SET', paste: 'PASTE', discard: 'DISCARD' };
|
||||
|
||||
@@ -114,16 +138,43 @@ describe('pasteText', () => {
|
||||
describe('sendInput wiring', () => {
|
||||
// sendInput() returns before touching tmux under vitest (IS_TEST_MODE), so the
|
||||
// branch that submits text is pinned on the source instead.
|
||||
it('submits text through pasteText, not send-keys -l', async () => {
|
||||
async function source(): Promise<string> {
|
||||
const { readFileSync } = await import('node:fs');
|
||||
const src = readFileSync(new URL('../src/tmux-manager.ts', import.meta.url), 'utf-8');
|
||||
const body = src.slice(src.indexOf('async sendInput('), src.indexOf('// ========== Pane Methods'));
|
||||
return readFileSync(new URL('../src/tmux-manager.ts', import.meta.url), 'utf-8');
|
||||
}
|
||||
|
||||
async function sendInputBody(): Promise<string> {
|
||||
const src = await source();
|
||||
return src.slice(src.indexOf('async sendInput('), src.indexOf('// ========== Pane Methods'));
|
||||
}
|
||||
|
||||
it('submits text through pasteText, not send-keys -l', async () => {
|
||||
const body = await sendInputBody();
|
||||
const submitBranch = body.slice(
|
||||
body.indexOf('if (textPart && hasCarriageReturn)'),
|
||||
body.indexOf('} else if (textPart)')
|
||||
);
|
||||
expect(submitBranch).toContain('pasteText(');
|
||||
expect(submitBranch).toContain('buildPasteTextCommands(');
|
||||
expect(submitBranch).not.toMatch(/send-keys[^`]*-l/);
|
||||
const pasteBranch = submitBranch.slice(submitBranch.indexOf('} else {'));
|
||||
expect(pasteBranch).toContain('pasteText(');
|
||||
expect(pasteBranch).toContain('buildPasteTextCommands(');
|
||||
expect(pasteBranch).not.toMatch(/send-keys[^`]*-l/);
|
||||
});
|
||||
|
||||
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).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 inSend = [await sendInputBody(), src.slice(src.indexOf('sendInputToPane(muxName'))]
|
||||
.map((b) => b.slice(0, b.indexOf('\n }\n')))
|
||||
.join('\n');
|
||||
const typedHere = typed.filter((cmd) => inSend.includes(cmd));
|
||||
expect(typedHere.length).toBe(4);
|
||||
for (const cmd of typedHere) expect(cmd).toContain('escapeTmuxTrailingSemicolon(textPart)');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user