Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/core/prompts/tools/execute-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ import { ToolArgs } from "./types"

export function getExecuteCommandDescription(args: ToolArgs): string | undefined {
return `## execute_command
Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Prefer relative commands and paths that avoid location sensitivity for terminal consistency, e.g: \`touch ./testdata/example.file\`, \`dir ./examples/model1/data/yaml\`, or \`go test ./cmd/front --config ./cmd/front/config.yml\`. If directed by the user, you may open a terminal in a different directory by using the \`cwd\` parameter.
Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for your shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Prefer relative commands and paths that avoid location sensitivity for terminal consistency, e.g: \`touch ./testdata/example.file\`, \`dir ./examples/model1/data/yaml\`, or \`go test ./cmd/front --config ./cmd/front/config.yml\`. If directed by the user, you may open a terminal in a different directory by using the \`cwd\` parameter.
Parameters:
- command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions.
- cwd: (optional) The working directory to execute the command in (default: ${args.cwd})
- background: (optional) Set to "true" to run the command in the background without interrupting with questions. Default is false.
Usage:
<execute_command>
<command>Your command here</command>
<cwd>Working directory path (optional)</cwd>
<background>true</background>
</execute_command>

Example: Requesting to execute npm run dev
Expand All @@ -21,5 +23,11 @@ Example: Requesting to execute ls in a specific directory if directed
<execute_command>
<command>ls -la</command>
<cwd>/home/user/projects</cwd>
</execute_command>

Example: Running a long-running command in the background
<execute_command>
<command>npm run build</command>
<background>true</background>
</execute_command>`
}
24 changes: 15 additions & 9 deletions src/core/tools/executeCommandTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export async function executeCommandTool(
) {
let command: string | undefined = block.params.command
const customCwd: string | undefined = block.params.cwd
const background: boolean = block.params.background === "true"

try {
if (block.partial) {
Expand Down Expand Up @@ -90,6 +91,7 @@ export async function executeCommandTool(
executionId,
command,
customCwd,
background,
terminalShellIntegrationDisabled,
terminalOutputLineLimit,
terminalOutputCharacterLimit,
Expand Down Expand Up @@ -137,6 +139,7 @@ export type ExecuteCommandOptions = {
executionId: string
command: string
customCwd?: string
background?: boolean
terminalShellIntegrationDisabled?: boolean
terminalOutputLineLimit?: number
terminalOutputCharacterLimit?: number
Expand All @@ -149,6 +152,7 @@ export async function executeCommand(
executionId,
command,
customCwd,
background = false,
terminalShellIntegrationDisabled = true,
terminalOutputLineLimit = 500,
terminalOutputCharacterLimit = DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT,
Expand All @@ -174,7 +178,7 @@ export async function executeCommand(
}

let message: { text?: string; images?: string[] } | undefined
let runInBackground = false
let runInBackground = background
let completed = false
let result: string = ""
let exitDetails: ExitCodeDetails | undefined
Expand All @@ -199,15 +203,17 @@ export async function executeCommand(
return
}

try {
const { response, text, images } = await task.ask("command_output", "")
runInBackground = true
if (!background) {
try {
const { response, text, images } = await task.ask("command_output", "")
runInBackground = true

if (response === "messageResponse") {
message = { text, images }
process.continue()
}
} catch (_error) {}
if (response === "messageResponse") {
message = { text, images }
process.continue()
}
} catch (_error) {}
}
},
onCompleted: (output: string | undefined) => {
result = Terminal.compressTerminalOutput(
Expand Down
161 changes: 161 additions & 0 deletions src/integrations/terminal/__tests__/background-execution.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// background-execution.spec.ts

import { describe, it, expect, vi } from "vitest"
import { Task } from "../../../core/task/Task"
import { executeCommand, ExecuteCommandOptions } from "../../../core/tools/executeCommandTool"
import { TerminalRegistry } from "../TerminalRegistry"
import { Terminal } from "../Terminal"

// Mock Task class
class MockTask {
taskId = "test-task"
cwd = "/test/workspace"
consecutiveMistakeCount = 0
didRejectTool = false
lastMessageTs = undefined
terminalProcess = undefined

async say() {}
async ask() {
throw new Error("Should not be called for background execution")
}
recordToolError() {}
}

vi.mock("fs/promises")
vi.mock("../TerminalRegistry")
vi.mock("../Terminal")
vi.mock("../../../i18n")

describe("Background execution", () => {
let mockTask: MockTask
let mockProvider: any

beforeEach(() => {
mockTask = new MockTask()
mockProvider = {
postMessageToWebview: vi.fn(),
getState: vi.fn().mockResolvedValue({
terminalOutputLineLimit: 500,
terminalOutputCharacterLimit: 10000,
terminalShellIntegrationDisabled: true,
}),
}

// Mock terminal methods
vi.mocked(TerminalRegistry.getOrCreateTerminal).mockResolvedValue({
getCurrentWorkingDirectory: () => "/test/workspace",
runCommand: vi.fn().mockReturnValue(Promise.resolve()),
terminal: {
show: vi.fn(),
},
} as any)
})

it("should execute command in background when background parameter is true", async () => {
const options: ExecuteCommandOptions = {
executionId: "test-id",
command: "echo 'test'",
background: true,
terminalOutputLineLimit: 500,
terminalOutputCharacterLimit: 10000,
}

// Mock the callbacks
const callbacks = {
onLine: vi.fn(),
onCompleted: vi.fn(),
onShellExecutionStarted: vi.fn(),
onShellExecutionComplete: vi.fn(),
}

// Mock terminal instance
const mockTerminal = {
getCurrentWorkingDirectory: () => "/test/workspace",
runCommand: vi.fn().mockReturnValue(Promise.resolve()),
terminal: { show: vi.fn() },
}

vi.mocked(TerminalRegistry.getOrCreateTerminal).mockResolvedValue(mockTerminal as any)

// Execute command with background = true
const [rejected, result] = await executeCommand(mockTask, options)

// Verify that task.ask was NOT called (which means it ran in background)
// Since we can't easily test the internal callback behavior, we verify the command was set up for background execution
expect(rejected).toBe(false)
expect(typeof result).toBe("string")
})

it("should execute command with background=false by default", async () => {
const options: ExecuteCommandOptions = {
executionId: "test-id",
command: "echo 'test'",
background: false,
terminalOutputLineLimit: 500,
terminalOutputCharacterLimit: 10000,
}

// Mock terminal instance
const mockTerminal = {
getCurrentWorkingDirectory: () => "/test/workspace",
runCommand: vi.fn().mockReturnValue(Promise.resolve()),
terminal: { show: vi.fn() },
}

vi.mocked(TerminalRegistry.getOrCreateTerminal).mockResolvedValue(mockTerminal as any)

const [rejected, result] = await executeCommand(mockTask, options)

expect(rejected).toBe(false)
expect(typeof result).toBe("string")
})

it("should handle background parameter as boolean true", async () => {
const options: ExecuteCommandOptions = {
executionId: "test-id",
command: "echo 'test'",
background: true,
terminalOutputLineLimit: 500,
terminalOutputCharacterLimit: 10000,
}

// Mock terminal instance
const mockTerminal = {
getCurrentWorkingDirectory: () => "/test/workspace",
runCommand: vi.fn().mockReturnValue(Promise.resolve()),
terminal: { show: vi.fn() },
}

vi.mocked(TerminalRegistry.getOrCreateTerminal).mockResolvedValue(mockTerminal as any)

const [rejected, result] = await executeCommand(mockTask, options)

expect(rejected).toBe(false)
// When background is true, the command should run and complete
expect(result).toContain("Command executed in terminal")
})

it("should default background to false when not provided", async () => {
const options: ExecuteCommandOptions = {
executionId: "test-id",
command: "echo 'test'",
terminalOutputLineLimit: 500,
terminalOutputCharacterLimit: 10000,
}

// Mock terminal instance
const mockTerminal = {
getCurrentWorkingDirectory: () => "/test/workspace",
runCommand: vi.fn().mockReturnValue(Promise.resolve()),
terminal: { show: vi.fn() },
}

vi.mocked(TerminalRegistry.getOrCreateTerminal).mockResolvedValue(mockTerminal as any)

const [rejected, result] = await executeCommand(mockTask, options)

expect(rejected).toBe(false)
expect(typeof result).toBe("string")
})
})
3 changes: 2 additions & 1 deletion src/shared/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export const toolParamNames = [
"size",
"query",
"args",
"background",
"start_line",
"end_line",
"todos",
Expand All @@ -77,7 +78,7 @@ export interface ToolUse {
export interface ExecuteCommandToolUse extends ToolUse {
name: "execute_command"
// Pick<Record<ToolParamName, string>, "command"> makes "command" required, but Partial<> makes it optional
params: Partial<Pick<Record<ToolParamName, string>, "command" | "cwd">>
params: Partial<Pick<Record<ToolParamName, string>, "command" | "cwd" | "background">>
}

export interface ReadFileToolUse extends ToolUse {
Expand Down
Loading