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 <noreply@anthropic.com>
This commit is contained in:
parent
b861a0bd73
commit
32252859eb
101
src/agents/bash-tools.exec.tmux-autoenter.test.ts
Normal file
101
src/agents/bash-tools.exec.tmux-autoenter.test.ts
Normal file
@ -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");
|
||||
});
|
||||
});
|
||||
@ -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[] = [];
|
||||
|
||||
Loading…
Reference in New Issue
Block a user