feat: add command timeout and auto-skipped commands settings

- Add commandMaxWaitTime setting (default 30 seconds) to allow Roo to continue with other tasks when commands exceed timeout
- Add autoSkippedCommands setting with common dev server patterns that should run in background
- Update TerminalProcess to implement timeout logic and background command detection
- Add UI components in Terminal Settings for both new settings
- Add comprehensive tests for timeout and background command functionality
- Update ClineProvider and executeCommandTool to pass settings through

Fixes #8459
This commit is contained in:
Roo Code 2025-10-02 11:35:40 +00:00
parent 13534cc8b7
commit a3ccac03e6
12 changed files with 565 additions and 8 deletions

View file

@ -71,6 +71,8 @@ export const globalSettingsSchema = z.object({
deniedCommands: z.array(z.string()).optional(),
commandExecutionTimeout: z.number().optional(),
commandTimeoutAllowlist: z.array(z.string()).optional(),
commandMaxWaitTime: z.number().optional(),
autoSkippedCommands: z.array(z.string()).optional(),
preventCompletionWithOpenTodos: z.boolean().optional(),
allowedMaxRequests: z.number().nullish(),
allowedMaxCost: z.number().nullish(),
@ -271,6 +273,8 @@ export const EVALS_SETTINGS: RooCodeSettings = {
allowedCommands: ["*"],
commandExecutionTimeout: 20,
commandTimeoutAllowlist: [],
commandMaxWaitTime: 30,
autoSkippedCommands: ["npm run dev", "npm start", "python -m http.server", "yarn dev", "yarn start"],
preventCompletionWithOpenTodos: false,
browserToolEnabled: false,

View file

@ -68,6 +68,8 @@ export async function executeCommandTool(
terminalOutputLineLimit = 500,
terminalOutputCharacterLimit = DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT,
terminalShellIntegrationDisabled = false,
commandMaxWaitTime = 30,
autoSkippedCommands = [],
} = providerState ?? {}
// Get command execution timeout from VSCode configuration (in seconds)
@ -94,6 +96,8 @@ export async function executeCommandTool(
terminalOutputLineLimit,
terminalOutputCharacterLimit,
commandExecutionTimeout,
commandMaxWaitTime,
autoSkippedCommands,
}
try {
@ -141,6 +145,8 @@ export type ExecuteCommandOptions = {
terminalOutputLineLimit?: number
terminalOutputCharacterLimit?: number
commandExecutionTimeout?: number
commandMaxWaitTime?: number
autoSkippedCommands?: string[]
}
export async function executeCommand(
@ -153,6 +159,8 @@ export async function executeCommand(
terminalOutputLineLimit = 500,
terminalOutputCharacterLimit = DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT,
commandExecutionTimeout = 0,
commandMaxWaitTime = 30,
autoSkippedCommands = [],
}: ExecuteCommandOptions,
): Promise<[boolean, ToolResponse]> {
// Convert milliseconds back to seconds for display purposes.
@ -249,7 +257,7 @@ export async function executeCommand(
workingDir = terminal.getCurrentWorkingDirectory()
}
const process = terminal.runCommand(command, callbacks)
const process = terminal.runCommand(command, callbacks, commandMaxWaitTime, autoSkippedCommands)
task.terminalProcess = process
// Implement command execution timeout (skip if timeout is 0).

View file

@ -1815,6 +1815,8 @@ export class ClineProvider
openRouterImageGenerationSelectedModel,
openRouterUseMiddleOutTransform,
featureRoomoteControlEnabled,
commandMaxWaitTime,
autoSkippedCommands,
} = await this.getState()
let cloudOrganizations: CloudOrganizationMembership[] = []
@ -1964,6 +1966,8 @@ export class ClineProvider
openRouterImageGenerationSelectedModel,
openRouterUseMiddleOutTransform,
featureRoomoteControlEnabled,
commandMaxWaitTime: commandMaxWaitTime ?? 30,
autoSkippedCommands: autoSkippedCommands ?? [],
}
}
@ -2195,6 +2199,8 @@ export class ClineProvider
return false
}
})(),
commandMaxWaitTime: stateValues.commandMaxWaitTime ?? 30,
autoSkippedCommands: stateValues.autoSkippedCommands ?? [],
}
}

