From d20c821b72c7411d093a09a9470d213768d09d35 Mon Sep 17 00:00:00 2001 From: Hoang Nguyen Date: Sun, 28 Jun 2026 07:07:30 +0000 Subject: [PATCH] fix(agent-manager): paste tmux send body with bracketed paste --- .../src/__tests__/terminal/TtyWriter.test.ts | 27 +++++++++++++---- .../agent-manager/src/terminal/TtyWriter.ts | 29 +++++++++++++++---- 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/packages/agent-manager/src/__tests__/terminal/TtyWriter.test.ts b/packages/agent-manager/src/__tests__/terminal/TtyWriter.test.ts index 00bb7c58..499dff85 100644 --- a/packages/agent-manager/src/__tests__/terminal/TtyWriter.test.ts +++ b/packages/agent-manager/src/__tests__/terminal/TtyWriter.test.ts @@ -19,6 +19,7 @@ function mockExecFileSuccess(stdout = '') { mockedExecFile.mockImplementation((...args: unknown[]) => { const cb = args[args.length - 1] as (err: Error | null, result: { stdout: string }, stderr: string) => void; cb(null, { stdout }, ''); + return { stdin: { end: vi.fn() } }; }); } @@ -41,22 +42,36 @@ describe('TtyWriter', () => { tty: '/dev/ttys030', }; - it('sends message and Enter as separate tmux send-keys calls', async () => { + it('pastes message in bracketed paste mode and sends Enter separately', async () => { mockExecFileSuccess(); + const message = 'line 1\nline 2\n'; - await TtyWriter.send(location, 'continue'); + await TtyWriter.send(location, message); - expect(mockedExecFile).toHaveBeenCalledWith( + const loadArgs = mockedExecFile.mock.calls[0]?.[1] as string[]; + const bufferName = loadArgs[2]; + expect(bufferName).toMatch(/^ai-devkit-send-/); + expect(mockedExecFile).toHaveBeenNthCalledWith( + 1, 'tmux', - ['send-keys', '-t', 'main:0.1', '-l', 'continue'], + ['load-buffer', '-b', bufferName, '-'], expect.any(Function), ); - expect(mockedExecFile).toHaveBeenCalledWith( + expect(mockedExecFile.mock.results[0]?.value.stdin.end) + .toHaveBeenCalledWith(message); + expect(mockedExecFile).toHaveBeenNthCalledWith( + 2, + 'tmux', + ['paste-buffer', '-t', 'main:0.1', '-b', bufferName, '-p', '-d'], + expect.any(Function), + ); + expect(mockedExecFile).toHaveBeenNthCalledWith( + 3, 'tmux', ['send-keys', '-t', 'main:0.1', 'Enter'], expect.any(Function), ); - expect(mockedExecFile).toHaveBeenCalledTimes(2); + expect(mockedExecFile).toHaveBeenCalledTimes(3); }); it('throws on tmux failure', async () => { diff --git a/packages/agent-manager/src/terminal/TtyWriter.ts b/packages/agent-manager/src/terminal/TtyWriter.ts index 864efc6f..082c4256 100644 --- a/packages/agent-manager/src/terminal/TtyWriter.ts +++ b/packages/agent-manager/src/terminal/TtyWriter.ts @@ -39,16 +39,33 @@ export class TtyWriter { } private static async sendViaTmux(identifier: string, message: string): Promise { - // Send text and Enter as two separate calls so that Enter arrives - // outside of bracketed paste mode. When the inner application (e.g. - // Claude Code) has bracketed paste enabled, tmux wraps the send-keys - // payload in paste brackets — if Enter is included, it gets swallowed - // as part of the paste instead of acting as a submit action. - await execFileAsync('tmux', ['send-keys', '-t', identifier, '-l', message]); + // Paste the message body using tmux bracketed paste, then send Enter as + // a separate key so the inner TUI treats it as submission rather than + // pasted content. + const bufferName = `ai-devkit-send-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`; + await TtyWriter.execFileWithInput('tmux', ['load-buffer', '-b', bufferName, '-'], message); + await execFileAsync('tmux', ['paste-buffer', '-t', identifier, '-b', bufferName, '-p', '-d']); await new Promise((resolve) => setTimeout(resolve, 150)); await execFileAsync('tmux', ['send-keys', '-t', identifier, 'Enter']); } + private static async execFileWithInput(command: string, args: string[], input: string): Promise { + await new Promise((resolve, reject) => { + const child = execFile(command, args, (error) => { + if (error) { + reject(error); + return; + } + resolve(); + }); + if (!child.stdin) { + reject(new Error(`Cannot write stdin to ${command}`)); + return; + } + child.stdin.end(input); + }); + } + /** * Build an AppleScript that finds an iTerm2 session by TTY and runs a * command against it. The `sessionCommand` is inserted inside a