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.",
|
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 =
|
export type ExecToolDetails =
|
||||||
@ -749,12 +755,21 @@ export function createExecTool(
|
|||||||
security?: string;
|
security?: string;
|
||||||
ask?: string;
|
ask?: string;
|
||||||
node?: string;
|
node?: string;
|
||||||
|
autoEnter?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!params.command) {
|
if (!params.command) {
|
||||||
throw new Error("Provide a command to start.");
|
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 maxOutput = DEFAULT_MAX_OUTPUT;
|
||||||
const pendingMaxOutput = DEFAULT_PENDING_MAX_OUTPUT;
|
const pendingMaxOutput = DEFAULT_PENDING_MAX_OUTPUT;
|
||||||
const warnings: string[] = [];
|
const warnings: string[] = [];
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user