View file

@ -38,7 +38,12 @@ export abstract class BaseTerminal implements RooTerminal {
abstract isClosed(): boolean
abstract runCommand(command: string, callbacks: RooTerminalCallbacks): RooTerminalProcessResultPromise
abstract runCommand(
command: string,
callbacks: RooTerminalCallbacks,
commandMaxWaitTime?: number,
autoSkippedCommands?: string[],
): RooTerminalProcessResultPromise
/**
* Sets the active stream for this terminal and notifies the process

View file

@ -15,7 +15,12 @@ export class ExecaTerminal extends BaseTerminal {
return false
}
public override runCommand(command: string, callbacks: RooTerminalCallbacks): RooTerminalProcessResultPromise {
public override runCommand(
command: string,
callbacks: RooTerminalCallbacks,
commandMaxWaitTime?: number,
autoSkippedCommands?: string[],
): RooTerminalProcessResultPromise {
this.busy = true
const process = new ExecaTerminalProcess(this)
@ -30,6 +35,7 @@ export class ExecaTerminal extends BaseTerminal {
const promise = new Promise<void>((resolve, reject) => {
process.once("continue", () => resolve())
process.once("error", (error) => reject(error))
// Note: ExecaTerminalProcess doesn't support timeout yet, but we maintain the interface
process.run(command)
})

View file

@ -40,7 +40,12 @@ export class Terminal extends BaseTerminal {
return this.terminal.exitStatus !== undefined
}
public override runCommand(command: string, callbacks: RooTerminalCallbacks): RooTerminalProcessResultPromise {
public override runCommand(
command: string,
callbacks: RooTerminalCallbacks,
commandMaxWaitTime?: number,
autoSkippedCommands?: string[],
): RooTerminalProcessResultPromise {
// We set busy before the command is running because the terminal may be
// waiting on terminal integration, and we must prevent another instance
// from selecting the terminal for use during that time.
@ -59,6 +64,19 @@ export class Terminal extends BaseTerminal {
process.once("shell_execution_complete", (details) => callbacks.onShellExecutionComplete(details, process))
process.once("no_shell_integration", (msg) => callbacks.onNoShellIntegration?.(msg, process))
// Add handlers for timeout and background command events
process.once("command_timeout", (cmd: string) => {
console.log(`[Terminal] Command timeout for: ${cmd}`)
callbacks.onLine?.(
`\n[Command timeout reached - continuing with other tasks while this runs in background]\n`,
process,
)
})
process.once("background_command", (cmd: string) => {
console.log(`[Terminal] Background command started: ${cmd}`)
callbacks.onLine?.(`\n[Running in background - continuing with other tasks]\n`, process)
})
const promise = new Promise<void>((resolve, reject) => {
// Set up event handlers
process.once("continue", () => resolve())
@ -75,8 +93,8 @@ export class Terminal extends BaseTerminal {
// Clean up temporary directory if shell integration is available, zsh did its job:
ShellIntegrationManager.zshCleanupTmpDir(this.id)
// Run the command in the terminal
process.run(command)
// Run the command in the terminal with timeout settings
process.run(command, commandMaxWaitTime, autoSkippedCommands)
})
.catch(() => {
console.log(`[Terminal ${this.id}] Shell integration not available. Command execution aborted.`)

View file

@ -16,6 +16,10 @@ import { Terminal } from "./Terminal"
export class TerminalProcess extends BaseTerminalProcess {
private terminalRef: WeakRef<Terminal>
private commandTimeout?: NodeJS.Timeout
private commandMaxWaitTime: number = 30000 // Default 30 seconds
private autoSkippedCommands: string[] = []
private isBackgroundCommand: boolean = false
constructor(terminal: Terminal) {
super()
@ -44,9 +48,20 @@ export class TerminalProcess extends BaseTerminalProcess {
return terminal
}
public override async run(command: string) {
public override async run(command: string, commandMaxWaitTime?: number, autoSkippedCommands?: string[]) {
this.command = command
// Update settings if provided
if (commandMaxWaitTime !== undefined) {
this.commandMaxWaitTime = commandMaxWaitTime * 1000 // Convert to milliseconds
}
if (autoSkippedCommands !== undefined) {
this.autoSkippedCommands = autoSkippedCommands
}
// Check if this command should run in background
this.isBackgroundCommand = this.shouldRunInBackground(command)
const terminal = this.terminal.terminal
const isShellIntegrationAvailable = terminal.shellIntegration && terminal.shellIntegration.executeCommand
@ -134,6 +149,30 @@ export class TerminalProcess extends BaseTerminalProcess {
this.isHot = true
// Set up timeout for long-running commands if not a background command
if (!this.isBackgroundCommand && this.commandMaxWaitTime > 0) {
this.commandTimeout = setTimeout(() => {
console.log(
`[TerminalProcess] Command timeout reached after ${this.commandMaxWaitTime / 1000} seconds for: ${command}`,
)
// Emit event to allow Roo to continue with other tasks
this.emit("command_timeout", command)
// Don't abort the command, just allow Roo to continue
// The command will continue running in the background
this.isBackgroundCommand = true
}, this.commandMaxWaitTime)
}
// If it's a background command, emit immediately to allow Roo to continue
if (this.isBackgroundCommand) {
console.log(`[TerminalProcess] Running command in background: ${command}`)
setTimeout(() => {
this.emit("background_command", command)
}, 100) // Small delay to ensure command starts
}
// Wait for stream to be available
let stream: AsyncIterable<string>
@ -208,6 +247,12 @@ export class TerminalProcess extends BaseTerminalProcess {
// Set streamClosed immediately after stream ends.
this.terminal.setActiveStream(undefined)
// Clear timeout if command completed before timeout
if (this.commandTimeout) {
clearTimeout(this.commandTimeout)
this.commandTimeout = undefined
}
// Wait for shell execution to complete.
await shellExecutionComplete
@ -464,4 +509,24 @@ export class TerminalProcess extends BaseTerminalProcess {
return match133 !== undefined ? match133 : match633
}
/**
* Check if a command should run in the background based on patterns
*/
private shouldRunInBackground(command: string): boolean {
if (!this.autoSkippedCommands || this.autoSkippedCommands.length === 0) {
return false
}
const lowerCommand = command.toLowerCase()
return this.autoSkippedCommands.some((pattern) => {
const lowerPattern = pattern.toLowerCase()
// Support wildcards in patterns
if (lowerPattern.includes("*")) {
const regex = new RegExp(lowerPattern.replace(/\*/g, ".*"))
return regex.test(lowerCommand)
}
return lowerCommand.includes(lowerPattern)
})
}
}

