diff --git a/packages/types/src/tool.ts b/packages/types/src/tool.ts
index 2c7495e5ebef..2bad4ec5b385 100644
--- a/packages/types/src/tool.ts
+++ b/packages/types/src/tool.ts
@@ -33,6 +33,7 @@ export const toolNames = [
"new_task",
"fetch_instructions",
"codebase_search",
+ "terminal_kill",
"update_todo_list",
"run_slash_command",
"generate_image",
diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts
index 689675999fd1..5cf02fe74c04 100644
--- a/src/core/assistant-message/presentAssistantMessage.ts
+++ b/src/core/assistant-message/presentAssistantMessage.ts
@@ -37,6 +37,7 @@ import { Task } from "../task/Task"
import { codebaseSearchTool } from "../tools/codebaseSearchTool"
import { experiments, EXPERIMENT_IDS } from "../../shared/experiments"
import { applyDiffToolLegacy } from "../tools/applyDiffTool"
+import { terminalKillTool } from "../tools/terminalKillTool"
/**
* Processes and presents assistant message content to the user interface.
@@ -223,10 +224,14 @@ export async function presentAssistantMessage(cline: Task) {
const modeName = getModeBySlug(mode, customModes)?.name ?? mode
return `[${block.name} in ${modeName} mode: '${message}']`
}
+ case "terminal_kill":
+ return `[${block.name}]`
case "run_slash_command":
return `[${block.name} for '${block.params.command}'${block.params.args ? ` with args: ${block.params.args}` : ""}]`
case "generate_image":
return `[${block.name} for '${block.params.path}']`
+ default:
+ return `[${block.name}]`
}
}
@@ -552,6 +557,9 @@ export async function presentAssistantMessage(cline: Task) {
askFinishSubTaskApproval,
)
break
+ case "terminal_kill":
+ await terminalKillTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
+ break
case "run_slash_command":
await runSlashCommandTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
break
diff --git a/src/core/prompts/tools/execute-command.ts b/src/core/prompts/tools/execute-command.ts
index c1fc1ea3f19c..8c9220ad54c3 100644
--- a/src/core/prompts/tools/execute-command.ts
+++ b/src/core/prompts/tools/execute-command.ts
@@ -6,15 +6,18 @@ Description: Request to execute a CLI command on the system. Use this when you n
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})
+- run_in_background: (optional) If true, runs the command in the background without user interaction. Useful for long-running commands like dev servers or monitoring (default: false)
+
Usage:
Your command here
Working directory path (optional)
-Example: Requesting to execute npm run dev
+Example: Requesting to execute npm run dev in background
npm run dev
+true
Example: Requesting to execute ls in a specific directory if directed
diff --git a/src/core/prompts/tools/index.ts b/src/core/prompts/tools/index.ts
index c212b18a3de4..dffb83535ff3 100644
--- a/src/core/prompts/tools/index.ts
+++ b/src/core/prompts/tools/index.ts
@@ -28,6 +28,7 @@ import { getUpdateTodoListDescription } from "./update-todo-list"
import { getRunSlashCommandDescription } from "./run-slash-command"
import { getGenerateImageDescription } from "./generate-image"
import { CodeIndexManager } from "../../../services/code-index/manager"
+import { getTerminalKillDescription } from "./terminal-ctrl"
// Map of tool names to their description functions
const toolDescriptionMap: Record string | undefined> = {
@@ -58,6 +59,7 @@ const toolDescriptionMap: Record string | undefined>
apply_diff: (args) =>
args.diffStrategy ? args.diffStrategy.getToolDescription({ cwd: args.cwd, toolOptions: args.toolOptions }) : "",
update_todo_list: (args) => getUpdateTodoListDescription(args),
+ terminal_kill: (args) => getTerminalKillDescription(args),
run_slash_command: () => getRunSlashCommandDescription(),
generate_image: (args) => getGenerateImageDescription(args),
}
@@ -177,6 +179,7 @@ export {
getSwitchModeDescription,
getInsertContentDescription,
getSearchAndReplaceDescription,
+ getTerminalKillDescription,
getCodebaseSearchDescription,
getRunSlashCommandDescription,
getGenerateImageDescription,
diff --git a/src/core/prompts/tools/terminal-ctrl.ts b/src/core/prompts/tools/terminal-ctrl.ts
new file mode 100644
index 000000000000..4cdc57259a02
--- /dev/null
+++ b/src/core/prompts/tools/terminal-ctrl.ts
@@ -0,0 +1,14 @@
+import { ToolArgs } from "./types"
+
+export function getTerminalKillDescription(args: ToolArgs): string | undefined {
+ return `## terminal_kill
+Description: Manage running processes in terminals.
+
+Parameters:
+- terminal_id: (required) The terminal ID containing the process to kill
+
+Usage example: Kill a process running in terminal 1
+
+1
+`
+}
diff --git a/src/core/tools/__tests__/executeCommandTimeout.integration.spec.ts b/src/core/tools/__tests__/executeCommandTimeout.integration.spec.ts
index b9e0af3a8a38..e1c5f48c429b 100644
--- a/src/core/tools/__tests__/executeCommandTimeout.integration.spec.ts
+++ b/src/core/tools/__tests__/executeCommandTimeout.integration.spec.ts
@@ -21,6 +21,7 @@ vitest.mock("../../prompts/responses", () => ({
formatResponse: {
toolError: vitest.fn((msg) => `Tool Error: ${msg}`),
rooIgnoreError: vitest.fn((msg) => `RooIgnore Error: ${msg}`),
+ toolResult: vitest.fn((content, images) => `Tool Result: ${content}`),
},
}))
vitest.mock("../../../utils/text-normalization", () => ({
@@ -52,6 +53,7 @@ describe("Command Execution Timeout Integration", () => {
postMessageToWebview: vitest.fn(),
}),
},
+ ask: vitest.fn(),
say: vitest.fn().mockResolvedValue(undefined),
}
diff --git a/src/core/tools/__tests__/executeCommandTool.spec.ts b/src/core/tools/__tests__/executeCommandTool.spec.ts
index dbb1945177a3..a46690685a9f 100644
--- a/src/core/tools/__tests__/executeCommandTool.spec.ts
+++ b/src/core/tools/__tests__/executeCommandTool.spec.ts
@@ -67,7 +67,8 @@ beforeEach(() => {
// Get the custom working directory if provided
const customCwd = block.params.cwd
- const [userRejected, result] = await mockExecuteCommand(cline, block.params.command, customCwd)
+ const runInBackground = block.params.run_in_background === "true"
+ const [userRejected, result] = await mockExecuteCommand(cline, block.params.command, customCwd, runInBackground)
if (userRejected) {
cline.didRejectTool = true
@@ -309,4 +310,41 @@ describe("executeCommandTool", () => {
expect(mockOptions.commandExecutionTimeout).toBeDefined()
})
})
+
+ describe("run_in_background parameter", () => {
+ it("should extract run_in_background parameter when set to 'true'", async () => {
+ mockToolUse.params.command = "npm run dev"
+ mockToolUse.params.run_in_background = "true"
+
+ await executeCommandTool(
+ mockCline as unknown as Task,
+ mockToolUse,
+ mockAskApproval as unknown as AskApproval,
+ mockHandleError as unknown as HandleError,
+ mockPushToolResult as unknown as PushToolResult,
+ mockRemoveClosingTag as unknown as RemoveClosingTag,
+ )
+
+ expect(mockExecuteCommand).toHaveBeenCalled()
+ const lastCall = mockExecuteCommand.mock.calls[mockExecuteCommand.mock.calls.length - 1]
+ expect(lastCall[3]).toBe(true) // run_in_background parameter should be true
+ })
+
+ it("should default run_in_background to false when parameter is missing", async () => {
+ mockToolUse.params.command = "echo test"
+
+ await executeCommandTool(
+ mockCline as unknown as Task,
+ mockToolUse,
+ mockAskApproval as unknown as AskApproval,
+ mockHandleError as unknown as HandleError,
+ mockPushToolResult as unknown as PushToolResult,
+ mockRemoveClosingTag as unknown as RemoveClosingTag,
+ )
+
+ expect(mockExecuteCommand).toHaveBeenCalled()
+ const lastCall = mockExecuteCommand.mock.calls[mockExecuteCommand.mock.calls.length - 1]
+ expect(lastCall[3]).toBe(false) // run_in_background parameter should be false
+ })
+ })
})
diff --git a/src/core/tools/executeCommandTool.ts b/src/core/tools/executeCommandTool.ts
index 2c7ce0d023e2..25d8dcfbfb59 100644
--- a/src/core/tools/executeCommandTool.ts
+++ b/src/core/tools/executeCommandTool.ts
@@ -30,6 +30,7 @@ export async function executeCommandTool(
) {
let command: string | undefined = block.params.command
const customCwd: string | undefined = block.params.cwd
+ const runInBackground: boolean = block.params.run_in_background === "true"
try {
if (block.partial) {
@@ -94,6 +95,7 @@ export async function executeCommandTool(
terminalOutputLineLimit,
terminalOutputCharacterLimit,
commandExecutionTimeout,
+ runInBackground,
}
try {
@@ -141,6 +143,7 @@ export type ExecuteCommandOptions = {
terminalOutputLineLimit?: number
terminalOutputCharacterLimit?: number
commandExecutionTimeout?: number
+ runInBackground?: boolean
}
export async function executeCommand(
@@ -150,6 +153,7 @@ export async function executeCommand(
command,
customCwd,
terminalShellIntegrationDisabled = false,
+ runInBackground: runInBackgroundRequested = false,
terminalOutputLineLimit = 500,
terminalOutputCharacterLimit = DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT,
commandExecutionTimeout = 0,
@@ -174,7 +178,7 @@ export async function executeCommand(
}
let message: { text?: string; images?: string[] } | undefined
- let runInBackground = false
+ let runInBackground = runInBackgroundRequested
let completed = false
let result: string = ""
let exitDetails: ExitCodeDetails | undefined
@@ -219,10 +223,14 @@ export async function executeCommand(
task.say("command_output", result)
completed = true
},
- onShellExecutionStarted: (pid: number | undefined) => {
+ onShellExecutionStarted: (pid: number | undefined, process: RooTerminalProcess) => {
console.log(`[executeCommand] onShellExecutionStarted: ${pid}`)
const status: CommandExecutionStatus = { executionId, status: "started", pid, command }
provider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) })
+
+ if (runInBackground) {
+ process.continue()
+ }
},
onShellExecutionComplete: (details: ExitCodeDetails) => {
const status: CommandExecutionStatus = { executionId, status: "exited", exitCode: details.exitCode }
diff --git a/src/core/tools/terminalKillTool.ts b/src/core/tools/terminalKillTool.ts
new file mode 100644
index 000000000000..97c48ce00e4e
--- /dev/null
+++ b/src/core/tools/terminalKillTool.ts
@@ -0,0 +1,111 @@
+import { TerminalRegistry } from "../../integrations/terminal/TerminalRegistry"
+import { Terminal } from "../../integrations/terminal/Terminal"
+import { formatResponse } from "../prompts/responses"
+import { Task } from "../task/Task"
+import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools"
+
+export async function terminalKillTool(
+ task: Task,
+ block: ToolUse,
+ askApproval: AskApproval,
+ handleError: HandleError,
+ pushToolResult: PushToolResult,
+ removeClosingTag: RemoveClosingTag,
+) {
+ const terminalId: string | undefined = block.params.terminal_id
+
+ try {
+ if (block.partial) {
+ await task.ask("tool", removeClosingTag("terminal_id", terminalId), block.partial).catch(() => {})
+ return
+ }
+
+ if (!terminalId) {
+ task.consecutiveMistakeCount++
+ task.recordToolError("terminal_kill")
+ pushToolResult(await task.sayAndCreateMissingParamError("terminal_kill", "terminal_id"))
+ return
+ }
+
+ // Get approval for the action
+ const didApprove = await askApproval("tool", `Kill process in terminal ${terminalId}`)
+ if (!didApprove) {
+ return
+ }
+
+ try {
+ const parsedId = parseInt(terminalId)
+ if (isNaN(parsedId)) {
+ task.consecutiveMistakeCount++
+ task.recordToolError("terminal_kill")
+ pushToolResult(formatResponse.toolError(`Invalid terminal_id "${terminalId}". Must be a number.`))
+ return
+ }
+
+ const result = await killTerminalProcess(parsedId)
+ pushToolResult(formatResponse.toolResult(result))
+ } catch (error) {
+ await handleError("killing terminal process", error)
+ }
+ } catch (error) {
+ await handleError("terminal control operation", error)
+ }
+}
+
+/**
+ * Kills a process running in a specific terminal by sending Ctrl+C
+ * @param terminalId The terminal ID containing the process to kill
+ * @returns Promise Result message
+ */
+async function killTerminalProcess(terminalId: number): Promise {
+ const targetTerminal = findTerminal(terminalId)
+ if (!targetTerminal) {
+ return getTerminalNotFoundMessage(terminalId)
+ }
+
+ if (!targetTerminal.busy && !targetTerminal.process) {
+ return `Terminal ${terminalId} is not running any process.`
+ }
+
+ try {
+ if (targetTerminal instanceof Terminal) {
+ // For VSCode terminals, send Ctrl+C
+ targetTerminal.terminal.sendText("\x03")
+ return `Sent Ctrl+C to terminal ${terminalId}. Process should terminate shortly.`
+ } else {
+ // For ExecaTerminal, use the abort method
+ if (targetTerminal.process) {
+ targetTerminal.process.abort()
+ return `Terminated process in terminal ${terminalId}.`
+ } else {
+ return `No active process found in terminal ${terminalId}.`
+ }
+ }
+ } catch (error) {
+ throw new Error(
+ `Failed to kill process in terminal ${terminalId}: ${error instanceof Error ? error.message : String(error)}`,
+ )
+ }
+}
+
+/**
+ * Helper function to find a terminal by ID
+ */
+function findTerminal(terminalId: number) {
+ const busyTerminals = TerminalRegistry.getTerminals(true)
+ const allTerminals = TerminalRegistry.getTerminals(false)
+ const allTerminalsList = [...busyTerminals, ...allTerminals]
+
+ return allTerminalsList.find((t) => t.id === terminalId)
+}
+
+/**
+ * Helper function to get terminal not found message
+ */
+function getTerminalNotFoundMessage(terminalId: number): string {
+ const busyTerminals = TerminalRegistry.getTerminals(true)
+ const allTerminals = TerminalRegistry.getTerminals(false)
+ const allTerminalsList = [...busyTerminals, ...allTerminals]
+
+ return `Terminal ${terminalId} not found. Available terminals: ${allTerminalsList.map((t) => t.id).join(", ")}`
+}
diff --git a/src/shared/tools.ts b/src/shared/tools.ts
index 608b50752e7d..615c835f48f4 100644
--- a/src/shared/tools.ts
+++ b/src/shared/tools.ts
@@ -67,6 +67,8 @@ export const toolParamNames = [
"todos",
"prompt",
"image",
+ "run_in_background",
+ "terminal_id",
] as const
export type ToolParamName = (typeof toolParamNames)[number]
@@ -82,7 +84,12 @@ export interface ToolUse {
export interface ExecuteCommandToolUse extends ToolUse {
name: "execute_command"
// Pick, "command"> makes "command" required, but Partial<> makes it optional
- params: Partial, "command" | "cwd">>
+ params: Partial, "command" | "cwd" | "run_in_background">>
+}
+
+export interface TerminalCtrlToolUse extends ToolUse {
+ name: "terminal_kill"
+ params: Partial, "terminal_id">>
}
export interface ReadFileToolUse extends ToolUse {
@@ -200,6 +207,7 @@ export const TOOL_DISPLAY_NAMES: Record = {
new_task: "create new task",
insert_content: "insert content",
search_and_replace: "search and replace",
+ terminal_kill: "terminal kill",
codebase_search: "codebase search",
update_todo_list: "update todo list",
run_slash_command: "run slash command",
@@ -225,7 +233,7 @@ export const TOOL_GROUPS: Record = {
tools: ["browser_action"],
},
command: {
- tools: ["execute_command"],
+ tools: ["execute_command", "terminal_kill"],
},
mcp: {
tools: ["use_mcp_tool", "access_mcp_resource"],