From 32252859ebaae0e80d8a64d982c95162a64ab39e Mon Sep 17 00:00:00 2001 From: Yeachan-Heo Date: Tue, 27 Jan 2026 01:16:33 +0000 Subject: [PATCH] feat(exec): add autoEnter option for tmux send-keys commands Add optional `autoEnter` parameter to the exec tool that automatically appends `Enter` to tmux send-keys commands when it's missing. This helps agents reliably execute commands in tmux sessions without forgetting to press Enter. - Add `autoEnter?: boolean` to exec tool schema - Auto-detect `tmux send-keys` pattern and append Enter if missing - Default is false for safety (opt-in behavior) - Add comprehensive tests for the feature Co-Authored-By: Claude Opus 4.5 --- .../bash-tools.exec.tmux-autoenter.test.ts | 101 ++++++++++++++++++ src/agents/bash-tools.exec.ts | 15 +++ 2 files changed, 116 insertions(+) create mode 100644 src/agents/bash-tools.exec.tmux-autoenter.test.ts diff --git a/src/agents/bash-tools.exec.tmux-autoenter.test.ts b/src/agents/bash-tools.exec.tmux-autoenter.test.ts new file mode 100644 index 000000000..d653a4915 --- /dev/null +++ b/src/agents/bash-tools.exec.tmux-autoenter.test.ts @@ -0,0 +1,101 @@ +import { afterEach, describe, expect, test } from "vitest"; + +import { resetProcessRegistryForTests } from "./bash-process-registry"; +import { createExecTool } from "./bash-tools.exec"; + +afterEach(() => { + resetProcessRegistryForTests(); +}); + +describe("tmux send-keys autoEnter", () => { + test("regular exec commands unchanged when autoEnter is true", async () => { + const execTool = createExecTool(); + const result = await execTool.execute("toolcall", { + command: "echo hello", + autoEnter: true, + }); + // Should complete normally without modification + expect(result.details.status).toBe("completed"); + expect((result.details as { aggregated?: string }).aggregated).toContain("hello"); + }); + + test("tmux send-keys with Enter unchanged when autoEnter is true", async () => { + const execTool = createExecTool(); + // This command already has Enter, so it shouldn't be added again + // We use echo to simulate the command (can't actually run tmux in test) + const result = await execTool.execute("toolcall", { + command: 'echo "tmux send-keys -t test echo hello Enter"', + autoEnter: true, + }); + expect(result.details.status).toBe("completed"); + // The echo should show the command unchanged (only one Enter) + const output = (result.details as { aggregated?: string }).aggregated ?? ""; + expect(output).toContain("Enter"); + // Should not have "Enter Enter" (double Enter) + expect(output).not.toMatch(/Enter\s+Enter/); + }); + + test("tmux send-keys without Enter gets Enter appended when autoEnter is true", async () => { + const execTool = createExecTool(); + // We can verify the auto-enter logic by checking that the command runs with Enter appended. + // Since we can't run actual tmux in tests, we use a marker approach: + // The command will include 'send-keys' and we can verify via echo that Enter is appended. + const result = await execTool.execute("toolcall", { + command: 'sh -c \'echo "$0" "$@"\' tmux send-keys -t test "hello"', + autoEnter: true, + }); + expect(result.details.status).toBe("completed"); + const output = (result.details as { aggregated?: string }).aggregated ?? ""; + // Since autoEnter is true and the command contains 'tmux send-keys' without Enter, + // Enter should be appended. The echo will show the arguments including Enter. + expect(output).toContain("Enter"); + }); + + test("tmux send-keys unchanged when autoEnter is false", async () => { + const execTool = createExecTool(); + const result = await execTool.execute("toolcall", { + command: 'sh -c \'echo "$0" "$@"\' tmux send-keys -t test "hello"', + autoEnter: false, + }); + expect(result.details.status).toBe("completed"); + const output = (result.details as { aggregated?: string }).aggregated ?? ""; + // autoEnter is false, so Enter should NOT be appended + expect(output).not.toContain("Enter"); + }); + + test("tmux send-keys unchanged when autoEnter is not set", async () => { + const execTool = createExecTool(); + const result = await execTool.execute("toolcall", { + command: 'sh -c \'echo "$0" "$@"\' tmux send-keys -t test "hello"', + }); + expect(result.details.status).toBe("completed"); + const output = (result.details as { aggregated?: string }).aggregated ?? ""; + // autoEnter defaults to undefined/false, so Enter should NOT be appended + expect(output).not.toContain("Enter"); + }); + + test("tmux send-keys with trailing quote and Enter unchanged", async () => { + const execTool = createExecTool(); + const result = await execTool.execute("toolcall", { + command: 'echo "tmux send-keys -t test hello Enter"', + autoEnter: true, + }); + expect(result.details.status).toBe("completed"); + const output = (result.details as { aggregated?: string }).aggregated ?? ""; + // Should have exactly one Enter, not double + const enterCount = (output.match(/Enter/g) || []).length; + expect(enterCount).toBe(1); + }); + + test("handles tmux -S socket send-keys pattern", async () => { + const execTool = createExecTool(); + const result = await execTool.execute("toolcall", { + command: 'sh -c \'echo "$0" "$@"\' tmux -S /tmp/socket send-keys -t test "cmd"', + autoEnter: true, + }); + expect(result.details.status).toBe("completed"); + const output = (result.details as { aggregated?: string }).aggregated ?? ""; + // Should detect 'tmux ... send-keys' and append Enter + expect(output).toContain("Enter"); + }); +}); diff --git a/src/agents/bash-tools.exec.ts b/src/agents/bash-tools.exec.ts index b9de81872..f79747b02 100644 --- a/src/agents/bash-tools.exec.ts +++ b/src/agents/bash-tools.exec.ts @@ -190,6 +190,12 @@ const execSchema = Type.Object({ description: "Node id/name for host=node.", }), ), + autoEnter: Type.Optional( + Type.Boolean({ + description: + "Auto-append Enter to tmux send-keys commands that are missing it. Default false.", + }), + ), }); export type ExecToolDetails = @@ -749,12 +755,21 @@ export function createExecTool( security?: string; ask?: string; node?: string; + autoEnter?: boolean; }; if (!params.command) { throw new Error("Provide a command to start."); } + // Auto-append Enter to tmux send-keys commands if autoEnter is enabled and missing. + if (params.autoEnter && /tmux\s+.*send-keys/i.test(params.command)) { + // Check if the command already ends with Enter (possibly followed by whitespace or quotes). + if (!/\bEnter['"]?\s*$/.test(params.command)) { + params.command = `${params.command} Enter`; + } + } + const maxOutput = DEFAULT_MAX_OUTPUT; const pendingMaxOutput = DEFAULT_PENDING_MAX_OUTPUT; const warnings: string[] = [];