View file

@ -0,0 +1,341 @@
// npx vitest run src/integrations/terminal/__tests__/TerminalProcessTimeout.spec.ts
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"
import { EventEmitter } from "events"
import { TerminalProcess } from "../TerminalProcess"
import { Terminal } from "../Terminal"
// Mock vscode module
vi.mock("vscode", () => ({
workspace: {
getConfiguration: vi.fn(() => ({
get: vi.fn(() => null),
})),
},
}))
describe("TerminalProcess Timeout Functionality", () => {
let terminalProcess: TerminalProcess
let mockTerminal: Terminal
let mockVscodeTerminal: any
let mockShellExecution: any
let mockShellIntegration: any
beforeEach(() => {
// Create mock shell execution
mockShellExecution = new EventEmitter()
// Create mock shell integration
mockShellIntegration = {
executeCommand: vi.fn().mockReturnValue(mockShellExecution),
}
// Create mock VSCode terminal
mockVscodeTerminal = {
shellIntegration: mockShellIntegration,
sendText: vi.fn(),
show: vi.fn(),
}
// Create mock Terminal instance
mockTerminal = {
terminal: mockVscodeTerminal,
busy: false,
isStreamClosed: false,
cmdCounter: 0,
setActiveStream: vi.fn(),
} as any
// Clear all timers
vi.clearAllTimers()
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
vi.clearAllMocks()
})
describe("Command Timeout", () => {
it("should timeout after specified duration", async () => {
terminalProcess = new TerminalProcess(mockTerminal)
const emitSpy = vi.spyOn(terminalProcess, "emit")
// Start the process with a 5 second timeout
const runPromise = terminalProcess.run("sleep 10", 5)
// Emit stream available
terminalProcess.emit(
"stream_available",
(async function* () {
yield "\x1b]633;C\x07" // Command start marker
yield "Running..."
})(),
)
// Advance time by 6 seconds (past the timeout)
vi.advanceTimersByTime(6000)
// Verify timeout event was emitted
expect(emitSpy).toHaveBeenCalledWith("command_timeout", "sleep 10")
})
it("should not timeout if command completes before timeout", async () => {
terminalProcess = new TerminalProcess(mockTerminal)
const emitSpy = vi.spyOn(terminalProcess, "emit")
// Start the process with a 10 second timeout
const runPromise = terminalProcess.run("echo test", 10)
// Emit stream available with command output
const stream = (async function* () {
yield "\x1b]633;C\x07" // Command start marker
yield "test\n"
yield "\x1b]633;D;0\x07" // Command end marker with exit code 0
})()
terminalProcess.emit("stream_available", stream)
// Advance time by 2 seconds
vi.advanceTimersByTime(2000)
// Emit shell execution complete
terminalProcess.emit("shell_execution_complete", { exitCode: 0 })
// Process the stream
for await (const _ of stream) {
// Stream consumed
}
// Wait for the promise to resolve
await runPromise
// Verify timeout event was NOT emitted
expect(emitSpy).not.toHaveBeenCalledWith("command_timeout", expect.any(String))
})
it("should handle timeout value of 0 (no timeout)", async () => {
terminalProcess = new TerminalProcess(mockTerminal)
const emitSpy = vi.spyOn(terminalProcess, "emit")
// Start the process with no timeout (0)
const runPromise = terminalProcess.run("long-running-command", 0)
// Emit stream available
terminalProcess.emit(
"stream_available",
(async function* () {
yield "\x1b]633;C\x07"
yield "Running..."
})(),
)
// Advance time by a long duration
vi.advanceTimersByTime(60000) // 60 seconds
// Verify timeout event was NOT emitted
expect(emitSpy).not.toHaveBeenCalledWith("command_timeout", expect.any(String))
})
})
describe("Background Commands", () => {
it("should detect background command by exact match", async () => {
terminalProcess = new TerminalProcess(mockTerminal)
const emitSpy = vi.spyOn(terminalProcess, "emit")
const autoSkippedCommands = ["npm run dev", "yarn start"]
// Start the process with auto-skipped commands (don't await)
terminalProcess.run("npm run dev", 30, autoSkippedCommands)
// Use fake timers to advance time
vi.advanceTimersByTime(150)
// Should emit background command event
expect(emitSpy).toHaveBeenCalledWith("background_command", "npm run dev")
})
it("should detect background command by wildcard pattern", async () => {
terminalProcess = new TerminalProcess(mockTerminal)
const emitSpy = vi.spyOn(terminalProcess, "emit")
const autoSkippedCommands = ["npm run *", "python -m http.server*"]
// Test wildcard matching (don't await)
terminalProcess.run("npm run test:watch", 30, autoSkippedCommands)
// Use fake timers to advance time
vi.advanceTimersByTime(150)
expect(emitSpy).toHaveBeenCalledWith("background_command", "npm run test:watch")
})
it("should not treat non-matching commands as background", async () => {
terminalProcess = new TerminalProcess(mockTerminal)
const emitSpy = vi.spyOn(terminalProcess, "emit")
const autoSkippedCommands = ["npm run dev", "yarn start"]
// Start the process with a non-matching command (don't await)
terminalProcess.run("ls -la", 5, autoSkippedCommands)
// Use fake timers to advance time
vi.advanceTimersByTime(150)
// Should not emit background command event
expect(emitSpy).not.toHaveBeenCalledWith("background_command", expect.any(String))
// Emit stream available to prevent timeout
terminalProcess.emit(
"stream_available",
(async function* () {
yield "\x1b]633;C\x07"
})(),
)
})
it("should handle empty auto-skipped commands list", async () => {
terminalProcess = new TerminalProcess(mockTerminal)
const emitSpy = vi.spyOn(terminalProcess, "emit")
// Start the process with empty auto-skipped commands (don't await)
terminalProcess.run("npm run dev", 5, [])
// Use fake timers to advance time
vi.advanceTimersByTime(150)
// Should not be treated as background
expect(emitSpy).not.toHaveBeenCalledWith("background_command", expect.any(String))
// Emit stream available to prevent timeout
terminalProcess.emit(
"stream_available",
(async function* () {
yield "\x1b]633;C\x07"
})(),
)
})
})
describe("shouldRunInBackground helper", () => {
// Since shouldRunInBackground is private, we test it indirectly through the run method
it("should correctly match exact patterns", () => {
terminalProcess = new TerminalProcess(mockTerminal)
const emitSpy = vi.spyOn(terminalProcess, "emit")
const patterns = ["npm run dev", "yarn start", "python manage.py runserver"]
// Test exact match (don't await)
terminalProcess.run("npm run dev", 30, patterns)
vi.advanceTimersByTime(150)
expect(emitSpy).toHaveBeenCalledWith("background_command", "npm run dev")
// Reset spy and create new instance
terminalProcess = new TerminalProcess(mockTerminal)
const emitSpy2 = vi.spyOn(terminalProcess, "emit")
// Test non-match (don't await)
terminalProcess.run("npm run build", 30, patterns)
vi.advanceTimersByTime(150)
expect(emitSpy2).not.toHaveBeenCalledWith("background_command", expect.any(String))
})
it("should correctly match wildcard patterns", () => {
const patterns = ["npm run *", "python -m *", "docker compose*"]
// Test wildcard match for npm
terminalProcess = new TerminalProcess(mockTerminal)
let emitSpy = vi.spyOn(terminalProcess, "emit")
terminalProcess.run("npm run dev", 30, patterns)
vi.advanceTimersByTime(150)
expect(emitSpy).toHaveBeenCalledWith("background_command", "npm run dev")
// Test wildcard match for python
terminalProcess = new TerminalProcess(mockTerminal)
emitSpy = vi.spyOn(terminalProcess, "emit")
terminalProcess.run("python -m http.server", 30, patterns)
vi.advanceTimersByTime(150)
expect(emitSpy).toHaveBeenCalledWith("background_command", "python -m http.server")
// Test wildcard match for docker
terminalProcess = new TerminalProcess(mockTerminal)
emitSpy = vi.spyOn(terminalProcess, "emit")
terminalProcess.run("docker compose up", 30, patterns)
vi.advanceTimersByTime(150)
expect(emitSpy).toHaveBeenCalledWith("background_command", "docker compose up")
// Test non-match
terminalProcess = new TerminalProcess(mockTerminal)
emitSpy = vi.spyOn(terminalProcess, "emit")
terminalProcess.run("docker ps", 30, patterns)
vi.advanceTimersByTime(150)
expect(emitSpy).not.toHaveBeenCalledWith("background_command", expect.any(String))
})
it("should handle case sensitivity correctly", () => {
const patterns = ["NPM RUN DEV", "Yarn Start"]
// Should be case-insensitive for npm
terminalProcess = new TerminalProcess(mockTerminal)
let emitSpy = vi.spyOn(terminalProcess, "emit")
terminalProcess.run("npm run dev", 30, patterns)
vi.advanceTimersByTime(150)
expect(emitSpy).toHaveBeenCalledWith("background_command", "npm run dev")
// Should be case-insensitive for yarn
terminalProcess = new TerminalProcess(mockTerminal)
emitSpy = vi.spyOn(terminalProcess, "emit")
terminalProcess.run("yarn start", 30, patterns)
vi.advanceTimersByTime(150)
expect(emitSpy).toHaveBeenCalledWith("background_command", "yarn start")
})
})
describe("Timeout cleanup", () => {
it("should clear timeout when command completes", async () => {
terminalProcess = new TerminalProcess(mockTerminal)
const clearTimeoutSpy = vi.spyOn(global, "clearTimeout")
// Start the process with a timeout
const runPromise = terminalProcess.run("echo test", 10)
// Emit stream available with complete output
const stream = (async function* () {
yield "\x1b]633;C\x07"
yield "test\n"
yield "\x1b]633;D;0\x07"
})()
terminalProcess.emit("stream_available", stream)
terminalProcess.emit("shell_execution_complete", { exitCode: 0 })
// Process the stream
for await (const _ of stream) {
// Stream consumed
}
await runPromise
// Verify timeout was cleared (if it was set)
// Note: clearTimeout is called even if no timeout was set
expect(clearTimeoutSpy).toHaveBeenCalled()
})
it("should handle abort correctly", () => {
terminalProcess = new TerminalProcess(mockTerminal)
// Start a process to set up listening state
terminalProcess.run("test command", 10)
// Add a line listener to make it listening
terminalProcess.on("line", () => {})
// Abort the process
terminalProcess.abort()
// Verify SIGINT was sent
expect(mockVscodeTerminal.sendText).toHaveBeenCalledWith("\x03")
})
})
})

