diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 6e61c3950f5..67cb3afbdb6 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -160,6 +160,7 @@ export const globalSettingsSchema = z.object({ terminalZshP10k: z.boolean().optional(), terminalZdotdir: z.boolean().optional(), terminalCompressProgressBar: z.boolean().optional(), + windowsScriptExecutionEnabled: z.boolean().optional(), diagnosticsEnabled: z.boolean().optional(), diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index 75b28bbb213..9b9c589720a 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -68,6 +68,19 @@ async function generatePrompt( throw new Error("Extension context is required for generating system prompt") } + const promptSettings: SystemPromptSettings = settings + ? { ...settings, isWindows: settings.isWindows ?? os.platform() === "win32" } + : { + maxConcurrentFileReads: 5, + todoListEnabled: true, + browserToolEnabled: true, + useAgentRules: true, + newTaskRequireTodos: false, + toolProtocol: undefined, + windowsScriptExecutionEnabled: true, + isWindows: os.platform() === "win32", + } + // If diff is disabled, don't pass the diffStrategy const effectiveDiffStrategy = diffEnabled ? diffStrategy : undefined @@ -111,7 +124,7 @@ async function generatePrompt( customModeConfigs, experiments, partialReadsEnabled, - settings, + promptSettings, enableMcpServerCreation, modelId, )}` @@ -139,7 +152,7 @@ ${getObjectiveSection()} ${await addCustomInstructions(baseInstructions, globalCustomInstructions || "", cwd, mode, { language: language ?? formatLanguage(vscode.env.language), rooIgnoreInstructions, - settings, + settings: promptSettings, })}` return basePrompt diff --git a/src/core/prompts/tools/execute-command.ts b/src/core/prompts/tools/execute-command.ts index c1fc1ea3f19..99ab6b054a3 100644 --- a/src/core/prompts/tools/execute-command.ts +++ b/src/core/prompts/tools/execute-command.ts @@ -1,7 +1,10 @@ import { ToolArgs } from "./types" export function getExecuteCommandDescription(args: ToolArgs): string | undefined { - return `## execute_command + const isWindows = args.settings?.isWindows ?? args.isWindows ?? process.platform === "win32" + const scriptModeEnabled = isWindows && (args.settings?.windowsScriptExecutionEnabled ?? true) + + const base = `## 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. 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. @@ -21,5 +24,25 @@ Example: Requesting to execute ls in a specific directory if directed ls -la /home/user/projects +` + + if (!scriptModeEnabled) { + return base + } + + return `${base} + +Windows-only script mode for long inputs: +- If the command would exceed Windows command-length limits or is heavy on quotes/percent signs (cmd expands %VAR% and requires %% inside FOR), send the script body in \`script_content\` and the interpreter/shell in \`script_runner\` (for example \`powershell.exe\`, \`cmd.exe\`, \`python\`). +- Roo will write the script to a temporary file, run it with the provided runner, and delete the file automatically. Do not provide filenames, args, or env separately—put everything needed inside the script body. +- Use multi-line bodies (here-strings) instead of one giant inline command to avoid escaping issues, especially with cmd percent signs and PowerShell quotes. + +Example: Running a long PowerShell snippet + +@' +Write-Output "Hello from script" +Write-Output "Another line without needing to escape % or quotes" +'@ +powershell.exe ` } diff --git a/src/core/prompts/tools/index.ts b/src/core/prompts/tools/index.ts index a9e195c166e..307d61c6329 100644 --- a/src/core/prompts/tools/index.ts +++ b/src/core/prompts/tools/index.ts @@ -87,6 +87,7 @@ export function getToolDescriptionsForMode( modelId, }, experiments, + isWindows: settings?.isWindows, } const tools = new Set() diff --git a/src/core/prompts/tools/native-tools/execute_command.ts b/src/core/prompts/tools/native-tools/execute_command.ts index 4b97b99eb57..7a4d3d695b1 100644 --- a/src/core/prompts/tools/native-tools/execute_command.ts +++ b/src/core/prompts/tools/native-tools/execute_command.ts @@ -1,44 +1,85 @@ import type OpenAI from "openai" -const 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. +const buildDescription = (enableScriptMode: boolean) => `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.${ + enableScriptMode + ? " On Windows you can also provide a script body when the command would be too long or hard to escape (percent signs in cmd, nested quotes, here-strings): supply the script text via script_content and the interpreter/shell via script_runner; Roo will write it to a temporary file, run it, and remove it." + : "" +} 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 +- command: (required unless script_content is used) 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${ + enableScriptMode + ? ` +- script_content: (Windows-only, optional) The full text of the script to run when the command would be too long or requires heavy quoting. Do not pass arguments separately—put them in the script. Use multi-line text (here-strings) to avoid escaping percent signs and quotes. +- script_runner: (Windows-only, optional) The interpreter or shell to execute the temporary script file (e.g., powershell.exe, cmd.exe, python).` + : "" +} Example: Executing npm run dev { "command": "npm run dev", "cwd": null } Example: Executing ls in a specific directory if directed -{ "command": "ls -la", "cwd": "/home/user/projects" } +{ "command": "ls -la", "cwd": "/home/user/projects" }${ + enableScriptMode + ? ` -Example: Using relative paths -{ "command": "touch ./testdata/example.file", "cwd": null }` +Example: Long script executed via temporary file on Windows +{ "script_content": "@' +Write-Output \"Hello\" +Write-Output \"Handles %PATH% safely\" +'@", "script_runner": "powershell.exe" }` + : "" +}` const COMMAND_PARAMETER_DESCRIPTION = `Shell command to execute` - const CWD_PARAMETER_DESCRIPTION = `Optional working directory for the command, relative or absolute` +const SCRIPT_CONTENT_DESCRIPTION = `Windows-only: script body to write into a temporary file when the command is too long or hard to escape (percent signs, nested quotes)` +const SCRIPT_RUNNER_DESCRIPTION = `Windows-only: program/shell that should run the temporary script file (e.g., powershell.exe, cmd.exe, python)` + +export function getExecuteCommandTool(options: { enableScriptMode: boolean }): OpenAI.Chat.ChatCompletionTool { + const properties: Record = { + command: { + type: "string", + description: COMMAND_PARAMETER_DESCRIPTION, + }, + cwd: { + type: ["string", "null"], + description: CWD_PARAMETER_DESCRIPTION, + }, + } + + if (options.enableScriptMode) { + properties["script_content"] = { + type: "string", + description: SCRIPT_CONTENT_DESCRIPTION, + } + properties["script_runner"] = { + type: "string", + description: SCRIPT_RUNNER_DESCRIPTION, + } + } -export default { - type: "function", - function: { - name: "execute_command", - description: EXECUTE_COMMAND_DESCRIPTION, - strict: true, - parameters: { - type: "object", - properties: { - command: { - type: "string", - description: COMMAND_PARAMETER_DESCRIPTION, - }, - cwd: { - type: ["string", "null"], - description: CWD_PARAMETER_DESCRIPTION, - }, + return { + type: "function", + function: { + name: "execute_command", + description: buildDescription(options.enableScriptMode), + strict: true, + parameters: { + type: "object", + properties, + required: options.enableScriptMode ? [] : ["command"], + additionalProperties: false, + ...(options.enableScriptMode + ? { + oneOf: [ + { required: ["command"] }, + { required: ["script_content", "script_runner"] }, + ], + } + : {}), }, - required: ["command", "cwd"], - additionalProperties: false, }, - }, -} satisfies OpenAI.Chat.ChatCompletionTool + } satisfies OpenAI.Chat.ChatCompletionTool +} diff --git a/src/core/prompts/tools/native-tools/index.ts b/src/core/prompts/tools/native-tools/index.ts index 1cb0baab837..9a562cb31ad 100644 --- a/src/core/prompts/tools/native-tools/index.ts +++ b/src/core/prompts/tools/native-tools/index.ts @@ -6,7 +6,7 @@ import askFollowupQuestion from "./ask_followup_question" import attemptCompletion from "./attempt_completion" import browserAction from "./browser_action" import codebaseSearch from "./codebase_search" -import executeCommand from "./execute_command" +import { getExecuteCommandTool } from "./execute_command" import fetchInstructions from "./fetch_instructions" import generateImage from "./generate_image" import listCodeDefinitionNames from "./list_code_definition_names" @@ -27,9 +27,15 @@ export { convertOpenAIToolToAnthropic, convertOpenAIToolsToAnthropic } from "./c * Get native tools array, optionally customizing based on settings. * * @param partialReadsEnabled - Whether to include line_ranges support in read_file tool (default: true) + * @param options - Optional flags (e.g., enable Windows script mode for execute_command) * @returns Array of native tool definitions */ -export function getNativeTools(partialReadsEnabled: boolean = true): OpenAI.Chat.ChatCompletionTool[] { +export function getNativeTools( + partialReadsEnabled: boolean = true, + options: { windowsScriptModeEnabled?: boolean } = {}, +): OpenAI.Chat.ChatCompletionTool[] { + const executeCommand = getExecuteCommandTool({ enableScriptMode: options.windowsScriptModeEnabled ?? false }) + return [ accessMcpResource, apply_diff, diff --git a/src/core/prompts/tools/types.ts b/src/core/prompts/tools/types.ts index 9471d100d7b..88674f06df0 100644 --- a/src/core/prompts/tools/types.ts +++ b/src/core/prompts/tools/types.ts @@ -11,4 +11,5 @@ export type ToolArgs = { partialReadsEnabled?: boolean settings?: Record experiments?: Record + isWindows?: boolean } diff --git a/src/core/prompts/types.ts b/src/core/prompts/types.ts index 041027d1ec2..5a5be327446 100644 --- a/src/core/prompts/types.ts +++ b/src/core/prompts/types.ts @@ -12,4 +12,6 @@ export interface SystemPromptSettings { toolProtocol?: ToolProtocol /** When true, model should hide vendor/company identity in responses */ isStealthModel?: boolean + windowsScriptExecutionEnabled?: boolean + isWindows?: boolean } diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index d084bf4b924..c3ba0c19031 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -3323,6 +3323,8 @@ export class Task extends EventEmitter implements TaskLike { .get("newTaskRequireTodos", false), toolProtocol, isStealthModel: modelInfo?.isStealthModel, + windowsScriptExecutionEnabled: state?.windowsScriptExecutionEnabled ?? true, + isWindows: process.platform === "win32", }, undefined, // todoList this.api.getModel().id, @@ -3659,6 +3661,7 @@ export class Task extends EventEmitter implements TaskLike { maxReadFileLine: state?.maxReadFileLine ?? -1, browserToolEnabled: state?.browserToolEnabled ?? true, modelInfo, + windowsScriptExecutionEnabled: state?.windowsScriptExecutionEnabled ?? true, }) } diff --git a/src/core/task/build-tools.ts b/src/core/task/build-tools.ts index 4586e4b546b..61de38f94ae 100644 --- a/src/core/task/build-tools.ts +++ b/src/core/task/build-tools.ts @@ -14,6 +14,7 @@ interface BuildToolsOptions { maxReadFileLine: number browserToolEnabled: boolean modelInfo?: ModelInfo + windowsScriptExecutionEnabled?: boolean } /** @@ -53,7 +54,9 @@ export async function buildNativeToolsArray(options: BuildToolsOptions): Promise const partialReadsEnabled = maxReadFileLine !== -1 // Build native tools with dynamic read_file tool based on partialReadsEnabled - const nativeTools = getNativeTools(partialReadsEnabled) + const nativeTools = getNativeTools(partialReadsEnabled, { + windowsScriptModeEnabled: process.platform === "win32" && (options.windowsScriptExecutionEnabled ?? true), + }) // Filter native tools based on mode restrictions const filteredNativeTools = filterNativeToolsForMode( diff --git a/src/core/tools/ExecuteCommandTool.ts b/src/core/tools/ExecuteCommandTool.ts index f7271bffe9c..7b4eb3b6528 100644 --- a/src/core/tools/ExecuteCommandTool.ts +++ b/src/core/tools/ExecuteCommandTool.ts @@ -1,5 +1,7 @@ import fs from "fs/promises" import * as path from "path" +import * as os from "os" +import { randomUUID } from "crypto" import * as vscode from "vscode" import delay from "delay" @@ -22,7 +24,9 @@ import { BaseTool, ToolCallbacks } from "./BaseTool" class ShellIntegrationError extends Error {} interface ExecuteCommandParams { - command: string + command?: string + script_content?: string + script_runner?: string cwd?: string } @@ -32,23 +36,78 @@ export class ExecuteCommandTool extends BaseTool<"execute_command"> { parseLegacy(params: Partial>): ExecuteCommandParams { return { command: params.command || "", + script_content: params.script_content, + script_runner: params.script_runner, cwd: params.cwd, } } async execute(params: ExecuteCommandParams, task: Task, callbacks: ToolCallbacks): Promise { - const { command, cwd: customCwd } = params + const { command, cwd: customCwd, script_content, script_runner } = params const { handleError, pushToolResult, askApproval, removeClosingTag, toolProtocol } = callbacks + const provider = await task.providerRef.deref() + const providerState = await provider?.getState() + const isWindows = process.platform === "win32" + const scriptModeEnabled = isWindows && (providerState?.windowsScriptExecutionEnabled ?? true) + + const trimmedScriptContent = script_content ? unescapeHtmlEntities(script_content).trim() : "" + const trimmedScriptRunner = script_runner ? script_runner.trim() : "" + const hasScriptRequest = trimmedScriptContent.length > 0 + const shouldUseScriptMode = hasScriptRequest && scriptModeEnabled + + let tempScriptPath: string | undefined + let tempScriptDir: string | undefined + let commandToRun = command + try { - if (!command) { + if (hasScriptRequest && !scriptModeEnabled) { + if (!command || !command.trim()) { + pushToolResult( + "Script execution mode is not available on this platform or is disabled. Please send a regular command instead.", + ) + return + } + } + + if (shouldUseScriptMode) { + if (!trimmedScriptRunner) { + task.consecutiveMistakeCount++ + task.recordToolError("execute_command") + pushToolResult(await task.sayAndCreateMissingParamError("execute_command", "script_runner")) + return + } + + // Prevent command injection: runner must be a single token/path without whitespace or shell metacharacters. + // Allow only safe runner tokens: alphanumerics plus common path separators and dots/underscores/hyphens/colons. + // This prevents injection via shell metacharacters while still allowing full paths. + const runnerPattern = /^[\w.\-\\/:\u0080-\uFFFF]+$/ + if (!runnerPattern.test(trimmedScriptRunner)) { + task.recordToolError("execute_command") + pushToolResult( + "script_runner must be a single executable or path without spaces or shell metacharacters.", + ) + return + } + + tempScriptDir = await fs.mkdtemp(path.join(os.tmpdir(), "roo-script-")) + tempScriptPath = path.join(tempScriptDir, `script-${randomUUID()}.tmp`) + await fs.writeFile(tempScriptPath, trimmedScriptContent, "utf-8") + + // Quote path to handle spaces; runner is provided by the model. + commandToRun = `${trimmedScriptRunner} ${JSON.stringify(tempScriptPath)}` + } + + if (!commandToRun || !commandToRun.trim()) { task.consecutiveMistakeCount++ task.recordToolError("execute_command") pushToolResult(await task.sayAndCreateMissingParamError("execute_command", "command")) return } - const ignoredFileAttemptedToAccess = task.rooIgnoreController?.validateCommand(command) + const unescapedCommand = unescapeHtmlEntities(commandToRun) + + const ignoredFileAttemptedToAccess = task.rooIgnoreController?.validateCommand(unescapedCommand) if (ignoredFileAttemptedToAccess) { await task.say("rooignore_error", ignoredFileAttemptedToAccess) @@ -58,17 +117,25 @@ export class ExecuteCommandTool extends BaseTool<"execute_command"> { task.consecutiveMistakeCount = 0 - const unescapedCommand = unescapeHtmlEntities(command) - const didApprove = await askApproval("command", unescapedCommand) + const approvalPreview = shouldUseScriptMode + ? [ + `Runner: ${trimmedScriptRunner}`, + `Temporary script file will be created and removed automatically: ${tempScriptPath}`, + "", + trimmedScriptContent, + ].join("\n") + : unescapedCommand + + const didApprove = await askApproval("command", approvalPreview) if (!didApprove) { + if (tempScriptPath) { + await fs.rm(tempScriptPath).catch(() => {}) + } return } const executionId = task.lastMessageTs?.toString() ?? Date.now().toString() - const provider = await task.providerRef.deref() - const providerState = await provider?.getState() - const { terminalOutputLineLimit = 500, terminalOutputCharacterLimit = DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT, @@ -136,6 +203,12 @@ export class ExecuteCommandTool extends BaseTool<"execute_command"> { } catch (error) { await handleError("executing command", error as Error) return + } finally { + if (tempScriptDir) { + await fs.rm(tempScriptDir, { recursive: true, force: true }).catch(() => {}) + } else if (tempScriptPath) { + await fs.rm(tempScriptPath).catch(() => {}) + } } } @@ -327,6 +400,34 @@ export async function executeCommandInTerminal( // grouping command_output messages despite any gaps anyways). await delay(50) + // If we reached this point without any output or exit details, the command likely + // failed before producing stream events (common with malformed scripts on Windows). + // Surface a failure result so the UI doesn't stay in a pending state. + if (!message && !completed && !exitDetails) { + let terminalSnapshot = "" + try { + // Try to grab whatever the terminal has, to surface any hidden errors. + if (terminal instanceof Terminal) { + terminalSnapshot = await Terminal.getTerminalContents(1) + } + } catch (snapshotError) { + console.warn("[ExecuteCommandTool] Failed to grab terminal contents:", snapshotError) + } + + const workingDirInfo = workingDir ? ` in '${workingDir.toPosix()}'` : "" + const snapshotInfo = terminalSnapshot ? `\n\nTerminal output snapshot:\n${terminalSnapshot}` : "" + + return [ + false, + [ + "Command finished without producing output or exit details.", + `It may have failed immediately${workingDirInfo}.`, + "Please check runner availability, file paths, or script syntax and retry.", + snapshotInfo, + ].join("\n"), + ] + } + if (message) { const { text, images } = message await task.say("user_feedback", text, images) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 3e054ce7d25..9548148b883 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -2258,6 +2258,7 @@ export class ClineProvider terminalZshP10k: stateValues.terminalZshP10k ?? false, terminalZdotdir: stateValues.terminalZdotdir ?? false, terminalCompressProgressBar: stateValues.terminalCompressProgressBar ?? true, + windowsScriptExecutionEnabled: stateValues.windowsScriptExecutionEnabled ?? true, mode: stateValues.mode ?? defaultModeSlug, language: stateValues.language ?? formatLanguage(vscode.env.language), mcpEnabled: stateValues.mcpEnabled ?? true, diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 00b4d6c42d3..431ea90b8c0 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -267,6 +267,7 @@ export type ExtensionState = Pick< | "terminalZshP10k" | "terminalZdotdir" | "terminalCompressProgressBar" + | "windowsScriptExecutionEnabled" | "diagnosticsEnabled" | "diffEnabled" | "fuzzyMatchThreshold" diff --git a/src/shared/tools.ts b/src/shared/tools.ts index 0961c829aa4..ff5c394a1bd 100644 --- a/src/shared/tools.ts +++ b/src/shared/tools.ts @@ -42,6 +42,8 @@ export const toolParamNames = [ "regex", "file_pattern", "recursive", + "script_content", + "script_runner", "action", "url", "coordinate", @@ -86,7 +88,8 @@ export type NativeToolArgs = { access_mcp_resource: { server_name: string; uri: string } read_file: { files: FileEntry[] } attempt_completion: { result: string } - execute_command: { command: string; cwd?: string } + execute_command: { command?: string; cwd?: string; script_content?: string; script_runner?: string } + insert_content: { path: string; line: number; content: string } apply_diff: { path: string; diff: string } search_and_replace: { path: string; operations: Array<{ search: string; replace: string }> } apply_patch: { patch: string } @@ -147,7 +150,7 @@ export interface McpToolUse { export interface ExecuteCommandToolUse extends ToolUse<"execute_command"> { name: "execute_command" // Pick, "command"> makes "command" required, but Partial<> makes it optional - params: Partial, "command" | "cwd">> + params: Partial, "command" | "cwd" | "script_content" | "script_runner">> } export interface ReadFileToolUse extends ToolUse<"read_file"> { diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 6ee163fe41b..e6504aaa990 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -988,12 +988,13 @@ const ChatViewComponent: React.ForwardRefRenderFunction { - if (!isHidden && !sendingDisabled && !enableButtons) { + const hasPendingFollowUp = lastMessage?.type === "ask" && lastMessage.ask === "followup" + if (!isHidden && !sendingDisabled && !enableButtons && !hasPendingFollowUp) { textAreaRef.current?.focus() } }, 50, - [isHidden, sendingDisabled, enableButtons], + [isHidden, sendingDisabled, enableButtons, lastMessage?.ask, lastMessage?.type], ) useEffect(() => { diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 4e0dc638b7f..8ab755d98bf 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -188,6 +188,7 @@ const SettingsView = forwardRef(({ onDone, t terminalZshOhMy, terminalZshP10k, terminalZdotdir, + windowsScriptExecutionEnabled, writeDelayMs, showRooIgnoredFiles, remoteBrowserEnabled, @@ -807,6 +808,7 @@ const SettingsView = forwardRef(({ onDone, t terminalZshP10k={terminalZshP10k} terminalZdotdir={terminalZdotdir} terminalCompressProgressBar={terminalCompressProgressBar} + windowsScriptExecutionEnabled={windowsScriptExecutionEnabled} setCachedStateField={setCachedStateField} /> )} diff --git a/webview-ui/src/components/settings/TerminalSettings.tsx b/webview-ui/src/components/settings/TerminalSettings.tsx index c647344c088..82c3f3d9837 100644 --- a/webview-ui/src/components/settings/TerminalSettings.tsx +++ b/webview-ui/src/components/settings/TerminalSettings.tsx @@ -28,6 +28,7 @@ type TerminalSettingsProps = HTMLAttributes & { terminalZshP10k?: boolean terminalZdotdir?: boolean terminalCompressProgressBar?: boolean + windowsScriptExecutionEnabled?: boolean setCachedStateField: SetCachedStateField< | "terminalOutputLineLimit" | "terminalOutputCharacterLimit" @@ -40,6 +41,7 @@ type TerminalSettingsProps = HTMLAttributes & { | "terminalZshP10k" | "terminalZdotdir" | "terminalCompressProgressBar" + | "windowsScriptExecutionEnabled" > } @@ -55,6 +57,7 @@ export const TerminalSettings = ({ terminalZshP10k, terminalZdotdir, terminalCompressProgressBar, + windowsScriptExecutionEnabled, setCachedStateField, className, ...props @@ -199,6 +202,27 @@ export const TerminalSettings = ({
+
+ + setCachedStateField("windowsScriptExecutionEnabled", e.target.checked) + } + data-testid="terminal-windows-script-mode-checkbox"> + + {t("settings:terminal.windowsScriptExecution.label", { + defaultValue: "Windows: run long commands via temporary script", + })} + + +
+ {t("settings:terminal.windowsScriptExecution.description", { + defaultValue: + "On Windows you can send long commands as script text: Roo will create a temp file, run it with the specified interpreter, and delete it. Disable to always use plain commands.", + })} +
+
+