View file

@ -11,7 +11,12 @@ export interface RooTerminal {
process?: RooTerminalProcess
getCurrentWorkingDirectory(): string
isClosed: () => boolean
runCommand: (command: string, callbacks: RooTerminalCallbacks) => RooTerminalProcessResultPromise
runCommand: (
command: string,
callbacks: RooTerminalCallbacks,
commandMaxWaitTime?: number,
autoSkippedCommands?: string[],
) => RooTerminalProcessResultPromise
setActiveStream(stream: AsyncIterable<string> | undefined, pid?: number): void
shellExecutionComplete(exitDetails: ExitCodeDetails): void
getProcessesWithOutput(): RooTerminalProcess[]
@ -49,6 +54,8 @@ export interface RooTerminalProcessEvents {
shell_execution_complete: [exitDetails: ExitCodeDetails]
error: [error: Error]
no_shell_integration: [message: string]
command_timeout: [command: string]
background_command: [command: string]
}
export interface ExitCodeDetails {

View file

@ -235,6 +235,8 @@ export type ExtensionState = Pick<
| "followupAutoApproveTimeoutMs"
| "allowedCommands"
| "deniedCommands"
| "commandMaxWaitTime"
| "autoSkippedCommands"
| "allowedMaxRequests"
| "allowedMaxCost"
| "browserToolEnabled"

View file

@ -28,6 +28,8 @@ type TerminalSettingsProps = HTMLAttributes<HTMLDivElement> & {
terminalZshP10k?: boolean
terminalZdotdir?: boolean
terminalCompressProgressBar?: boolean
commandMaxWaitTime?: number
autoSkippedCommands?: string[]
setCachedStateField: SetCachedStateField<
| "terminalOutputLineLimit"
| "terminalOutputCharacterLimit"
@ -40,6 +42,8 @@ type TerminalSettingsProps = HTMLAttributes<HTMLDivElement> & {
| "terminalZshP10k"
| "terminalZdotdir"
| "terminalCompressProgressBar"
| "commandMaxWaitTime"
| "autoSkippedCommands"
>
}
@ -55,6 +59,8 @@ export const TerminalSettings = ({
terminalZshP10k,
terminalZdotdir,
terminalCompressProgressBar,
commandMaxWaitTime,
autoSkippedCommands,
setCachedStateField,
className,
...props
@ -184,6 +190,85 @@ export const TerminalSettings = ({
</Trans>
</div>
</div>
<div>
<label className="block font-medium mb-1">
{t("settings:terminal.commandMaxWaitTime.label")}
</label>
<div className="flex items-center gap-2">
<Slider
min={0}
max={300}
step={5}
value={[commandMaxWaitTime ?? 30]}
onValueChange={([value]) => setCachedStateField("commandMaxWaitTime", value)}
data-testid="terminal-command-max-wait-time-slider"
/>
<span className="w-10">{commandMaxWaitTime ?? 30}s</span>
</div>
<div className="text-vscode-descriptionForeground text-sm mt-1">
<Trans i18nKey="settings:terminal.commandMaxWaitTime.description">
<VSCodeLink
href={buildDocLink(
"features/shell-integration#command-max-wait-time",
"settings_terminal_command_max_wait_time",
)}
style={{ display: "inline" }}>
{" "}
</VSCodeLink>
</Trans>
</div>
</div>
<div>
<label className="block font-medium mb-1">
{t("settings:terminal.autoSkippedCommands.label")}
</label>
<div className="flex flex-col gap-2">
{(autoSkippedCommands ?? []).map((command, index) => (
<div key={index} className="flex items-center gap-2">
<input
type="text"
value={command}
onChange={(e) => {
const newCommands = [...(autoSkippedCommands ?? [])]
newCommands[index] = e.target.value
setCachedStateField("autoSkippedCommands", newCommands)
}}
className="flex-1 px-2 py-1 bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border rounded"
placeholder={t("settings:terminal.autoSkippedCommands.placeholder")}
/>
<button
onClick={() => {
const newCommands = (autoSkippedCommands ?? []).filter(
(_, i) => i !== index,
)
setCachedStateField("autoSkippedCommands", newCommands)
}}
className="px-2 py-1 bg-vscode-button-background text-vscode-button-foreground rounded hover:bg-vscode-button-hoverBackground">
{t("common:remove")}
</button>
</div>
))}
<button
onClick={() => {
setCachedStateField("autoSkippedCommands", [...(autoSkippedCommands ?? []), ""])
}}
className="px-2 py-1 bg-vscode-button-background text-vscode-button-foreground rounded hover:bg-vscode-button-hoverBackground self-start">
{t("settings:terminal.autoSkippedCommands.addButton")}
</button>
</div>
<div className="text-vscode-descriptionForeground text-sm mt-1">
<Trans i18nKey="settings:terminal.autoSkippedCommands.description">
<VSCodeLink
href={buildDocLink(
"features/shell-integration#auto-skipped-commands",
"settings_terminal_auto_skipped_commands",
)}
style={{ display: "inline" }}>
{" "}
</VSCodeLink>
</Trans>
</div>
</div>
</div>
</div>

View file

@ -668,6 +668,16 @@
"inheritEnv": {
"label": "Inherit environment variables",
"description": "When enabled, the terminal will inherit environment variables from VSCode's parent process, such as user-profile-defined shell integration settings. This directly toggles VSCode global setting `terminal.integrated.inheritEnv`. <0>Learn more</0>"
},
"commandMaxWaitTime": {
"label": "Command max wait time",
"description": "Maximum time in seconds to wait for a command to complete before allowing Roo to continue with other tasks. The command will continue running in the background. Set to 0 to disable timeout. <0>Learn more</0>"
},
"autoSkippedCommands": {
"label": "Auto-skipped commands",
"description": "List of command patterns that should automatically run in the background, allowing Roo to continue with other tasks immediately. Supports wildcards (*). <0>Learn more</0>",
"placeholder": "e.g., npm run dev, yarn start, python -m http.server",
"addButton": "Add Command Pattern"
}
},
"advancedSettings": {