diff --git a/packages/telemetry/src/TelemetryService.ts b/packages/telemetry/src/TelemetryService.ts index 7a11e3d388..7d59f2a417 100644 --- a/packages/telemetry/src/TelemetryService.ts +++ b/packages/telemetry/src/TelemetryService.ts @@ -152,6 +152,23 @@ export class TelemetryService { this.captureEvent(TelemetryEventName.CONSECUTIVE_MISTAKE_ERROR, { taskId }) } + /** + * Captures when a tool execution times out + * @param taskId The task ID where the timeout occurred + * @param toolName The name of the tool that timed out + * @param timeoutMs The timeout duration in milliseconds + * @param executionTimeMs The actual execution time before timeout + */ + public captureToolTimeout(taskId: string, toolName: string, timeoutMs: number, executionTimeMs: number): void { + this.captureEvent(TelemetryEventName.TOOL_TIMEOUT, { + taskId, + toolName, + timeoutMs, + executionTimeMs, + timeoutRatio: executionTimeMs / timeoutMs, + }) + } + /** * Captures when a tab is shown due to user action * @param tab The tab that was shown diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 514b15d783..e1aceb7fea 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -85,6 +85,10 @@ export const globalSettingsSchema = z.object({ terminalZdotdir: z.boolean().optional(), terminalCompressProgressBar: z.boolean().optional(), + // Timeout settings + toolExecutionTimeoutMs: z.number().min(1000).max(1800000).optional(), // 1s to 30min + timeoutFallbackEnabled: z.boolean().optional(), + rateLimitSeconds: z.number().optional(), diffEnabled: z.boolean().optional(), fuzzyMatchThreshold: z.number().optional(), @@ -227,6 +231,10 @@ export const EVALS_SETTINGS: RooCodeSettings = { enableCheckpoints: false, + // Timeout settings + toolExecutionTimeoutMs: 60000, // 1 minute default + timeoutFallbackEnabled: false, + rateLimitSeconds: 0, maxOpenTabsContext: 20, maxWorkspaceFiles: 200, diff --git a/packages/types/src/message.ts b/packages/types/src/message.ts index 0c87655fc0..12598d71b3 100644 --- a/packages/types/src/message.ts +++ b/packages/types/src/message.ts @@ -80,6 +80,7 @@ export type ClineAsk = z.infer * - `condense_context`: Context condensation/summarization has started * - `condense_context_error`: Error occurred during context condensation * - `codebase_search_result`: Results from searching the codebase + * - `tool_timeout`: Indicates a tool operation has timed out */ export const clineSays = [ "error", @@ -107,6 +108,7 @@ export const clineSays = [ "condense_context_error", "codebase_search_result", "user_edit_todos", + "tool_timeout", ] as const export const clineSaySchema = z.enum(clineSays) diff --git a/packages/types/src/telemetry.ts b/packages/types/src/telemetry.ts index 223c39484c..8b3e092668 100644 --- a/packages/types/src/telemetry.ts +++ b/packages/types/src/telemetry.ts @@ -65,6 +65,7 @@ export enum TelemetryEventName { DIFF_APPLICATION_ERROR = "Diff Application Error", SHELL_INTEGRATION_ERROR = "Shell Integration Error", CONSECUTIVE_MISTAKE_ERROR = "Consecutive Mistake Error", + TOOL_TIMEOUT = "Tool Timeout", CODE_INDEX_ERROR = "Code Index Error", } @@ -167,6 +168,7 @@ export const rooCodeTelemetryEventSchema = z.discriminatedUnion("type", [ TelemetryEventName.TAB_SHOWN, TelemetryEventName.MODE_SETTINGS_CHANGED, TelemetryEventName.CUSTOM_MODE_CREATED, + TelemetryEventName.TOOL_TIMEOUT, ]), properties: telemetryPropertiesSchema, }), diff --git a/src/core/prompts/__tests__/timeout-fallback-responses.spec.ts b/src/core/prompts/__tests__/timeout-fallback-responses.spec.ts new file mode 100644 index 0000000000..3899df3923 --- /dev/null +++ b/src/core/prompts/__tests__/timeout-fallback-responses.spec.ts @@ -0,0 +1,180 @@ +import { describe, it, expect } from "vitest" +import { formatResponse } from "../responses" + +describe("timeout fallback responses", () => { + describe("toolTimeout", () => { + it("should include background operation warning for execute_command", () => { + const message = formatResponse.toolTimeout("execute_command", 30000, 35000) + + expect(message).toContain("timed out after 30 seconds") + expect(message).toContain( + "**Important**: The execute_command operation may still be running in the background", + ) + expect(message).toContain("You might see output or effects from this operation later") + }) + + it("should include background operation warning for browser_action", () => { + const message = formatResponse.toolTimeout("browser_action", 15000, 20000) + + expect(message).toContain("timed out after 15 seconds") + expect(message).toContain( + "**Important**: The browser_action operation may still be running in the background", + ) + expect(message).toContain("You might see output or effects from this operation later") + }) + + it("should not include background operation warning for other tools", () => { + const message = formatResponse.toolTimeout("read_file", 10000, 12000) + + expect(message).toContain("timed out after 10 seconds") + expect(message).not.toContain("may still be running in the background") + expect(message).not.toContain("**Important**") + }) + + it("should format timeout details correctly", () => { + const message = formatResponse.toolTimeout("write_to_file", 5000, 6000) + + expect(message).toContain("") + expect(message).toContain("Tool: write_to_file") + expect(message).toContain("Configured Timeout: 5s") + expect(message).toContain("Execution Time: 6s") + expect(message).toContain("Status: Canceled") + expect(message).toContain("") + }) + }) + + describe("generateContextualSuggestions", () => { + it("should generate execute_command suggestions", () => { + const suggestions = formatResponse.timeoutFallbackSuggestions.generateContextualSuggestions( + "execute_command", + { command: "npm install" }, + ) + + expect(suggestions).toHaveLength(4) + expect(suggestions[0].text).toContain("npm install") + expect(suggestions[0].text).toContain("still running in the background") + expect(suggestions[1].text).toContain("smaller, sequential steps") + }) + + it("should generate read_file suggestions", () => { + const suggestions = formatResponse.timeoutFallbackSuggestions.generateContextualSuggestions("read_file", { + path: "/large/file.txt", + }) + + expect(suggestions).toHaveLength(4) + expect(suggestions[0].text).toContain("/large/file.txt") + expect(suggestions[0].text).toContain("smaller chunks") + }) + + it("should generate write_to_file suggestions", () => { + const suggestions = formatResponse.timeoutFallbackSuggestions.generateContextualSuggestions( + "write_to_file", + { path: "/output/file.js" }, + ) + + expect(suggestions).toHaveLength(4) + expect(suggestions[0].text).toContain("/output/file.js") + expect(suggestions[0].text).toContain("insert_content") + }) + + it("should generate browser_action suggestions", () => { + const suggestions = formatResponse.timeoutFallbackSuggestions.generateContextualSuggestions( + "browser_action", + { action: "click" }, + ) + + expect(suggestions).toHaveLength(4) + expect(suggestions[0].text).toContain("browser action") + expect(suggestions[0].text).toContain("still processing in the background") + expect(suggestions[1].text).toContain("smaller, more targeted steps") + }) + + it("should generate search_files suggestions", () => { + const suggestions = formatResponse.timeoutFallbackSuggestions.generateContextualSuggestions( + "search_files", + { regex: "complex.*pattern" }, + ) + + expect(suggestions).toHaveLength(4) + expect(suggestions[0].text).toContain("Narrow the search scope") + }) + + it("should generate generic suggestions for unknown tools", () => { + const suggestions = formatResponse.timeoutFallbackSuggestions.generateContextualSuggestions( + "unknown_tool" as any, + ) + + expect(suggestions).toHaveLength(4) + expect(suggestions[0].text).toContain("unknown_tool operation") + expect(suggestions[0].text).toContain("smaller steps") + }) + }) + + describe("individual suggestion generators", () => { + it("should generate command suggestions with default command name", () => { + const suggestions = formatResponse.timeoutFallbackSuggestions.generateCommandSuggestions() + + expect(suggestions).toHaveLength(4) + expect(suggestions[0].text).toContain("the command") + expect(suggestions[0].text).toContain("still running in the background") + }) + + it("should generate read file suggestions with default file name", () => { + const suggestions = formatResponse.timeoutFallbackSuggestions.generateReadFileSuggestions() + + expect(suggestions).toHaveLength(4) + expect(suggestions[0].text).toContain("the file") + }) + + it("should generate write file suggestions with default file name", () => { + const suggestions = formatResponse.timeoutFallbackSuggestions.generateWriteFileSuggestions() + + expect(suggestions).toHaveLength(4) + expect(suggestions[0].text).toContain("the file") + }) + + it("should generate browser suggestions with default action name", () => { + const suggestions = formatResponse.timeoutFallbackSuggestions.generateBrowserSuggestions() + + expect(suggestions).toHaveLength(4) + expect(suggestions[0].text).toContain("browser action") + expect(suggestions[0].text).toContain("still processing in the background") + }) + + it("should generate search suggestions", () => { + const suggestions = formatResponse.timeoutFallbackSuggestions.generateSearchSuggestions() + + expect(suggestions).toHaveLength(4) + expect(suggestions[0].text).toContain("Narrow the search scope") + }) + + it("should generate generic suggestions", () => { + const suggestions = formatResponse.timeoutFallbackSuggestions.generateGenericSuggestions("new_task") + + expect(suggestions).toHaveLength(4) + expect(suggestions[0].text).toContain("new_task operation") + }) + }) + + describe("suggestion structure", () => { + it("should return suggestions with text property", () => { + const suggestions = formatResponse.timeoutFallbackSuggestions.generateGenericSuggestions("new_task") + + suggestions.forEach((suggestion) => { + expect(suggestion).toHaveProperty("text") + expect(typeof suggestion.text).toBe("string") + expect(suggestion.text.length).toBeGreaterThan(0) + }) + }) + + it("should optionally include mode property", () => { + const suggestions = formatResponse.timeoutFallbackSuggestions.generateGenericSuggestions("new_task") + + suggestions.forEach((suggestion) => { + if (suggestion.mode) { + expect(typeof suggestion.mode).toBe("string") + } + }) + }) + }) +}) diff --git a/src/core/prompts/instructions/__tests__/timeout-fallback.spec.ts b/src/core/prompts/instructions/__tests__/timeout-fallback.spec.ts new file mode 100644 index 0000000000..4e941d1208 --- /dev/null +++ b/src/core/prompts/instructions/__tests__/timeout-fallback.spec.ts @@ -0,0 +1,134 @@ +import { describe, it, expect } from "vitest" +import { + createTimeoutFallbackPrompt, + parseTimeoutFallbackResponse, + type TimeoutFallbackContext, +} from "../timeout-fallback" + +describe("timeout-fallback", () => { + describe("createTimeoutFallbackPrompt", () => { + it("should create a basic prompt with required context", () => { + const context: TimeoutFallbackContext = { + toolName: "execute_command", + timeoutMs: 30000, + executionTimeMs: 32000, + } + + const prompt = createTimeoutFallbackPrompt(context) + + expect(prompt).toContain("execute_command operation has timed out after 30 seconds") + expect(prompt).toContain("actual execution time: 32 seconds") + expect(prompt).toContain("Tool: execute_command") + expect(prompt).toContain("Generate exactly 3-4 specific, actionable suggestions") + }) + + it("should include tool parameters when provided", () => { + const context: TimeoutFallbackContext = { + toolName: "read_file", + timeoutMs: 15000, + executionTimeMs: 16000, + toolParams: { + path: "/large/file.txt", + line_range: "1-10000", + }, + } + + const prompt = createTimeoutFallbackPrompt(context) + + expect(prompt).toContain("Parameters:") + expect(prompt).toContain("/large/file.txt") + expect(prompt).toContain("1-10000") + }) + + it("should include task context when provided", () => { + const context: TimeoutFallbackContext = { + toolName: "write_to_file", + timeoutMs: 20000, + executionTimeMs: 22000, + taskContext: { + currentStep: "Creating configuration file", + workingDirectory: "/project/config", + }, + } + + const prompt = createTimeoutFallbackPrompt(context) + + expect(prompt).toContain("Current step: Creating configuration file") + expect(prompt).toContain("Working directory: /project/config") + }) + }) + + describe("parseTimeoutFallbackResponse", () => { + it("should parse numbered list responses", () => { + const response = `Here are the suggestions: +1. Break the command into smaller parts +2. Use background execution with nohup +3. Try an alternative approach +4. Increase the timeout setting` + + const suggestions = parseTimeoutFallbackResponse(response) + + expect(suggestions).toHaveLength(4) + expect(suggestions[0].text).toBe("Break the command into smaller parts") + expect(suggestions[1].text).toBe("Use background execution with nohup") + expect(suggestions[2].text).toBe("Try an alternative approach") + expect(suggestions[3].text).toBe("Increase the timeout setting") + }) + + it("should parse numbered list with parentheses", () => { + const response = `Suggestions: +1) Check file permissions +2) Use smaller chunks +3) Try a different tool` + + const suggestions = parseTimeoutFallbackResponse(response) + + expect(suggestions).toHaveLength(3) + expect(suggestions[0].text).toBe("Check file permissions") + expect(suggestions[1].text).toBe("Use smaller chunks") + expect(suggestions[2].text).toBe("Try a different tool") + }) + + it("should fallback to sentence parsing when no numbered list found", () => { + const response = `You should try breaking the operation into smaller parts. Consider using an alternative approach. Check system resources and try again.` + + const suggestions = parseTimeoutFallbackResponse(response) + + expect(suggestions.length).toBeGreaterThan(0) + expect(suggestions[0].text).toBe("You should try breaking the operation into smaller parts") + }) + + it("should limit suggestions to 4 items", () => { + const response = `1. First suggestion +2. Second suggestion +3. Third suggestion +4. Fourth suggestion +5. Fifth suggestion +6. Sixth suggestion` + + const suggestions = parseTimeoutFallbackResponse(response) + + expect(suggestions).toHaveLength(4) + }) + + it("should filter out suggestions that are too long", () => { + const response = `1. Good suggestion +2. This is a very long suggestion that exceeds the maximum character limit and should be filtered out because it's too verbose +3. Another good suggestion` + + const suggestions = parseTimeoutFallbackResponse(response) + + expect(suggestions).toHaveLength(2) + expect(suggestions[0].text).toBe("Good suggestion") + expect(suggestions[1].text).toBe("Another good suggestion") + }) + + it("should return empty array for invalid responses", () => { + const response = "" + + const suggestions = parseTimeoutFallbackResponse(response) + + expect(suggestions).toHaveLength(0) + }) + }) +}) diff --git a/src/core/prompts/instructions/timeout-fallback.ts b/src/core/prompts/instructions/timeout-fallback.ts new file mode 100644 index 0000000000..a77b1e8a9a --- /dev/null +++ b/src/core/prompts/instructions/timeout-fallback.ts @@ -0,0 +1,106 @@ +import type { ToolName } from "@roo-code/types" + +export interface TimeoutFallbackContext { + toolName: ToolName + timeoutMs: number + executionTimeMs: number + toolParams?: Record + errorMessage?: string + taskContext?: { + currentStep?: string + previousActions?: string[] + workingDirectory?: string + } +} + +/** + * Create a prompt for the AI to generate contextual timeout fallback suggestions + */ +export function createTimeoutFallbackPrompt(context: TimeoutFallbackContext): string { + const { toolName, timeoutMs, executionTimeMs, toolParams, taskContext } = context + + const timeoutSeconds = Math.round(timeoutMs / 1000) + const executionSeconds = Math.round(executionTimeMs / 1000) + + let prompt = `A ${toolName} operation has timed out after ${timeoutSeconds} seconds (actual execution time: ${executionSeconds} seconds). + +Context: +- Tool: ${toolName} +- Timeout limit: ${timeoutSeconds}s +- Actual execution time: ${executionSeconds}s` + + // Add tool-specific context + if (toolParams) { + prompt += `\n- Parameters: ${JSON.stringify(toolParams, null, 2)}` + } + + // Add task context if available + if (taskContext) { + if (taskContext.currentStep) { + prompt += `\n- Current step: ${taskContext.currentStep}` + } + if (taskContext.workingDirectory) { + prompt += `\n- Working directory: ${taskContext.workingDirectory}` + } + } + + prompt += ` + +Generate exactly 3-4 specific, actionable suggestions for how to proceed after this timeout. Each suggestion should be: +1. Contextually relevant to the specific ${toolName} operation that timed out +2. Actionable and specific (not generic advice) +3. Focused on solving the immediate problem +4. Ordered by likelihood of success + +Format your response as a simple numbered list: +1. [First suggestion] +2. [Second suggestion] +3. [Third suggestion] +4. [Fourth suggestion (optional)] + +Focus on practical solutions like: +- Breaking the operation into smaller parts +- Using alternative tools or methods +- Adjusting parameters or settings +- Checking for underlying issues +- Optimizing the approach + +Keep each suggestion concise (under 80 characters) and actionable.` + + return prompt +} + +/** + * Parse AI response to extract suggestions + */ +export function parseTimeoutFallbackResponse(response: string): Array<{ text: string; mode?: string }> { + const suggestions: Array<{ text: string; mode?: string }> = [] + + // Look for numbered list items + const lines = response.split("\n") + for (const line of lines) { + const trimmed = line.trim() + + // Match patterns like "1. suggestion", "2) suggestion", etc. + const match = trimmed.match(/^(\d+)[.)]\s*(.+)$/) + if (match && match[2]) { + const suggestionText = match[2].trim() + if (suggestionText.length > 0 && suggestionText.length <= 120) { + suggestions.push({ text: suggestionText }) + } + } + } + + // If no numbered list found, try to extract sentences + if (suggestions.length === 0) { + const sentences = response + .split(/[.!?]+/) + .map((s) => s.trim()) + .filter((s) => s.length > 10 && s.length <= 120) + for (let i = 0; i < Math.min(4, sentences.length); i++) { + suggestions.push({ text: sentences[i] }) + } + } + + return suggestions.slice(0, 4) // Limit to 4 suggestions +} diff --git a/src/core/prompts/responses.ts b/src/core/prompts/responses.ts index 3f38789fdc..37282e13c1 100644 --- a/src/core/prompts/responses.ts +++ b/src/core/prompts/responses.ts @@ -3,6 +3,7 @@ import * as path from "path" import * as diff from "diff" import { RooIgnoreController, LOCK_TEXT_SYMBOL } from "../ignore/RooIgnoreController" import { RooProtectedController } from "../protect/RooProtectedController" +import type { ToolName } from "@roo-code/types" export const formatResponse = { toolDenied: () => `The user denied this operation.`, @@ -15,6 +16,27 @@ export const formatResponse = { toolError: (error?: string) => `The tool execution failed with the following error:\n\n${error}\n`, + toolTimeout: (toolName: string, timeoutMs: number, executionTimeMs: number) => { + // Determine if this tool might continue running in the background + const backgroundOperationTools = ["execute_command", "browser_action"] + const mightContinueInBackground = backgroundOperationTools.includes(toolName) + + const backgroundWarning = mightContinueInBackground + ? `\n\n**Important**: The ${toolName} operation may still be running in the background. You might see output or effects from this operation later.` + : "" + + return `The ${toolName} operation timed out after ${Math.round(timeoutMs / 1000)} seconds and was automatically canceled. + + +Tool: ${toolName} +Configured Timeout: ${Math.round(timeoutMs / 1000)}s +Execution Time: ${Math.round(executionTimeMs / 1000)}s +Status: Canceled +${backgroundWarning} + +The operation has been terminated to prevent system resource issues. Please consider one of the following approaches to complete your task.` + }, + rooIgnoreError: (path: string) => `Access to ${path} is blocked by the .rooignore file settings. You must try to continue in the task without using this file, or ask the user to update the .rooignore file.`, @@ -173,6 +195,141 @@ Otherwise, if you have not completed the task and do not need additional informa const prettyPatchLines = lines.slice(4) return prettyPatchLines.join("\n") }, + + /** + * Generate contextual timeout fallback suggestions based on tool type and parameters + */ + timeoutFallbackSuggestions: { + generateContextualSuggestions: ( + toolName: ToolName, + toolParams?: Record, + ): Array<{ text: string; mode?: string }> => { + switch (toolName) { + case "execute_command": + return formatResponse.timeoutFallbackSuggestions.generateCommandSuggestions(toolParams) + case "read_file": + return formatResponse.timeoutFallbackSuggestions.generateReadFileSuggestions(toolParams) + case "write_to_file": + return formatResponse.timeoutFallbackSuggestions.generateWriteFileSuggestions(toolParams) + case "browser_action": + return formatResponse.timeoutFallbackSuggestions.generateBrowserSuggestions(toolParams) + case "search_files": + return formatResponse.timeoutFallbackSuggestions.generateSearchSuggestions(toolParams) + default: + return formatResponse.timeoutFallbackSuggestions.generateGenericSuggestions(toolName) + } + }, + + generateCommandSuggestions: (params?: Record): Array<{ text: string; mode?: string }> => { + const command = params?.command || "the command" + + return [ + { + text: `Check if "${command}" is still running in the background and wait for it to complete`, + }, + { + text: `Break "${command}" into smaller, sequential steps that can complete faster`, + }, + { + text: `Run "${command}" in the background using '&' or 'nohup' to avoid blocking`, + }, + { + text: `Try an alternative approach or tool to accomplish the same goal`, + }, + ] + }, + + generateReadFileSuggestions: (params?: Record): Array<{ text: string; mode?: string }> => { + const filePath = params?.path || "the file" + + return [ + { + text: `Read "${filePath}" in smaller chunks using line ranges`, + }, + { + text: `Check if "${filePath}" is accessible and not locked by another process`, + }, + { + text: `Use a different approach to access the file content`, + }, + { + text: `Increase the timeout if this is a legitimately large file`, + }, + ] + }, + + generateWriteFileSuggestions: (params?: Record): Array<{ text: string; mode?: string }> => { + const filePath = params?.path || "the file" + + return [ + { + text: `Write to "${filePath}" incrementally using insert_content instead`, + }, + { + text: `Check if "${filePath}" is writable and not locked`, + }, + { + text: `Use apply_diff for targeted changes instead of full file replacement`, + }, + { + text: `Break the content into smaller write operations`, + }, + ] + }, + + generateBrowserSuggestions: (params?: Record): Array<{ text: string; mode?: string }> => { + const action = params?.action || "browser action" + + return [ + { + text: `Check if the browser action is still processing in the background`, + }, + { + text: `Simplify the "${action}" into smaller, more targeted steps`, + }, + { + text: `Wait for specific elements to load before proceeding`, + }, + { + text: `Reset the browser session and try again`, + }, + ] + }, + + generateSearchSuggestions: (params?: Record): Array<{ text: string; mode?: string }> => { + return [ + { + text: `Narrow the search scope to specific directories`, + }, + { + text: `Use simpler search patterns or literal strings`, + }, + { + text: `Apply file type filters to reduce search space`, + }, + { + text: `Search incrementally in smaller batches`, + }, + ] + }, + + generateGenericSuggestions: (toolName: ToolName): Array<{ text: string; mode?: string }> => { + return [ + { + text: `Break the ${toolName} operation into smaller steps`, + }, + { + text: `Try an alternative approach to accomplish the same goal`, + }, + { + text: `Check system resources and try again`, + }, + { + text: `Increase the timeout setting for this operation`, + }, + ] + }, + }, } // to avoid circular dependency diff --git a/src/core/timeout/TimeoutFallbackHandler.ts b/src/core/timeout/TimeoutFallbackHandler.ts new file mode 100644 index 0000000000..6c9e09dc97 --- /dev/null +++ b/src/core/timeout/TimeoutFallbackHandler.ts @@ -0,0 +1,223 @@ +import type { ToolName } from "@roo-code/types" +import type { Task } from "../task/Task" +import type { SingleCompletionHandler } from "../../api" +import { + createTimeoutFallbackPrompt, + parseTimeoutFallbackResponse, + type TimeoutFallbackContext, +} from "../prompts/instructions/timeout-fallback" +import { formatResponse } from "../prompts/responses" + +export interface TimeoutFallbackResult { + success: boolean + toolCall?: { + name: "ask_followup_question" + params: { + question: string + follow_up: string + } + } + error?: string +} + +/** + * Unified timeout fallback handler that generates AI-powered fallback suggestions + * and creates timeout responses in a single optimized flow + */ +export class TimeoutFallbackHandler { + private static readonly BACKGROUND_OPERATION_TOOLS = ["execute_command", "browser_action"] + + /** + * Helper method to generate the timeout question message + */ + private static generateTimeoutQuestion(toolName: ToolName, timeoutMs: number): string { + const mightContinueInBackground = this.BACKGROUND_OPERATION_TOOLS.includes(toolName) + const baseMessage = `The ${toolName} operation timed out after ${Math.round(timeoutMs / 1000)} seconds` + const backgroundSuffix = mightContinueInBackground ? " but may still be running in the background" : "" + return `${baseMessage}${backgroundSuffix}. How would you like to proceed?` + } + + /** + * Create a timeout response with AI-generated fallback question in a single optimized query + */ + public static async createTimeoutResponse( + toolName: ToolName, + timeoutMs: number, + executionTimeMs: number, + context?: any, + task?: Task, + ): Promise { + const baseResponse = formatResponse.toolTimeout(toolName, timeoutMs, executionTimeMs) + + // Create a timeout message for display in the chat + if (task) { + const mightContinueInBackground = this.BACKGROUND_OPERATION_TOOLS.includes(toolName) + + // Pass a JSON string with the tool info so the UI can determine if it should show a background warning + const timeoutInfo = JSON.stringify({ toolName, mightContinueInBackground }) + await task.say("tool_timeout", timeoutInfo, undefined, false, undefined, undefined, { + isNonInteractive: true, + }) + } + + // Create context for AI fallback generation + const aiContext: TimeoutFallbackContext = { + toolName, + timeoutMs, + executionTimeMs, + toolParams: context, + taskContext: task + ? { + workingDirectory: task.cwd, + } + : undefined, + } + + // Generate AI-powered fallback (with static fallback if AI fails) in a single call + const aiResult = await this.generateAiFallback(aiContext, task) + + if (aiResult.success && aiResult.toolCall) { + // Return a response that instructs the model to ask a follow-up question + const { question, follow_up } = aiResult.toolCall.params + + // Format the response to explicitly instruct the model to ask the follow-up question + return `${baseResponse} + +The operation timed out. You MUST now use the ask_followup_question tool with the following parameters: + + +${question} + +${follow_up} + + + +This is required to help the user decide how to proceed after the timeout.` + } + + // This should rarely happen since generateAiFallback always provides static fallback + return `${baseResponse}\n\nThe operation timed out. Please consider breaking this into smaller steps or trying a different approach.` + } + + /** + * Generate an AI-powered ask_followup_question tool call for timeout scenarios + */ + public static async generateAiFallback( + context: TimeoutFallbackContext, + task?: Task, + ): Promise { + // Try to use AI to generate contextual suggestions + if (task?.api && "completePrompt" in task.api) { + try { + // Pass the timeout from context to use exact tool timeout + const aiResult = await this.generateAiSuggestions( + context, + task.api as SingleCompletionHandler, + context.timeoutMs, + ) + if (aiResult.success) { + console.log(`[TimeoutFallbackHandler] AI suggestions generated successfully`) + return aiResult + } + console.error(`[TimeoutFallbackHandler] AI suggestions failed:`, aiResult.error) + } catch (error) { + // AI failed, fall through to static suggestions + } + } + + // Fallback to static suggestions if AI fails or is unavailable + const toolCall = this.generateStaticToolCall(context) + + return { + success: true, + toolCall, + } + } + + /** + * Generate AI-powered suggestions using the task's API handler + */ + private static async generateAiSuggestions( + context: TimeoutFallbackContext, + apiHandler: SingleCompletionHandler, + timeoutMs: number, + ): Promise { + try { + const prompt = createTimeoutFallbackPrompt(context) + + // Create a timeout promise using exactly the same timeout as the tool + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => { + reject(new Error(`AI fallback generation timed out after ${timeoutMs / 1000} seconds`)) + }, timeoutMs) + }) + + // Race between AI completion and timeout + const aiResponse = await Promise.race([apiHandler.completePrompt(prompt), timeoutPromise]) + + // Parse the AI response to extract suggestions + const suggestions = parseTimeoutFallbackResponse(aiResponse) + + if (suggestions.length === 0) { + throw new Error("No valid suggestions generated by AI") + } + + const question = this.generateTimeoutQuestion(context.toolName, context.timeoutMs) + + const followUpXml = suggestions + .map((suggestion) => + suggestion.mode + ? `${suggestion.text}` + : `${suggestion.text}`, + ) + .join("\n") + + return { + success: true, + toolCall: { + name: "ask_followup_question", + params: { + question, + follow_up: followUpXml, + }, + }, + } + } catch (error) { + // If it's a timeout error, include that in the error message + const errorMessage = error instanceof Error ? error.message : "Unknown error generating AI suggestions" + + return { + success: false, + error: errorMessage, + } + } + } + + /** + * Generate static fallback suggestions when AI is unavailable + */ + private static generateStaticToolCall(context: TimeoutFallbackContext): TimeoutFallbackResult["toolCall"] { + const suggestions = formatResponse.timeoutFallbackSuggestions.generateContextualSuggestions( + context.toolName, + context.toolParams, + ) + + const question = this.generateTimeoutQuestion(context.toolName, context.timeoutMs) + + const followUpXml = suggestions + .map((suggestion) => + suggestion.mode + ? `${suggestion.text}` + : `${suggestion.text}`, + ) + .join("\n") + + return { + name: "ask_followup_question", + params: { + question, + follow_up: followUpXml, + }, + } + } +} diff --git a/src/core/timeout/TimeoutManager.ts b/src/core/timeout/TimeoutManager.ts new file mode 100644 index 0000000000..fe1ae3c458 --- /dev/null +++ b/src/core/timeout/TimeoutManager.ts @@ -0,0 +1,241 @@ +import { EventEmitter } from "events" +import type { ToolName } from "@roo-code/types" +import { TelemetryService } from "@roo-code/telemetry" + +export interface TimeoutConfig { + toolName: ToolName + timeoutMs: number + enableFallback: boolean + taskId?: string +} + +export interface TimeoutResult { + success: boolean + result?: T + timedOut: boolean + fallbackTriggered: boolean + error?: Error + executionTimeMs: number +} + +export interface TimeoutEvent { + toolName: ToolName + timeoutMs: number + executionTimeMs: number + taskId?: string + timestamp: number +} + +/** + * Manages timeouts for all tool executions with configurable fallback mechanisms + */ +export class TimeoutManager extends EventEmitter { + private static instance: TimeoutManager | undefined + private activeOperations = new Map() + /** + * Assumes there is only one active tool-- does not timeout edge cases + * like "Proceed While Running" + */ + private lastTimeoutEvent: TimeoutEvent | null = null + private logger: (...args: any[]) => void + + private constructor(logger?: (...args: any[]) => void) { + super() + this.logger = logger || console.log + } + + public static getInstance(logger?: (...args: any[]) => void): TimeoutManager { + if (!TimeoutManager.instance) { + TimeoutManager.instance = new TimeoutManager(logger) + } + return TimeoutManager.instance + } + + /** + * Execute a function with timeout protection + */ + public async executeWithTimeout( + operation: (signal: AbortSignal) => Promise, + config: TimeoutConfig, + ): Promise> { + const operationId = this.generateOperationId(config.toolName, config.taskId) + const controller = new AbortController() + const startTime = Date.now() + + this.logger(`[TimeoutManager] Starting operation ${operationId} with timeout ${config.timeoutMs}ms`) + + // Store the controller for potential cancellation + this.activeOperations.set(operationId, controller) + + try { + // Create timeout promise + const timeoutPromise = new Promise((_, reject) => { + const timeoutId = setTimeout(() => { + controller.abort() + reject(new Error(`Operation timed out after ${config.timeoutMs}ms`)) + }, config.timeoutMs) + + // Clean up timeout if operation completes + controller.signal.addEventListener("abort", () => { + clearTimeout(timeoutId) + }) + }) + + // Race between operation and timeout + const result = await Promise.race([operation(controller.signal), timeoutPromise]) + + const executionTimeMs = Date.now() - startTime + + this.logger(`[TimeoutManager] Operation ${operationId} completed successfully in ${executionTimeMs}ms`) + + return { + success: true, + result, + timedOut: false, + fallbackTriggered: false, + executionTimeMs, + } + } catch (error) { + const executionTimeMs = Date.now() - startTime + const timedOut = controller.signal.aborted + + if (timedOut) { + // Store the last timeout event + const timeoutEvent: TimeoutEvent = { + toolName: config.toolName, + timeoutMs: config.timeoutMs, + executionTimeMs, + taskId: config.taskId, + timestamp: Date.now(), + } + + this.lastTimeoutEvent = timeoutEvent + this.emit("timeout", timeoutEvent) + + // Log timeout event + this.logger( + `[TimeoutManager] Operation ${operationId} timed out after ${executionTimeMs}ms (limit: ${config.timeoutMs}ms)`, + ) + + // Capture telemetry if TelemetryService is available + if (TelemetryService.hasInstance() && config.taskId) { + TelemetryService.instance.captureToolTimeout( + config.taskId, + config.toolName, + config.timeoutMs, + executionTimeMs, + ) + } + + return { + success: false, + timedOut: true, + fallbackTriggered: config.enableFallback, + error: error as Error, + executionTimeMs, + } + } + + // Log non-timeout errors + this.logger( + `[TimeoutManager] Operation ${operationId} failed after ${executionTimeMs}ms: ${error instanceof Error ? error.message : String(error)}`, + ) + + return { + success: false, + timedOut: false, + fallbackTriggered: false, + error: error as Error, + executionTimeMs, + } + } finally { + // Clean up + this.activeOperations.delete(operationId) + } + } + + /** + * Cancel a specific operation by tool name and task ID + */ + public cancelOperation(toolName: ToolName, taskId?: string): boolean { + const operationId = this.generateOperationId(toolName, taskId) + const controller = this.activeOperations.get(operationId) + + if (controller) { + controller.abort() + this.activeOperations.delete(operationId) + return true + } + + return false + } + + /** + * Cancel all active operations + */ + public cancelAllOperations(): void { + for (const controller of this.activeOperations.values()) { + controller.abort() + } + this.activeOperations.clear() + } + + /** + * Get the last timeout event + */ + public getLastTimeoutEvent(): TimeoutEvent | null { + return this.lastTimeoutEvent + } + + /** + * Clear the last timeout event + */ + public clearLastTimeoutEvent(): void { + this.lastTimeoutEvent = null + } + + /** + * Get active operation count + */ + public getActiveOperationCount(): number { + return this.activeOperations.size + } + + /** + * Check if a specific operation is active + */ + public isOperationActive(toolName: ToolName, taskId?: string): boolean { + const operationId = this.generateOperationId(toolName, taskId) + return this.activeOperations.has(operationId) + } + + private generateOperationId(toolName: ToolName, taskId?: string): string { + return `${toolName}:${taskId || "default"}` + } + + /** + * Get timeout statistics for monitoring + */ + public getTimeoutStats(): { + lastTimeout: TimeoutEvent | null + activeOperations: number + operationIds: string[] + } { + return { + lastTimeout: this.lastTimeoutEvent, + activeOperations: this.activeOperations.size, + operationIds: Array.from(this.activeOperations.keys()), + } + } + + /** + * Cleanup method for graceful shutdown + */ + public dispose(): void { + this.cancelAllOperations() + this.removeAllListeners() + this.lastTimeoutEvent = null + } +} + +export const timeoutManager = TimeoutManager.getInstance() diff --git a/src/core/timeout/ToolExecutionWrapper.ts b/src/core/timeout/ToolExecutionWrapper.ts new file mode 100644 index 0000000000..50ac9c765f --- /dev/null +++ b/src/core/timeout/ToolExecutionWrapper.ts @@ -0,0 +1,32 @@ +import type { ToolName } from "@roo-code/types" +import { timeoutManager, type TimeoutConfig, type TimeoutResult } from "./TimeoutManager" + +export interface ToolExecutionOptions { + toolName: ToolName + taskId?: string + timeoutMs?: number + enableFallback?: boolean +} + +/** + * Wrapper for executing tools with timeout protection + */ +export class ToolExecutionWrapper { + /** + * Execute a tool operation with timeout protection + */ + public static async execute( + operation: (signal: AbortSignal) => Promise, + options: ToolExecutionOptions, + defaultTimeoutMs = 60000, // 1 minute default + ): Promise> { + const config: TimeoutConfig = { + toolName: options.toolName, + timeoutMs: options.timeoutMs ?? defaultTimeoutMs, + enableFallback: options.enableFallback ?? true, + taskId: options.taskId, + } + + return timeoutManager.executeWithTimeout(operation, config) + } +} diff --git a/src/core/timeout/__tests__/timeout-fallback.spec.ts b/src/core/timeout/__tests__/timeout-fallback.spec.ts new file mode 100644 index 0000000000..ee98890f2a --- /dev/null +++ b/src/core/timeout/__tests__/timeout-fallback.spec.ts @@ -0,0 +1,598 @@ +// npx vitest run src/core/timeout/__tests__/timeout-fallback.spec.ts + +import { describe, test, expect, beforeEach, vitest } from "vitest" +import { TimeoutFallbackHandler } from "../TimeoutFallbackHandler" +import type { TimeoutFallbackResult } from "../TimeoutFallbackHandler" +import type { ApiHandler, SingleCompletionHandler } from "../../../api" +import type { Task } from "../../task/Task" + +// Create a mock API handler that extends ApiHandler and includes completePrompt +interface MockApiHandler extends ApiHandler, SingleCompletionHandler {} + +describe("TimeoutFallbackHandler", () => { + let mockApiHandler: MockApiHandler + let mockTask: Partial + + beforeEach(() => { + vitest.clearAllMocks() + + // Mock API handler that simulates real AI responses + mockApiHandler = { + createMessage: vitest.fn(), + getModel: vitest.fn().mockReturnValue({ id: "test-model", info: { maxTokens: 4096 } }), + countTokens: vitest.fn().mockResolvedValue(100), + completePrompt: vitest.fn(), + } + + // Mock task with API handler + mockTask = { + api: mockApiHandler, + assistantMessageContent: [], + cwd: "/test/dir", + say: vitest.fn(), + } + }) + + describe("AI Fallback Generation", () => { + test("should use AI to generate contextual suggestions when available", async () => { + // Mock AI response with numbered suggestions + const mockAiResponse = `Here are some suggestions for the timeout: + +1. Break the npm install command into smaller package installations +2. Clear npm cache and try again with npm cache clean --force +3. Use npm install --no-optional to skip optional dependencies +4. Check network connectivity and try with different registry` + + ;(mockApiHandler.completePrompt as any).mockResolvedValueOnce(mockAiResponse) + + const context = { + toolName: "execute_command" as const, + timeoutMs: 30000, + executionTimeMs: 35000, + toolParams: { command: "npm install" }, + } + + const result = await TimeoutFallbackHandler.generateAiFallback(context, mockTask as Task) + + expect(result.success).toBe(true) + expect(result.toolCall).toBeDefined() + expect(result.toolCall?.name).toBe("ask_followup_question") + expect(result.toolCall?.params.question).toContain("execute_command") + expect(result.toolCall?.params.question).toContain("30 seconds") + expect(result.toolCall?.params.question).toContain("may still be running in the background") + + // Check that AI-generated suggestions are included + const followUp = result.toolCall?.params.follow_up || "" + expect(followUp).toContain("Break the npm install command into smaller package installations") + expect(followUp).toContain("Clear npm cache and try again") + expect(followUp).toContain("Use npm install --no-optional") + expect(followUp).toContain("Check network connectivity") + + // Verify AI was called with proper prompt + expect(mockApiHandler.completePrompt).toHaveBeenCalledWith( + expect.stringContaining("execute_command operation has timed out"), + ) + expect(mockApiHandler.completePrompt).toHaveBeenCalledWith(expect.stringContaining("npm install")) + }) + + test("should fallback to static suggestions when AI fails", async () => { + // Mock AI failure + ;(mockApiHandler.completePrompt as any).mockRejectedValueOnce(new Error("API Error")) + + const context = { + toolName: "execute_command" as const, + timeoutMs: 30000, + executionTimeMs: 35000, + toolParams: { command: "npm test" }, + } + + const result = await TimeoutFallbackHandler.generateAiFallback(context, mockTask as Task) + + expect(result.success).toBe(true) + expect(result.toolCall).toBeDefined() + expect(result.toolCall?.name).toBe("ask_followup_question") + + // Should contain static fallback suggestions + const followUp = result.toolCall?.params.follow_up || "" + expect(followUp).toContain("still running in the background") + expect(followUp).toContain('Break "npm test" into smaller') + expect(followUp).toContain("background using") + expect(followUp).toContain("alternative approach") + }) + + test("should timeout AI fallback generation using exact tool timeout", async () => { + // Mock AI handler that takes longer than the timeout + ;(mockApiHandler.completePrompt as any).mockImplementation(() => { + return new Promise((resolve) => { + // This would resolve after 5 seconds, but should timeout before that + setTimeout(() => { + resolve("This response should never be used") + }, 5000) + }) + }) + + const context = { + toolName: "execute_command" as const, + timeoutMs: 2000, // 2 second timeout for faster testing + executionTimeMs: 2500, + toolParams: { command: "npm install" }, + } + + const startTime = Date.now() + // The timeout is now taken from context.timeoutMs + const result = await TimeoutFallbackHandler.generateAiFallback(context, mockTask as Task) + const elapsedTime = Date.now() - startTime + + // Should timeout and fallback to static suggestions + expect(result.success).toBe(true) + expect(result.toolCall).toBeDefined() + expect(result.toolCall?.name).toBe("ask_followup_question") + + // Should have timed out around 2 seconds (exact tool timeout) + expect(elapsedTime).toBeGreaterThanOrEqual(1900) + expect(elapsedTime).toBeLessThan(2500) + + // Should contain static fallback suggestions since AI timed out + const followUp = result.toolCall?.params.follow_up || "" + expect(followUp).toContain("still running in the background") + expect(followUp).toContain('Break "npm install" into smaller') + expect(followUp).toContain("background using") + expect(followUp).toContain("alternative approach") + + // Verify AI was called + expect(mockApiHandler.completePrompt).toHaveBeenCalled() + }, 5000) // Test timeout of 5 seconds + + test("should fallback to static suggestions when API handler is unavailable", async () => { + // Task without API handler + const taskWithoutApi = {} + + const context = { + toolName: "read_file" as const, + timeoutMs: 5000, + executionTimeMs: 6000, + toolParams: { path: "/large/file.txt" }, + } + + const result = await TimeoutFallbackHandler.generateAiFallback(context, taskWithoutApi as Task) + + expect(result.success).toBe(true) + expect(result.toolCall).toBeDefined() + + // Should contain static fallback suggestions for read_file + const followUp = result.toolCall?.params.follow_up || "" + expect(followUp).toContain('Read "/large/file.txt" in smaller chunks') + expect(followUp).toContain("accessible and not locked") + }) + + test("should parse AI response with different numbering formats", async () => { + // Test different numbering formats + const mockAiResponse = `Here are the suggestions: + +1) Try breaking the command into parts +2. Use a different approach +3) Check system resources +4. Increase timeout duration` + + ;(mockApiHandler.completePrompt as any).mockResolvedValueOnce(mockAiResponse) + + const context = { + toolName: "execute_command" as const, + timeoutMs: 10000, + executionTimeMs: 12000, + toolParams: { command: "build script" }, + } + + const result = await TimeoutFallbackHandler.generateAiFallback(context, mockTask as Task) + + expect(result.success).toBe(true) + const followUp = result.toolCall?.params.follow_up || "" + expect(followUp).toContain("Try breaking the command into parts") + expect(followUp).toContain("Use a different approach") + expect(followUp).toContain("Check system resources") + expect(followUp).toContain("Increase timeout duration") + }) + + test("should handle AI response without numbered list", async () => { + // AI response without clear numbering + const mockAiResponse = `You could try splitting the operation. Another option is to check the network. Maybe increase the timeout. Consider using a different tool.` + + ;(mockApiHandler.completePrompt as any).mockResolvedValueOnce(mockAiResponse) + + const context = { + toolName: "browser_action" as const, + timeoutMs: 15000, + executionTimeMs: 16000, + toolParams: { action: "click" }, + } + + const result = await TimeoutFallbackHandler.generateAiFallback(context, mockTask as Task) + + expect(result.success).toBe(true) + const followUp = result.toolCall?.params.follow_up || "" + + // Should extract sentences as suggestions + expect(followUp).toContain("You could try splitting the operation") + expect(followUp).toContain("Another option is to check the network") + }) + + test("should include task context in AI prompt when available", async () => { + const mockAiResponse = `1. Try a different approach\n2. Check the working directory\n3. Break into steps` + ;(mockApiHandler.completePrompt as any).mockResolvedValueOnce(mockAiResponse) + + const context = { + toolName: "search_files" as const, + timeoutMs: 20000, + executionTimeMs: 22000, + toolParams: { path: "/project", regex: ".*\\.ts$" }, + taskContext: { + currentStep: "Finding TypeScript files", + workingDirectory: "/project/src", + previousActions: ["read package.json", "list files"], + }, + } + + const result = await TimeoutFallbackHandler.generateAiFallback(context, mockTask as Task) + + expect(result.success).toBe(true) + + // Verify the prompt included task context + const calledPrompt = (mockApiHandler.completePrompt as any).mock.calls[0][0] + expect(calledPrompt).toContain("Current step: Finding TypeScript files") + expect(calledPrompt).toContain("Working directory: /project/src") + expect(calledPrompt).toContain("search_files") + expect(calledPrompt).toContain("ts$") // Just check for the pattern ending + }) + + test("should limit suggestions to maximum of 4", async () => { + // AI response with many suggestions + const mockAiResponse = `Here are many suggestions: + +1. First suggestion +2. Second suggestion +3. Third suggestion +4. Fourth suggestion +5. Fifth suggestion +6. Sixth suggestion +7. Seventh suggestion` + + ;(mockApiHandler.completePrompt as any).mockResolvedValueOnce(mockAiResponse) + + const context = { + toolName: "write_to_file" as const, + timeoutMs: 8000, + executionTimeMs: 9000, + toolParams: { path: "/output.txt" }, + } + + const result = await TimeoutFallbackHandler.generateAiFallback(context, mockTask as Task) + + expect(result.success).toBe(true) + const followUp = result.toolCall?.params.follow_up || "" + + // Count the number of tags + const suggestCount = (followUp.match(//g) || []).length + expect(suggestCount).toBeLessThanOrEqual(4) + + // Should include first 4 suggestions + expect(followUp).toContain("First suggestion") + expect(followUp).toContain("Fourth suggestion") + // Should not include 5th and beyond + expect(followUp).not.toContain("Fifth suggestion") + }) + }) + + describe("End-to-End AI Tests", () => { + test("should generate realistic AI suggestions for execute_command timeout", async () => { + // Create a realistic mock API handler + const testApiHandler: MockApiHandler = { + createMessage: vitest.fn(), + getModel: vitest.fn().mockReturnValue({ id: "claude-3-sonnet", info: { maxTokens: 4096 } }), + countTokens: vitest.fn().mockResolvedValue(150), + completePrompt: vitest.fn().mockResolvedValue( + ` +Here are some suggestions for the npm install timeout: + +1. Clear npm cache with "npm cache clean --force" and retry +2. Break installation into smaller chunks by installing packages individually +3. Use "npm install --no-optional" to skip optional dependencies +4. Check network connectivity and try with a different registry + `.trim(), + ), + } + + const testTask: Partial = { + api: testApiHandler, + } + + const context = { + toolName: "execute_command" as const, + timeoutMs: 60000, + executionTimeMs: 65000, + toolParams: { + command: "npm install", + cwd: "/project", + }, + } + + const result = await TimeoutFallbackHandler.generateAiFallback(context, testTask as Task) + + // Verify the result structure + expect(result.success).toBe(true) + expect(result.toolCall).toBeDefined() + expect(result.toolCall?.name).toBe("ask_followup_question") + expect(result.toolCall?.params.question).toContain("execute_command") + expect(result.toolCall?.params.question).toContain("60 seconds") + + // Verify AI-generated suggestions are included + const followUp = result.toolCall?.params.follow_up || "" + expect(followUp).toContain("Clear npm cache") + expect(followUp).toContain("Break installation into smaller chunks") + expect(followUp).toContain("no-optional") + expect(followUp).toContain("network connectivity") + + // Verify the AI was called with a proper prompt + expect(testApiHandler.completePrompt).toHaveBeenCalledWith( + expect.stringContaining("execute_command operation has timed out"), + ) + expect(testApiHandler.completePrompt).toHaveBeenCalledWith(expect.stringContaining("npm install")) + expect(testApiHandler.completePrompt).toHaveBeenCalledWith(expect.stringContaining("60 seconds")) + }) + + test("should handle AI response with different formatting", async () => { + // Mock AI response with different numbering style + const testApiHandler: MockApiHandler = { + createMessage: vitest.fn(), + getModel: vitest.fn().mockReturnValue({ id: "gpt-4", info: { maxTokens: 8192 } }), + countTokens: vitest.fn().mockResolvedValue(200), + completePrompt: vitest.fn().mockResolvedValue( + ` +Based on the search_files timeout, here are my recommendations: + +• Limit search to specific subdirectories instead of entire project +• Use more specific regex patterns to reduce matches +• Try list_files first to understand directory structure +• Consider breaking search into multiple smaller operations + `.trim(), + ), + } + + const testTask: Partial = { + api: testApiHandler, + } + + const context = { + toolName: "search_files" as const, + timeoutMs: 30000, + executionTimeMs: 32000, + toolParams: { + path: "/large-project", + regex: ".*", + file_pattern: "*.ts", + }, + } + + const result = await TimeoutFallbackHandler.generateAiFallback(context, testTask as Task) + + expect(result.success).toBe(true) + expect(result.toolCall).toBeDefined() + + // Should extract suggestions even with bullet points + const followUp = result.toolCall?.params.follow_up || "" + expect(followUp).toContain("Narrow the search scope") + expect(followUp).toContain("simpler search patterns") + expect(followUp).toContain("file type filters") + expect(followUp).toContain("incrementally in smaller batches") + }) + + test("should gracefully handle AI failure and use static fallback", async () => { + // Mock API handler that fails + const testApiHandler: MockApiHandler = { + createMessage: vitest.fn(), + getModel: vitest.fn().mockReturnValue({ id: "test-model", info: { maxTokens: 4096 } }), + countTokens: vitest.fn().mockResolvedValue(100), + completePrompt: vitest.fn().mockRejectedValue(new Error("API rate limit exceeded")), + } + + const testTask: Partial = { + api: testApiHandler, + } + + const context = { + toolName: "read_file" as const, + timeoutMs: 10000, + executionTimeMs: 12000, + toolParams: { + path: "/very/large/file.log", + }, + } + + const result = await TimeoutFallbackHandler.generateAiFallback(context, testTask as Task) + + expect(result.success).toBe(true) + expect(result.toolCall).toBeDefined() + + // Should contain static fallback suggestions for read_file + const followUp = result.toolCall?.params.follow_up || "" + expect(followUp).toContain('Read "/very/large/file.log" in smaller chunks') + expect(followUp).toContain("accessible and not locked") + expect(followUp).toContain("different approach") + expect(followUp).toContain("Increase the timeout") + + // Verify AI was attempted but failed gracefully + expect(testApiHandler.completePrompt).toHaveBeenCalled() + }) + + test("should work without task API handler", async () => { + // Task without API handler + const testTask: Partial = {} + + const context = { + toolName: "browser_action" as const, + timeoutMs: 15000, + executionTimeMs: 16500, + toolParams: { + action: "click", + coordinate: "450,300", + }, + } + + const result = await TimeoutFallbackHandler.generateAiFallback(context, testTask as Task) + + expect(result.success).toBe(true) + expect(result.toolCall).toBeDefined() + + // Should contain static fallback suggestions for browser_action + const followUp = result.toolCall?.params.follow_up || "" + expect(followUp).toContain("still processing in the background") + expect(followUp).toContain('Simplify the "click"') + expect(followUp).toContain("Wait for specific elements") + expect(followUp).toContain("Reset the browser session") + }) + }) + + describe("Tool Call Response Generation", () => { + test("should return response with ask_followup_question tool instructions", async () => { + // Mock the TimeoutFallbackHandler to return a successful AI result + const mockAiResult: TimeoutFallbackResult = { + success: true, + toolCall: { + name: "ask_followup_question", + params: { + question: + "The execute_command operation timed out after 5 seconds. How would you like to proceed?", + follow_up: + "Try a different approach\nBreak into smaller steps", + }, + }, + } + + // Spy on the generateAiFallback method + const generateSpy = vitest.spyOn(TimeoutFallbackHandler, "generateAiFallback") + generateSpy.mockResolvedValue(mockAiResult) + + // Call createTimeoutResponse + const response = await TimeoutFallbackHandler.createTimeoutResponse( + "execute_command", + 5000, + 6000, + { command: "npm install" }, + mockTask as Task, + ) + + // Check that the response contains the base timeout message + expect(response).toContain("timed out after 5 seconds") + expect(response).toContain("Execution Time: 6s") + + // Check that the response includes instructions to use ask_followup_question + expect(response).toContain("You MUST now use the ask_followup_question tool") + expect(response).toContain("") + expect(response).toContain(`${mockAiResult.toolCall?.params.question}`) + expect(response).toContain("") + expect(response).toContain(mockAiResult.toolCall?.params.follow_up) + expect(response).toContain("") + expect(response).toContain("") + expect(response).toContain("This is required to help the user decide how to proceed after the timeout.") + + // Verify that assistantMessageContent was NOT modified + expect(mockTask.assistantMessageContent).toHaveLength(0) + + generateSpy.mockRestore() + }) + + test("should return fallback message when AI generation fails", async () => { + // Spy on the generateAiFallback method to return a failure + const generateSpy = vitest.spyOn(TimeoutFallbackHandler, "generateAiFallback") + generateSpy.mockResolvedValue({ + success: false, + error: "AI generation failed", + }) + + // Call createTimeoutResponse + const response = await TimeoutFallbackHandler.createTimeoutResponse( + "execute_command", + 5000, + 6000, + { command: "npm install" }, + mockTask as Task, + ) + + // Check that the response contains the base timeout message + expect(response).toContain("timed out after 5 seconds") + expect(response).toContain("Execution Time: 6s") + + // Check that the response contains the fallback message + expect(response).toContain( + "The operation timed out. Please consider breaking this into smaller steps or trying a different approach.", + ) + + // Should not contain ask_followup_question instructions + expect(response).not.toContain("ask_followup_question") + + generateSpy.mockRestore() + }) + }) + + describe("UI Integration", () => { + test("should generate AI fallbacks using static method", async () => { + const context = { + toolName: "execute_command" as const, + timeoutMs: 30000, + executionTimeMs: 25000, + toolParams: { command: "npm install" }, + } + + const result = await TimeoutFallbackHandler.generateAiFallback(context) + + expect(result.success).toBe(true) + expect(result.toolCall).toBeDefined() + expect(result.toolCall?.name).toBe("ask_followup_question") + expect(result.toolCall?.params.question).toContain("execute_command") + }) + + test("should create timeout response with AI fallbacks", async () => { + const response = await TimeoutFallbackHandler.createTimeoutResponse("execute_command", 30000, 25000, { + command: "npm install", + }) + + expect(response).toContain("execute_command") + expect(response).toContain("timed out") + expect(response.length).toBeGreaterThan(100) // Should contain substantial content + }) + + test("should handle different tool types with AI fallbacks", async () => { + const commandResponse = await TimeoutFallbackHandler.createTimeoutResponse( + "execute_command", + 30000, + 25000, + { + command: "npm test", + }, + ) + + const browserResponse = await TimeoutFallbackHandler.createTimeoutResponse("browser_action", 30000, 25000, { + action: "click", + }) + + expect(commandResponse).toContain("execute_command") + expect(browserResponse).toContain("browser_action") + expect(commandResponse).not.toEqual(browserResponse) + }) + + test("should validate UI setting flow", () => { + // This test validates that timeout settings can be toggled + const settings = { + timeoutFallbackEnabled: true, + toolExecutionTimeoutMs: 30000, + } + + // Simulate UI toggle + settings.timeoutFallbackEnabled = false + expect(settings.timeoutFallbackEnabled).toBe(false) + + // Simulate timeout duration change + settings.toolExecutionTimeoutMs = 60000 + expect(settings.toolExecutionTimeoutMs).toBe(60000) + }) + }) +}) diff --git a/src/core/timeout/__tests__/timeout-integration.spec.ts b/src/core/timeout/__tests__/timeout-integration.spec.ts new file mode 100644 index 0000000000..acc9161486 --- /dev/null +++ b/src/core/timeout/__tests__/timeout-integration.spec.ts @@ -0,0 +1,274 @@ +// npx vitest run src/core/timeout/__tests__/timeout-integration.spec.ts + +import { describe, test, expect, beforeEach, vitest } from "vitest" +import { TimeoutManager } from "../TimeoutManager" +import { ToolExecutionWrapper } from "../ToolExecutionWrapper" +import { TimeoutFallbackHandler } from "../TimeoutFallbackHandler" +import type { Task } from "../../task/Task" + +describe("Timeout Integration Tests", () => { + beforeEach(() => { + vitest.clearAllMocks() + }) + + describe("TimeoutManager Integration", () => { + test("should handle basic timeout operations", async () => { + const manager = TimeoutManager.getInstance() + + // Test successful operation within timeout + const result = await manager.executeWithTimeout( + async () => { + await new Promise((resolve) => setTimeout(resolve, 10)) + return "success" + }, + { + toolName: "execute_command", + timeoutMs: 100, + enableFallback: true, + }, + ) + + expect(result.success).toBe(true) + expect(result.result).toBe("success") + expect(result.timedOut).toBe(false) + }) + + test("should handle timeout scenarios", async () => { + const manager = TimeoutManager.getInstance() + + // Test operation that times out + const result = await manager.executeWithTimeout( + async () => { + await new Promise((resolve) => setTimeout(resolve, 200)) + return "should not reach here" + }, + { + toolName: "execute_command", + timeoutMs: 50, + enableFallback: true, + }, + ) + + expect(result.success).toBe(false) + expect(result.timedOut).toBe(true) + expect(result.error?.message).toContain("Operation timed out") + }) + }) + + describe("ToolExecutionWrapper Integration", () => { + test("should wrap operations correctly", async () => { + const mockOperation = vitest.fn().mockImplementation(async (signal: AbortSignal) => { + // Simulate checking abort signal + if (signal.aborted) { + throw new Error("Operation was aborted") + } + await new Promise((resolve) => setTimeout(resolve, 10)) + return [false, "test result"] + }) + + const result = await ToolExecutionWrapper.execute( + mockOperation, + { + toolName: "execute_command", + taskId: "test-task", + timeoutMs: 100, + enableFallback: true, + }, + 100, + ) + + expect(result.success).toBe(true) + expect(result.result).toEqual([false, "test result"]) + expect(mockOperation).toHaveBeenCalledWith(expect.any(AbortSignal)) + }) + + test("should handle timeout with fallback", async () => { + const mockOperation = vitest.fn().mockImplementation(async () => { + await new Promise((resolve) => setTimeout(resolve, 200)) + return [false, "should not reach here"] + }) + + const result = await ToolExecutionWrapper.execute( + mockOperation, + { + toolName: "execute_command", + taskId: "test-task", + timeoutMs: 50, + enableFallback: true, + }, + 50, + ) + + expect(result.success).toBe(false) + expect(result.timedOut).toBe(true) + expect(result.fallbackTriggered).toBe(true) + }) + }) + + describe("TimeoutFallbackHandler Integration", () => { + test("should create AI-powered responses", async () => { + // Create a mock task to test tool injection + const mockTask = { + assistantMessageContent: [], + cwd: "/test/dir", + say: vitest.fn().mockResolvedValue(undefined), + } as unknown as Task + + const response = await TimeoutFallbackHandler.createTimeoutResponse( + "execute_command", + 5000, + 6000, + { command: "npm install" }, + mockTask, + ) + + // The response should contain the basic timeout information + expect(response).toContain("execute_command") + expect(response).toContain("5 seconds") + expect(response).toContain("6s") + expect(response.length).toBeGreaterThan(50) + + // The response should now contain instructions to use ask_followup_question + expect(response).toContain("You MUST now use the ask_followup_question tool") + expect(response).toContain("") + expect(response).toContain("") + expect(response).toContain("timed out") + expect(response).toContain("") + expect(response).toContain("") + expect(response).toContain("") + expect(response).toContain("") + + // Verify that assistantMessageContent was NOT modified + expect(mockTask.assistantMessageContent).toHaveLength(0) + }) + }) + + describe("AbortSignal Integration", () => { + test("should be properly handled", async () => { + const mockOperation = vitest.fn().mockImplementation(async (signal: AbortSignal) => { + // Simulate a long-running operation that checks abort signal + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + if (signal.aborted) { + reject(new Error("Operation was aborted")) + } else { + resolve("success") + } + }, 100) + + signal.addEventListener("abort", () => { + clearTimeout(timeout) + reject(new Error("Operation was aborted")) + }) + }) + }) + + const result = await ToolExecutionWrapper.execute( + mockOperation, + { + toolName: "execute_command", + taskId: "test-task", + timeoutMs: 50, // Shorter timeout to trigger abort + enableFallback: false, + }, + 50, + ) + + expect(result.success).toBe(false) + expect(result.timedOut).toBe(true) + }) + }) + + describe("Cross-Component Integration", () => { + test("should coordinate between TimeoutManager and ToolExecutionWrapper", async () => { + const manager = TimeoutManager.getInstance() + + const mockOperation = vitest.fn().mockImplementation(async (signal: AbortSignal) => { + // Simulate operation that respects abort signal + await new Promise((resolve, reject) => { + const timeout = setTimeout(resolve, 150) + signal.addEventListener("abort", () => { + clearTimeout(timeout) + reject(new Error("Operation was aborted")) + }) + }) + return [false, "completed"] + }) + + // Use ToolExecutionWrapper directly + const result = await ToolExecutionWrapper.execute( + mockOperation, + { + toolName: "read_file", + taskId: "integration-test", + timeoutMs: 100, + enableFallback: true, + }, + 100, + ) + + expect(result.success).toBe(false) + expect(result.timedOut).toBe(true) + }) + + test("should handle nested timeout scenarios", async () => { + const manager = TimeoutManager.getInstance() + + // Test nested timeout operations + const outerResult = await manager.executeWithTimeout( + async () => { + // Inner timeout operation + return await manager.executeWithTimeout( + async () => { + await new Promise((resolve) => setTimeout(resolve, 150)) + return "inner success" + }, + { + toolName: "search_files", + timeoutMs: 100, + enableFallback: true, + }, + ) + }, + { + toolName: "execute_command", + timeoutMs: 200, + enableFallback: true, + }, + ) + + // The inner operation should timeout, but the outer should succeed with the timeout result + expect(outerResult.success).toBe(true) + if (outerResult.result) { + expect(outerResult.result.success).toBe(false) + expect(outerResult.result.timedOut).toBe(true) + } + }) + + test("should maintain timeout event tracking across components", async () => { + const manager = TimeoutManager.getInstance() + manager.clearLastTimeoutEvent() + + // Execute operation that will timeout + await manager.executeWithTimeout( + async () => { + await new Promise((resolve) => setTimeout(resolve, 200)) + return "should timeout" + }, + { + toolName: "browser_action", + timeoutMs: 50, + enableFallback: true, + taskId: "tracking-test", + }, + ) + + // Verify timeout event was tracked + const timeoutEvent = manager.getLastTimeoutEvent() + expect(timeoutEvent).toBeTruthy() + expect(timeoutEvent?.toolName).toBe("browser_action") + expect(timeoutEvent?.taskId).toBe("tracking-test") + expect(timeoutEvent?.timeoutMs).toBe(50) + }) + }) +}) diff --git a/src/core/timeout/__tests__/timeout-manager.spec.ts b/src/core/timeout/__tests__/timeout-manager.spec.ts new file mode 100644 index 0000000000..5b218d19f5 --- /dev/null +++ b/src/core/timeout/__tests__/timeout-manager.spec.ts @@ -0,0 +1,178 @@ +// npx vitest run src/core/timeout/__tests__/timeout-manager.spec.ts + +import { describe, test, expect, beforeEach, vitest } from "vitest" +import { TimeoutManager } from "../TimeoutManager" + +describe("TimeoutManager", () => { + let manager: TimeoutManager + + beforeEach(() => { + manager = TimeoutManager.getInstance() + manager.clearLastTimeoutEvent() + vitest.clearAllMocks() + }) + + describe("Basic Operations", () => { + test("should handle successful operation within timeout", async () => { + const result = await manager.executeWithTimeout( + async () => { + await new Promise((resolve) => setTimeout(resolve, 10)) + return "success" + }, + { + toolName: "execute_command", + timeoutMs: 100, + enableFallback: true, + }, + ) + + expect(result.success).toBe(true) + expect(result.result).toBe("success") + expect(result.timedOut).toBe(false) + }) + + test("should handle timeout scenarios", async () => { + const result = await manager.executeWithTimeout( + async () => { + await new Promise((resolve) => setTimeout(resolve, 200)) + return "should not reach here" + }, + { + toolName: "execute_command", + timeoutMs: 50, + enableFallback: true, + }, + ) + + expect(result.success).toBe(false) + expect(result.timedOut).toBe(true) + expect(result.error?.message).toContain("Operation timed out") + }) + }) + + describe("Timeout Event Tracking", () => { + test("should track only the last timeout event", async () => { + // First timeout + await manager.executeWithTimeout( + async () => { + await new Promise((resolve) => setTimeout(resolve, 200)) + return "should timeout" + }, + { + toolName: "execute_command", + timeoutMs: 50, + enableFallback: true, + taskId: "task-1", + }, + ) + + const firstTimeout = manager.getLastTimeoutEvent() + expect(firstTimeout).toBeTruthy() + expect(firstTimeout?.toolName).toBe("execute_command") + expect(firstTimeout?.taskId).toBe("task-1") + + // Second timeout should replace the first + await manager.executeWithTimeout( + async () => { + await new Promise((resolve) => setTimeout(resolve, 200)) + return "should timeout" + }, + { + toolName: "read_file", + timeoutMs: 50, + enableFallback: true, + taskId: "task-2", + }, + ) + + const secondTimeout = manager.getLastTimeoutEvent() + expect(secondTimeout).toBeTruthy() + expect(secondTimeout?.toolName).toBe("read_file") + expect(secondTimeout?.taskId).toBe("task-2") + expect(secondTimeout?.timestamp).toBeGreaterThan(firstTimeout!.timestamp) + }) + + test("should clear last timeout event", async () => { + // Create a timeout + await manager.executeWithTimeout( + async () => { + await new Promise((resolve) => setTimeout(resolve, 200)) + return "should timeout" + }, + { + toolName: "execute_command", + timeoutMs: 50, + enableFallback: true, + }, + ) + + expect(manager.getLastTimeoutEvent()).toBeTruthy() + + // Clear it + manager.clearLastTimeoutEvent() + expect(manager.getLastTimeoutEvent()).toBeNull() + }) + + test("should return null when no timeout has occurred", () => { + expect(manager.getLastTimeoutEvent()).toBeNull() + }) + }) + + describe("Operation Management", () => { + test("cancelOperation should work with simplified operation IDs", async () => { + // Start a long-running operation + const operationPromise = manager.executeWithTimeout( + async (signal) => { + await new Promise((resolve, reject) => { + const timeout = setTimeout(resolve, 1000) + signal.addEventListener("abort", () => { + clearTimeout(timeout) + reject(new Error("Operation was aborted")) + }) + }) + return "should be cancelled" + }, + { + toolName: "execute_command", + timeoutMs: 2000, + enableFallback: true, + taskId: "cancel-test", + }, + ) + + // Cancel it immediately + const cancelled = manager.cancelOperation("execute_command", "cancel-test") + expect(cancelled).toBe(true) + + // Verify it was cancelled + const result = await operationPromise + expect(result.success).toBe(false) + expect(result.error?.message).toContain("Operation was aborted") + }) + + test("isOperationActive should work with simplified operation IDs", async () => { + // Start an operation + const operationPromise = manager.executeWithTimeout( + async () => { + await new Promise((resolve) => setTimeout(resolve, 100)) + return "success" + }, + { + toolName: "read_file", + timeoutMs: 200, + enableFallback: true, + taskId: "active-test", + }, + ) + + // Check if it's active + expect(manager.isOperationActive("read_file", "active-test")).toBe(true) + + // Wait for completion + await operationPromise + + // Should no longer be active + expect(manager.isOperationActive("read_file", "active-test")).toBe(false) + }) + }) +}) diff --git a/src/core/timeout/__tests__/timeout-telemetry.spec.ts b/src/core/timeout/__tests__/timeout-telemetry.spec.ts new file mode 100644 index 0000000000..a8fd4effc9 --- /dev/null +++ b/src/core/timeout/__tests__/timeout-telemetry.spec.ts @@ -0,0 +1,256 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" +import { TimeoutManager } from "../TimeoutManager" +import { TelemetryService } from "@roo-code/telemetry" + +// Mock TelemetryService +vi.mock("@roo-code/telemetry", () => ({ + TelemetryService: { + hasInstance: vi.fn(), + instance: { + captureToolTimeout: vi.fn(), + }, + }, +})) + +describe("TimeoutManager Telemetry and Logging", () => { + let timeoutManager: TimeoutManager + let mockLogger: ReturnType + let originalInstance: TimeoutManager | undefined + + beforeEach(() => { + // Store the original instance + originalInstance = (TimeoutManager as any).instance + // Reset the singleton instance + ;(TimeoutManager as any).instance = undefined + + // Create mock logger + mockLogger = vi.fn() + + // Create new instance with mock logger + timeoutManager = TimeoutManager.getInstance(mockLogger) + + // Setup TelemetryService mock + vi.mocked(TelemetryService.hasInstance).mockReturnValue(true) + vi.mocked(TelemetryService.instance.captureToolTimeout).mockClear() + }) + + afterEach(() => { + // Restore the original instance + ;(TimeoutManager as any).instance = originalInstance + vi.clearAllMocks() + }) + + describe("Logging", () => { + it("should log operation start", async () => { + const operation = vi.fn().mockResolvedValue("result") + + await timeoutManager.executeWithTimeout(operation, { + toolName: "execute_command", + timeoutMs: 1000, + enableFallback: false, + taskId: "task-123", + }) + + expect(mockLogger).toHaveBeenCalledWith( + expect.stringContaining( + "[TimeoutManager] Starting operation execute_command:task-123 with timeout 1000ms", + ), + ) + }) + + it("should log successful operation completion", async () => { + const operation = vi.fn().mockResolvedValue("result") + + await timeoutManager.executeWithTimeout(operation, { + toolName: "execute_command", + timeoutMs: 1000, + enableFallback: false, + taskId: "task-123", + }) + + expect(mockLogger).toHaveBeenCalledWith( + expect.stringMatching( + /\[TimeoutManager\] Operation execute_command:task-123 completed successfully in \d+ms/, + ), + ) + }) + + it("should log timeout events", async () => { + const operation = vi.fn().mockImplementation(() => new Promise((resolve) => setTimeout(resolve, 2000))) + + await timeoutManager.executeWithTimeout(operation, { + toolName: "execute_command", + timeoutMs: 100, + enableFallback: false, + taskId: "task-123", + }) + + expect(mockLogger).toHaveBeenCalledWith( + expect.stringMatching( + /\[TimeoutManager\] Operation execute_command:task-123 timed out after \d+ms \(limit: 100ms\)/, + ), + ) + }) + + it("should log non-timeout errors", async () => { + const error = new Error("Test error") + const operation = vi.fn().mockRejectedValue(error) + + await timeoutManager.executeWithTimeout(operation, { + toolName: "execute_command", + timeoutMs: 1000, + enableFallback: false, + taskId: "task-123", + }) + + expect(mockLogger).toHaveBeenCalledWith( + expect.stringMatching( + /\[TimeoutManager\] Operation execute_command:task-123 failed after \d+ms: Test error/, + ), + ) + }) + }) + + describe("Telemetry", () => { + it("should capture telemetry for timeout events when TelemetryService is available", async () => { + const operation = vi.fn().mockImplementation(() => new Promise((resolve) => setTimeout(resolve, 2000))) + + const result = await timeoutManager.executeWithTimeout(operation, { + toolName: "execute_command", + timeoutMs: 100, + enableFallback: false, + taskId: "task-123", + }) + + expect(result.timedOut).toBe(true) + expect(TelemetryService.instance.captureToolTimeout).toHaveBeenCalledWith( + "task-123", + "execute_command", + 100, + expect.any(Number), + ) + }) + + it("should not capture telemetry when taskId is not provided", async () => { + const operation = vi.fn().mockImplementation(() => new Promise((resolve) => setTimeout(resolve, 2000))) + + await timeoutManager.executeWithTimeout(operation, { + toolName: "execute_command", + timeoutMs: 100, + enableFallback: false, + }) + + expect(TelemetryService.instance.captureToolTimeout).not.toHaveBeenCalled() + }) + + it("should not capture telemetry when TelemetryService is not available", async () => { + vi.mocked(TelemetryService.hasInstance).mockReturnValue(false) + + const operation = vi.fn().mockImplementation(() => new Promise((resolve) => setTimeout(resolve, 2000))) + + await timeoutManager.executeWithTimeout(operation, { + toolName: "execute_command", + timeoutMs: 100, + enableFallback: false, + taskId: "task-123", + }) + + expect(TelemetryService.instance.captureToolTimeout).not.toHaveBeenCalled() + }) + + it("should not capture telemetry for successful operations", async () => { + const operation = vi.fn().mockResolvedValue("result") + + await timeoutManager.executeWithTimeout(operation, { + toolName: "execute_command", + timeoutMs: 1000, + enableFallback: false, + taskId: "task-123", + }) + + expect(TelemetryService.instance.captureToolTimeout).not.toHaveBeenCalled() + }) + + it("should not capture telemetry for non-timeout errors", async () => { + const operation = vi.fn().mockRejectedValue(new Error("Test error")) + + await timeoutManager.executeWithTimeout(operation, { + toolName: "execute_command", + timeoutMs: 1000, + enableFallback: false, + taskId: "task-123", + }) + + expect(TelemetryService.instance.captureToolTimeout).not.toHaveBeenCalled() + }) + }) + + describe("getTimeoutStats", () => { + it("should return timeout statistics", async () => { + // Execute an operation that will timeout + const operation = vi.fn().mockImplementation(() => new Promise((resolve) => setTimeout(resolve, 2000))) + + await timeoutManager.executeWithTimeout(operation, { + toolName: "execute_command", + timeoutMs: 100, + enableFallback: false, + taskId: "task-123", + }) + + const stats = timeoutManager.getTimeoutStats() + + expect(stats.lastTimeout).toBeTruthy() + expect(stats.lastTimeout?.toolName).toBe("execute_command") + expect(stats.lastTimeout?.taskId).toBe("task-123") + expect(stats.lastTimeout?.timeoutMs).toBe(100) + expect(stats.activeOperations).toBe(0) // Should be 0 after operation completes + }) + + it("should track active operations", async () => { + // Start a long-running operation + const operation = vi.fn().mockImplementation(() => new Promise((resolve) => setTimeout(resolve, 5000))) + + const promise = timeoutManager.executeWithTimeout(operation, { + toolName: "execute_command", + timeoutMs: 10000, + enableFallback: false, + taskId: "task-123", + }) + + // Check stats while operation is running + const stats = timeoutManager.getTimeoutStats() + expect(stats.activeOperations).toBe(1) + expect(stats.operationIds).toContain("execute_command:task-123") + + // Cancel to clean up + timeoutManager.cancelOperation("execute_command", "task-123") + await promise.catch(() => {}) // Ignore cancellation error + }) + }) + + describe("Timeout event emission", () => { + it("should emit timeout event with correct data", async () => { + const timeoutHandler = vi.fn() + timeoutManager.on("timeout", timeoutHandler) + + const operation = vi.fn().mockImplementation(() => new Promise((resolve) => setTimeout(resolve, 2000))) + + await timeoutManager.executeWithTimeout(operation, { + toolName: "execute_command", + timeoutMs: 100, + enableFallback: true, + taskId: "task-123", + }) + + expect(timeoutHandler).toHaveBeenCalledWith({ + toolName: "execute_command", + timeoutMs: 100, + executionTimeMs: expect.any(Number), + taskId: "task-123", + timestamp: expect.any(Number), + }) + + timeoutManager.off("timeout", timeoutHandler) + }) + }) +}) diff --git a/src/core/timeout/index.ts b/src/core/timeout/index.ts new file mode 100644 index 0000000000..ad808bc1ca --- /dev/null +++ b/src/core/timeout/index.ts @@ -0,0 +1,8 @@ +export { TimeoutManager, timeoutManager } from "./TimeoutManager" +export { ToolExecutionWrapper } from "./ToolExecutionWrapper" +export { TimeoutFallbackHandler } from "./TimeoutFallbackHandler" + +export type { TimeoutConfig, TimeoutResult, TimeoutEvent } from "./TimeoutManager" +export type { ToolExecutionOptions } from "./ToolExecutionWrapper" +export type { TimeoutFallbackResult } from "./TimeoutFallbackHandler" +export type { TimeoutFallbackContext } from "../prompts/instructions/timeout-fallback" diff --git a/src/core/tools/__tests__/executeCommandTimeout.integration.spec.ts b/src/core/tools/__tests__/executeCommandTimeout.integration.spec.ts index de98c9df20..c185f69df3 100644 --- a/src/core/tools/__tests__/executeCommandTimeout.integration.spec.ts +++ b/src/core/tools/__tests__/executeCommandTimeout.integration.spec.ts @@ -36,6 +36,10 @@ describe("Command Execution Timeout Integration", () => { providerRef: { deref: vitest.fn().mockResolvedValue({ postMessageToWebview: vitest.fn(), + getState: vitest.fn().mockResolvedValue({ + toolExecutionTimeoutMs: 60000, + timeoutFallbackEnabled: false, // Disable new timeout approach for legacy tests + }), }), }, say: vitest.fn().mockResolvedValue(undefined), diff --git a/src/core/tools/executeCommandTool.ts b/src/core/tools/executeCommandTool.ts index 407dc283b5..388ae06546 100644 --- a/src/core/tools/executeCommandTool.ts +++ b/src/core/tools/executeCommandTool.ts @@ -15,9 +15,12 @@ import { unescapeHtmlEntities } from "../../utils/text-normalization" import { ExitCodeDetails, RooTerminalCallbacks, RooTerminalProcess } from "../../integrations/terminal/types" import { TerminalRegistry } from "../../integrations/terminal/TerminalRegistry" import { Terminal } from "../../integrations/terminal/Terminal" +import { ToolExecutionWrapper, TimeoutFallbackHandler } from "../timeout" import { Package } from "../../shared/package" import { t } from "../../i18n" +const DEFAULT_TOOL_EXECUTION_TIMEOUT_MS = 60000 // 1 minute default + class ShellIntegrationError extends Error {} export async function executeCommandTool( @@ -63,7 +66,11 @@ export async function executeCommandTool( const executionId = cline.lastMessageTs?.toString() ?? Date.now().toString() const clineProvider = await cline.providerRef.deref() const clineProviderState = await clineProvider?.getState() - const { terminalOutputLineLimit = 500, terminalShellIntegrationDisabled = false } = clineProviderState ?? {} + const { + terminalOutputLineLimit = 500, + terminalShellIntegrationDisabled = false, + toolExecutionTimeoutMs = 60000, // 1 minute default + } = clineProviderState ?? {} // Get command execution timeout from VSCode configuration (in seconds) const commandExecutionTimeoutSeconds = vscode.workspace @@ -79,6 +86,7 @@ export async function executeCommandTool( customCwd, terminalShellIntegrationDisabled, terminalOutputLineLimit, + timeoutMs: toolExecutionTimeoutMs, commandExecutionTimeout, } @@ -125,6 +133,7 @@ export type ExecuteCommandOptions = { customCwd?: string terminalShellIntegrationDisabled?: boolean terminalOutputLineLimit?: number + timeoutMs?: number commandExecutionTimeout?: number } @@ -136,8 +145,82 @@ export async function executeCommand( customCwd, terminalShellIntegrationDisabled = false, terminalOutputLineLimit = 500, + timeoutMs, commandExecutionTimeout = 0, }: ExecuteCommandOptions, +): Promise<[boolean, ToolResponse]> { + // Get timeout from settings if not provided + const clineProvider = await cline.providerRef.deref() + const clineProviderState = await clineProvider?.getState() + const defaultTimeoutMs = clineProviderState?.toolExecutionTimeoutMs ?? DEFAULT_TOOL_EXECUTION_TIMEOUT_MS + const actualTimeoutMs = timeoutMs ?? defaultTimeoutMs + const timeoutFallbackEnabled = clineProviderState?.timeoutFallbackEnabled ?? true + + // Use the new timeout wrapper approach if timeoutMs is provided or fallback is enabled + if (actualTimeoutMs > 0 && timeoutFallbackEnabled) { + // Wrap the command execution with timeout + const timeoutResult = await ToolExecutionWrapper.execute( + async (signal: AbortSignal) => { + return executeCommandInternal(cline, { + executionId, + command, + customCwd, + terminalShellIntegrationDisabled, + terminalOutputLineLimit, + signal, + commandExecutionTimeout: 0, // Disable the old timeout when using new approach + }) + }, + { + toolName: "execute_command", + taskId: cline.taskId, + timeoutMs: actualTimeoutMs, + enableFallback: timeoutFallbackEnabled, + }, + actualTimeoutMs, + ) + + // Handle timeout result + if (timeoutResult.timedOut && timeoutResult.fallbackTriggered) { + const fallbackResponse = await TimeoutFallbackHandler.createTimeoutResponse( + "execute_command", + actualTimeoutMs, + timeoutResult.executionTimeMs, + { command }, + cline, + ) + return [false, fallbackResponse] + } + + if (!timeoutResult.success) { + return [false, formatResponse.toolError(timeoutResult.error?.message)] + } + + return timeoutResult.result! + } else { + // Use the legacy timeout approach + return executeCommandInternal(cline, { + executionId, + command, + customCwd, + terminalShellIntegrationDisabled, + terminalOutputLineLimit, + commandExecutionTimeout, + }) + } +} + +async function executeCommandInternal( + cline: Task, + { + executionId, + command, + customCwd, + terminalShellIntegrationDisabled = false, + terminalOutputLineLimit = 500, + signal, + commandExecutionTimeout = 0, + }: ExecuteCommandOptions & { signal?: AbortSignal }, ): Promise<[boolean, ToolResponse]> { // Convert milliseconds back to seconds for display purposes const commandExecutionTimeoutSeconds = commandExecutionTimeout / 1000 @@ -227,8 +310,30 @@ export async function executeCommand( const process = terminal.runCommand(command, callbacks) cline.terminalProcess = process - // Implement command execution timeout (skip if timeout is 0) - if (commandExecutionTimeout > 0) { + // Handle abort signal for timeout cancellation (new approach) + if (signal) { + const abortHandler = () => { + if (process && typeof process.abort === "function") { + process.abort() + } + cline.terminalProcess = undefined + } + + if (signal.aborted) { + abortHandler() + throw new Error("Command execution was cancelled due to timeout") + } + + signal.addEventListener("abort", abortHandler) + + try { + await process + } finally { + signal.removeEventListener("abort", abortHandler) + cline.terminalProcess = undefined + } + } else if (commandExecutionTimeout > 0) { + // Implement command execution timeout (legacy approach) let timeoutId: NodeJS.Timeout | undefined let isTimedOut = false diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 107122dcb4..6ce62994b3 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1436,6 +1436,8 @@ export class ClineProvider profileThresholds, alwaysAllowFollowupQuestions, followupAutoApproveTimeoutMs, + toolExecutionTimeoutMs, + timeoutFallbackEnabled, } = await this.getState() const telemetryKey = process.env.POSTHOG_API_KEY @@ -1555,6 +1557,8 @@ export class ClineProvider hasOpenedModeSelector: this.getGlobalState("hasOpenedModeSelector") ?? false, alwaysAllowFollowupQuestions: alwaysAllowFollowupQuestions ?? false, followupAutoApproveTimeoutMs: followupAutoApproveTimeoutMs ?? 60000, + toolExecutionTimeoutMs: toolExecutionTimeoutMs ?? 60000, + timeoutFallbackEnabled: timeoutFallbackEnabled ?? false, } } @@ -1668,6 +1672,8 @@ export class ClineProvider terminalZshP10k: stateValues.terminalZshP10k ?? false, terminalZdotdir: stateValues.terminalZdotdir ?? false, terminalCompressProgressBar: stateValues.terminalCompressProgressBar ?? true, + toolExecutionTimeoutMs: stateValues.toolExecutionTimeoutMs ?? 60000, + timeoutFallbackEnabled: stateValues.timeoutFallbackEnabled ?? false, mode: stateValues.mode ?? defaultModeSlug, language: stateValues.language ?? formatLanguage(vscode.env.language), mcpEnabled: stateValues.mcpEnabled ?? true, diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index e70b39df8f..6578e16da2 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1085,6 +1085,14 @@ export const webviewMessageHandler = async ( await updateGlobalState("terminalOutputLineLimit", message.value) await provider.postStateToWebview() break + case "toolExecutionTimeoutMs": + await updateGlobalState("toolExecutionTimeoutMs", message.value) + await provider.postStateToWebview() + break + case "timeoutFallbackEnabled": + await updateGlobalState("timeoutFallbackEnabled", message.bool) + await provider.postStateToWebview() + break case "terminalShellIntegrationTimeout": await updateGlobalState("terminalShellIntegrationTimeout", message.value) await provider.postStateToWebview() diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 833c51336b..0bdbc4c49b 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -228,6 +228,8 @@ export type ExtensionState = Pick< | "codebaseIndexConfig" | "codebaseIndexModels" | "profileThresholds" + | "toolExecutionTimeoutMs" + | "timeoutFallbackEnabled" > & { version: string clineMessages: ClineMessage[] diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index d5dc3f8c28..22d4774ec2 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -122,6 +122,8 @@ export interface WebviewMessage { | "terminalZshP10k" | "terminalZdotdir" | "terminalCompressProgressBar" + | "toolExecutionTimeoutMs" + | "timeoutFallbackEnabled" | "mcpEnabled" | "enableMcpServerCreation" | "searchCommits" diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index c508f7e906..e775893b23 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -287,6 +287,29 @@ export const ChatRowContent = ({ />, {t("chat:questions.hasQuestion")}, ] + case "tool_timeout": + let showBackgroundWarning = false + try { + const timeoutInfo = message.text ? JSON.parse(message.text) : null + showBackgroundWarning = timeoutInfo?.mightContinueInBackground || false + } catch (_e) { + // If parsing fails, don't show the warning + } + return [ + , + + {t("chat:toolTimeout.title")}{" "} + {t("chat:toolTimeout.subtitle")} + {showBackgroundWarning && ( + <> +
+ + {t("chat:toolTimeout.backgroundWarning")} + + + )} +
, + ] default: return [null, null] } @@ -1172,6 +1195,16 @@ export const ChatRowContent = ({ case "user_edit_todos": return {}} /> default: + // Don't render message text for tool_timeout as it contains JSON metadata + if (message.say === "tool_timeout") { + return title ? ( +
+ {icon} + {title} +
+ ) : null + } + return ( <> {title && ( diff --git a/webview-ui/src/components/settings/AutoApproveSettings.tsx b/webview-ui/src/components/settings/AutoApproveSettings.tsx index e1d3c52cb9..6c1436dea7 100644 --- a/webview-ui/src/components/settings/AutoApproveSettings.tsx +++ b/webview-ui/src/components/settings/AutoApproveSettings.tsx @@ -33,6 +33,8 @@ type AutoApproveSettingsProps = HTMLAttributes & { followupAutoApproveTimeoutMs?: number allowedCommands?: string[] deniedCommands?: string[] + timeoutFallbackEnabled?: boolean + toolExecutionTimeoutMs?: number setCachedStateField: SetCachedStateField< | "alwaysAllowReadOnly" | "alwaysAllowReadOnlyOutsideWorkspace" @@ -52,6 +54,8 @@ type AutoApproveSettingsProps = HTMLAttributes & { | "allowedCommands" | "deniedCommands" | "alwaysAllowUpdateTodoList" + | "timeoutFallbackEnabled" + | "toolExecutionTimeoutMs" > } @@ -74,6 +78,8 @@ export const AutoApproveSettings = ({ alwaysAllowUpdateTodoList, allowedCommands, deniedCommands, + timeoutFallbackEnabled, + toolExecutionTimeoutMs = 60000, setCachedStateField, ...props }: AutoApproveSettingsProps) => { @@ -393,6 +399,50 @@ export const AutoApproveSettings = ({ )} + + {/* TIMEOUT SETTINGS */} +
+
+ +
{t("settings:autoApprove.timeout.label")}
+
+ + {/* Enable timeout handling */} +
+ setCachedStateField("timeoutFallbackEnabled", e.target.checked)} + data-testid="timeout-fallback-enabled-checkbox"> + + {t("settings:autoApprove.timeout.timeoutFallbackEnabled.label")} + + +
+ {t("settings:autoApprove.timeout.timeoutFallbackEnabled.description")} +
+
+ + {/* Tool execution timeout duration */} +
+
+ {t("settings:autoApprove.timeout.toolExecutionTimeoutMs.label")} +
+ setCachedStateField("toolExecutionTimeoutMs", parseInt(e.target.value))} + disabled={!timeoutFallbackEnabled} + className="w-32" + data-testid="tool-execution-timeout-input" + /> +
+ {t("settings:autoApprove.timeout.toolExecutionTimeoutMs.description")} +
+
+
) diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index fd3a8a129b..bcad0e8f9b 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -176,6 +176,8 @@ const SettingsView = forwardRef(({ onDone, t alwaysAllowFollowupQuestions, alwaysAllowUpdateTodoList, followupAutoApproveTimeoutMs, + timeoutFallbackEnabled, + toolExecutionTimeoutMs, } = cachedState const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration]) @@ -325,6 +327,8 @@ const SettingsView = forwardRef(({ onDone, t vscode.postMessage({ type: "alwaysAllowFollowupQuestions", bool: alwaysAllowFollowupQuestions }) vscode.postMessage({ type: "alwaysAllowUpdateTodoList", bool: alwaysAllowUpdateTodoList }) vscode.postMessage({ type: "followupAutoApproveTimeoutMs", value: followupAutoApproveTimeoutMs }) + vscode.postMessage({ type: "timeoutFallbackEnabled", bool: timeoutFallbackEnabled }) + vscode.postMessage({ type: "toolExecutionTimeoutMs", value: toolExecutionTimeoutMs }) vscode.postMessage({ type: "condensingApiConfigId", text: condensingApiConfigId || "" }) vscode.postMessage({ type: "updateCondensingPrompt", text: customCondensingPrompt || "" }) vscode.postMessage({ type: "updateSupportPrompt", values: customSupportPrompts || {} }) @@ -617,6 +621,8 @@ const SettingsView = forwardRef(({ onDone, t followupAutoApproveTimeoutMs={followupAutoApproveTimeoutMs} allowedCommands={allowedCommands} deniedCommands={deniedCommands} + timeoutFallbackEnabled={timeoutFallbackEnabled} + toolExecutionTimeoutMs={toolExecutionTimeoutMs} setCachedStateField={setCachedStateField} /> )} diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 6c70c8940d..e1036c36ce 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -44,6 +44,10 @@ export interface ExtensionStateContextType extends ExtensionState { setAlwaysAllowFollowupQuestions: (value: boolean) => void // Setter for the new property followupAutoApproveTimeoutMs: number | undefined // Timeout in ms for auto-approving follow-up questions setFollowupAutoApproveTimeoutMs: (value: number) => void // Setter for the timeout + timeoutFallbackEnabled?: boolean // New property for timeout fallback enabled + setTimeoutFallbackEnabled: (value: boolean) => void // Setter for timeout fallback enabled + toolExecutionTimeoutMs: number | undefined // New property for tool execution timeout + setToolExecutionTimeoutMs: (value: number) => void // Setter for tool execution timeout condensingApiConfigId?: string setCondensingApiConfigId: (value: string) => void customCondensingPrompt?: string @@ -226,6 +230,9 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode }, codebaseIndexModels: { ollama: {}, openai: {} }, alwaysAllowUpdateTodoList: true, + // Timeout settings + timeoutFallbackEnabled: false, // Default to disabled + toolExecutionTimeoutMs: undefined, // Will be set from global settings }) const [didHydrateState, setDidHydrateState] = useState(false) @@ -239,6 +246,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode const [marketplaceItems, setMarketplaceItems] = useState([]) const [alwaysAllowFollowupQuestions, setAlwaysAllowFollowupQuestions] = useState(false) // Add state for follow-up questions auto-approve const [followupAutoApproveTimeoutMs, setFollowupAutoApproveTimeoutMs] = useState(undefined) // Will be set from global settings + const [timeoutFallbackEnabled, setTimeoutFallbackEnabledState] = useState(false) // Add state for timeout fallback enabled + const [toolExecutionTimeoutMs, setToolExecutionTimeoutMsState] = useState(undefined) // Will be set from global settings const [marketplaceInstalledMetadata, setMarketplaceInstalledMetadata] = useState({ project: {}, global: {}, @@ -276,6 +285,13 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode if ((newState as any).followupAutoApproveTimeoutMs !== undefined) { setFollowupAutoApproveTimeoutMs((newState as any).followupAutoApproveTimeoutMs) } + // Update timeout settings if present in state message + if ((newState as any).timeoutFallbackEnabled !== undefined) { + setTimeoutFallbackEnabledState((newState as any).timeoutFallbackEnabled) + } + if ((newState as any).toolExecutionTimeoutMs !== undefined) { + setToolExecutionTimeoutMsState((newState as any).toolExecutionTimeoutMs) + } // Handle marketplace data if present in state message if (newState.marketplaceItems !== undefined) { setMarketplaceItems(newState.marketplaceItems) @@ -375,6 +391,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode profileThresholds: state.profileThresholds ?? {}, alwaysAllowFollowupQuestions, followupAutoApproveTimeoutMs, + timeoutFallbackEnabled, + toolExecutionTimeoutMs, setExperimentEnabled: (id, enabled) => setState((prevState) => ({ ...prevState, experiments: { ...prevState.experiments, [id]: enabled } })), setApiConfiguration, @@ -469,6 +487,14 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setAlwaysAllowUpdateTodoList: (value) => { setState((prevState) => ({ ...prevState, alwaysAllowUpdateTodoList: value })) }, + setTimeoutFallbackEnabled: (value) => { + setState((prevState) => ({ ...prevState, timeoutFallbackEnabled: value })) + setTimeoutFallbackEnabledState(value) + }, + setToolExecutionTimeoutMs: (value) => { + setState((prevState) => ({ ...prevState, toolExecutionTimeoutMs: value })) + setToolExecutionTimeoutMsState(value) + }, } return {children} diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index 4c24d69f08..e7ac48dc7f 100644 --- a/webview-ui/src/i18n/locales/ca/chat.json +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -243,6 +243,11 @@ "autoSelectCountdown": "Selecció automàtica en {{count}}s", "countdownDisplay": "{{count}}s" }, + "toolTimeout": { + "title": "Temps d'espera d'eina configurat per l'usuari:", + "subtitle": "Suggerint alternatives...", + "backgroundWarning": "Nota: l'operació pot seguir executant-se en segon pla" + }, "announcement": { "title": "🎉 Roo Code {{version}} Llançat", "description": "Roo Code {{version}} porta noves funcions potents i millores significatives per millorar el vostre flux de treball de desenvolupament.", diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 15018e64ab..46bc86cc8b 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -193,6 +193,18 @@ "description": "Fes aquesta quantitat de sol·licituds API automàticament abans de demanar aprovació per continuar amb la tasca.", "unlimited": "Il·limitat" }, + "timeout": { + "label": "Temps d'espera", + "description": "Configura com Roo gestiona les operacions d'eines que superen els seus límits de temps d'espera", + "timeoutFallbackEnabled": { + "label": "Habilitar gestió de temps d'espera", + "description": "Atura automàticament les operacions d'eines de llarga durada i suggereix opcions alternatives." + }, + "toolExecutionTimeoutMs": { + "label": "Temps d'espera d'execució d'eines (ms)", + "description": "Temps màxim d'espera per a operacions d'eines abans d'activar la gestió de temps d'espera (1000-1800000ms)" + } + }, "selectOptionsFirst": "Seleccioneu almenys una opció a continuació per activar l'aprovació automàtica" }, "providers": { diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json index 8f09fab831..20103721bf 100644 --- a/webview-ui/src/i18n/locales/de/chat.json +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -243,6 +243,11 @@ "autoSelectCountdown": "Automatische Auswahl in {{count}}s", "countdownDisplay": "{{count}}s" }, + "toolTimeout": { + "title": "Benutzerdefiniertes Tool-Timeout:", + "subtitle": "Alternativen werden vorgeschlagen...", + "backgroundWarning": "Hinweis: Der Vorgang läuft möglicherweise noch im Hintergrund" + }, "announcement": { "title": "🎉 Roo Code {{version}} veröffentlicht", "description": "Roo Code {{version}} bringt mächtige neue Funktionen und bedeutende Verbesserungen, um deinen Entwicklungsworkflow zu verbessern.", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index bb5ed1146b..63411c8350 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -193,6 +193,18 @@ "description": "Automatisch so viele API-Anfragen stellen, bevor du um die Erlaubnis gebeten wirst, mit der Aufgabe fortzufahren.", "unlimited": "Unbegrenzt" }, + "timeout": { + "label": "Zeitüberschreitung", + "description": "Konfiguriere, wie Roo mit Tool-Operationen umgeht, die ihre Zeitlimits überschreiten", + "timeoutFallbackEnabled": { + "label": "Zeitüberschreitungsbehandlung aktivieren", + "description": "Automatisch lang laufende Tool-Operationen beenden und Fallback-Optionen vorschlagen." + }, + "toolExecutionTimeoutMs": { + "label": "Tool-Ausführungszeitlimit (ms)", + "description": "Maximale Wartezeit für Tool-Operationen, bevor die Zeitüberschreitungsbehandlung ausgelöst wird (1000-1800000ms)" + } + }, "selectOptionsFirst": "Wähle mindestens eine Option unten aus, um die automatische Genehmigung zu aktivieren" }, "providers": { diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index 53e529d4e4..619143b9ec 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -233,6 +233,11 @@ "hasQuestion": "Roo has a question:" }, "taskCompleted": "Task Completed", + "toolTimeout": { + "title": "User Configured Tool Timeout:", + "subtitle": "Suggesting alternatives...", + "backgroundWarning": "Note operation may still be running in the background" + }, "error": "Error", "diffError": { "title": "Edit Unsuccessful" diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 728e856502..fec165fb8c 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -191,6 +191,18 @@ "description": "Automatically make this many API requests before asking for approval to continue with the task.", "unlimited": "Unlimited" }, + "timeout": { + "label": "Timeout", + "description": "Configure how Roo handles tool operations that exceed their timeout limits", + "timeoutFallbackEnabled": { + "label": "Enable timeout handling", + "description": "Automatically timeout long-running tool operations and suggest fallback options." + }, + "toolExecutionTimeoutMs": { + "label": "Tool execution timeout (ms)", + "description": "Maximum time to wait for tool operations before triggering timeout handling (1000-1800000ms)" + } + }, "toggleAriaLabel": "Toggle auto-approval", "disabledAriaLabel": "Auto-approval disabled - select options first", "selectOptionsFirst": "Select at least one option below to enable auto-approval" diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json index bb84baa555..3f501b9d60 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -243,6 +243,11 @@ "autoSelectCountdown": "Selección automática en {{count}}s", "countdownDisplay": "{{count}}s" }, + "toolTimeout": { + "title": "Tiempo de espera de herramienta configurado por el usuario:", + "subtitle": "Sugiriendo alternativas...", + "backgroundWarning": "Nota: la operación puede seguir ejecutándose en segundo plano" + }, "announcement": { "title": "🎉 Roo Code {{version}} publicado", "description": "Roo Code {{version}} trae poderosas nuevas funcionalidades y mejoras significativas para mejorar tu flujo de trabajo de desarrollo.", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 5836933b46..06f9e0148a 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -193,6 +193,18 @@ "description": "Realizar automáticamente esta cantidad de solicitudes a la API antes de pedir aprobación para continuar con la tarea.", "unlimited": "Ilimitado" }, + "timeout": { + "label": "Tiempo de espera", + "description": "Configura cómo Roo maneja las operaciones de herramientas que exceden sus límites de tiempo", + "timeoutFallbackEnabled": { + "label": "Habilitar manejo de tiempo de espera", + "description": "Terminar automáticamente operaciones de herramientas de larga duración y sugerir opciones de respaldo." + }, + "toolExecutionTimeoutMs": { + "label": "Tiempo de espera de ejecución de herramienta (ms)", + "description": "Tiempo máximo de espera para operaciones de herramientas antes de activar el manejo de tiempo de espera (1000-1800000ms)" + } + }, "selectOptionsFirst": "Selecciona al menos una opción a continuación para habilitar la aprobación automática" }, "providers": { diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json index 70bd6011dd..749a1d4c67 100644 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -243,6 +243,11 @@ "autoSelectCountdown": "Sélection automatique dans {{count}}s", "countdownDisplay": "{{count}}s" }, + "toolTimeout": { + "title": "Délai d'expiration d'outil configuré par l'utilisateur :", + "subtitle": "Suggestion d'alternatives...", + "backgroundWarning": "Note : l'opération peut encore s'exécuter en arrière-plan" + }, "announcement": { "title": "🎉 Roo Code {{version}} est sortie", "description": "Roo Code {{version}} apporte de puissantes nouvelles fonctionnalités et des améliorations significatives pour améliorer ton flux de travail de développement.", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 833a789e5a..4f65bce735 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -193,6 +193,18 @@ "title": "Requêtes maximales", "description": "Effectuer automatiquement ce nombre de requêtes API avant de demander l'approbation pour continuer la tâche.", "unlimited": "Illimité" + }, + "timeout": { + "label": "Délai d'attente", + "description": "Configurez la façon dont Roo gère les opérations d'outils qui dépassent leurs limites de temps", + "timeoutFallbackEnabled": { + "label": "Activer la gestion des délais d'attente", + "description": "Arrêter automatiquement les opérations d'outils de longue durée et suggérer des options de secours." + }, + "toolExecutionTimeoutMs": { + "label": "Délai d'exécution des outils (ms)", + "description": "Temps d'attente maximal pour les opérations d'outils avant de déclencher la gestion des délais (1000-1800000ms)" + } } }, "providers": { diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json index 0fa95c2708..c6b6a93a3e 100644 --- a/webview-ui/src/i18n/locales/hi/chat.json +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -243,6 +243,11 @@ "autoSelectCountdown": "{{count}}s में स्वचालित रूप से चयन हो रहा है", "countdownDisplay": "{{count}}सेकंड" }, + "toolTimeout": { + "title": "उपयोगकर्ता द्वारा कॉन्फ़िगर किया गया टूल टाइमआउट:", + "subtitle": "विकल्प सुझा रहा है...", + "backgroundWarning": "नोट: ऑपरेशन अभी भी बैकग्राउंड में चल रहा हो सकता है" + }, "announcement": { "title": "🎉 Roo Code {{version}} रिलीज़ हुआ", "description": "Roo Code {{version}} आपके विकास वर्कफ़्लो को बेहतर बनाने के लिए शक्तिशाली नई सुविधाएं और महत्वपूर्ण सुधार लेकर आया है।", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 0749943508..79d83f56df 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -193,6 +193,18 @@ "description": "कार्य जारी रखने के लिए अनुमति मांगने से पहले स्वचालित रूप से इतने API अनुरोध करें।", "unlimited": "असीमित" }, + "timeout": { + "label": "टाइमआउट", + "description": "कॉन्फ़िगर करें कि Roo उन टूल ऑपरेशन्स को कैसे हैंडल करता है जो अपनी समय सीमा पार कर जाते हैं", + "timeoutFallbackEnabled": { + "label": "टाइमआउट हैंडलिंग सक्षम करें", + "description": "लंबे समय तक चलने वाले टूल ऑपरेशन्स को स्वचालित रूप से समाप्त करें और फॉलबैक विकल्प सुझाएं।" + }, + "toolExecutionTimeoutMs": { + "label": "टूल एक्जीक्यूशन टाइमआउट (ms)", + "description": "टाइमआउट हैंडलिंग ट्रिगर करने से पहले टूल ऑपरेशन्स के लिए अधिकतम प्रतीक्षा समय (1000-1800000ms)" + } + }, "selectOptionsFirst": "स्वतः-अनुमोदन सक्षम करने के लिए नीचे से कम से कम एक विकल्प चुनें" }, "providers": { diff --git a/webview-ui/src/i18n/locales/id/chat.json b/webview-ui/src/i18n/locales/id/chat.json index f8e2a5cb0e..787cf79d33 100644 --- a/webview-ui/src/i18n/locales/id/chat.json +++ b/webview-ui/src/i18n/locales/id/chat.json @@ -238,6 +238,11 @@ "questions": { "hasQuestion": "Roo punya pertanyaan:" }, + "toolTimeout": { + "title": "Timeout Tool yang Dikonfigurasi Pengguna:", + "subtitle": "Menyarankan alternatif...", + "backgroundWarning": "Catatan: operasi mungkin masih berjalan di latar belakang" + }, "taskCompleted": "Tugas Selesai", "error": "Error", "diffError": { diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 4a0c51d39d..f83193a340 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -197,6 +197,18 @@ "description": "Secara otomatis membuat sejumlah permintaan API ini sebelum meminta persetujuan untuk melanjutkan tugas.", "unlimited": "Tidak terbatas" }, + "timeout": { + "label": "Timeout", + "description": "Konfigurasi cara Roo menangani operasi tool yang melebihi batas waktu", + "timeoutFallbackEnabled": { + "label": "Aktifkan penanganan timeout", + "description": "Otomatis menghentikan operasi alat yang berjalan lama dan menyarankan opsi fallback." + }, + "toolExecutionTimeoutMs": { + "label": "Timeout eksekusi tool (ms)", + "description": "Waktu tunggu maksimum untuk operasi tool sebelum memicu penanganan timeout (1000-1800000ms)" + } + }, "selectOptionsFirst": "Pilih setidaknya satu opsi di bawah ini untuk mengaktifkan persetujuan otomatis" }, "providers": { diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json index bea63c047a..bd7457290e 100644 --- a/webview-ui/src/i18n/locales/it/chat.json +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -243,6 +243,11 @@ "autoSelectCountdown": "Selezione automatica in {{count}}s", "countdownDisplay": "{{count}}s" }, + "toolTimeout": { + "title": "Timeout Strumento Configurato dall'Utente:", + "subtitle": "Suggerendo alternative...", + "backgroundWarning": "Nota: l'operazione potrebbe essere ancora in esecuzione in background" + }, "announcement": { "title": "🎉 Rilasciato Roo Code {{version}}", "description": "Roo Code {{version}} porta nuove potenti funzionalità e miglioramenti significativi per potenziare il tuo flusso di lavoro di sviluppo.", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 9d9be82868..549b575a81 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -193,6 +193,18 @@ "description": "Esegui automaticamente questo numero di richieste API prima di chiedere l'approvazione per continuare con l'attività.", "unlimited": "Illimitato" }, + "timeout": { + "label": "Timeout", + "description": "Configura come Roo gestisce le operazioni degli strumenti che superano i loro limiti di tempo", + "timeoutFallbackEnabled": { + "label": "Abilita gestione timeout", + "description": "Termina automaticamente le operazioni degli strumenti di lunga durata e suggerisci opzioni di fallback." + }, + "toolExecutionTimeoutMs": { + "label": "Timeout esecuzione strumento (ms)", + "description": "Tempo massimo di attesa per le operazioni degli strumenti prima di attivare la gestione del timeout (1000-1800000ms)" + } + }, "selectOptionsFirst": "Seleziona almeno un'opzione qui sotto per abilitare l'approvazione automatica" }, "providers": { diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json index ca6443b3d8..16711cc448 100644 --- a/webview-ui/src/i18n/locales/ja/chat.json +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -243,6 +243,11 @@ "autoSelectCountdown": "{{count}}秒後に自動選択します", "countdownDisplay": "{{count}}秒" }, + "toolTimeout": { + "title": "ユーザー設定ツールタイムアウト:", + "subtitle": "代替案を提案中...", + "backgroundWarning": "注意: 操作がバックグラウンドで実行中の可能性があります" + }, "announcement": { "title": "🎉 Roo Code {{version}} リリース", "description": "Roo Code {{version}}は、開発ワークフローを向上させる強力な新機能と重要な改善をもたらします。", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index 9fc03cbfb1..7a2b08afea 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -193,6 +193,18 @@ "description": "タスクを続行するための承認を求める前に、自動的にこの数のAPIリクエストを行います。", "unlimited": "無制限" }, + "timeout": { + "label": "タイムアウト", + "description": "ツール操作が制限時間を超えた場合のRooの処理方法を設定", + "timeoutFallbackEnabled": { + "label": "タイムアウト処理を有効にする", + "description": "長時間実行されるツール操作を自動的に終了し、フォールバックオプションを提案します。" + }, + "toolExecutionTimeoutMs": { + "label": "ツール実行タイムアウト(ms)", + "description": "タイムアウト処理をトリガーする前のツール操作の最大待機時間(1000-1800000ms)" + } + }, "selectOptionsFirst": "自動承認を有効にするには、以下のオプションを少なくとも1つ選択してください" }, "providers": { diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json index 7e2c4467cd..14cf5fa703 100644 --- a/webview-ui/src/i18n/locales/ko/chat.json +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -243,6 +243,11 @@ "autoSelectCountdown": "{{count}}초 후 자동 선택", "countdownDisplay": "{{count}}초" }, + "toolTimeout": { + "title": "사용자 구성 도구 타임아웃:", + "subtitle": "대안을 제안하는 중...", + "backgroundWarning": "참고: 작업이 백그라운드에서 계속 실행 중일 수 있습니다" + }, "announcement": { "title": "🎉 Roo Code {{version}} 출시", "description": "Roo Code {{version}}은 개발 워크플로우를 향상시키는 강력한 새 기능과 중요한 개선사항을 제공합니다.", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 219daa05a4..b5696fb435 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -193,6 +193,18 @@ "description": "작업을 계속하기 위한 승인을 요청하기 전에 자동으로 이 수의 API 요청을 수행합니다.", "unlimited": "무제한" }, + "timeout": { + "label": "타임아웃", + "description": "도구 작업이 시간 제한을 초과할 때 Roo가 처리하는 방법을 구성", + "timeoutFallbackEnabled": { + "label": "타임아웃 처리 활성화", + "description": "장시간 실행되는 도구 작업을 자동으로 종료하고 폴백 옵션을 제안합니다." + }, + "toolExecutionTimeoutMs": { + "label": "도구 실행 타임아웃 (ms)", + "description": "타임아웃 처리를 트리거하기 전 도구 작업의 최대 대기 시간 (1000-1800000ms)" + } + }, "selectOptionsFirst": "자동 승인을 활성화하려면 아래에서 하나 이상의 옵션을 선택하세요" }, "providers": { diff --git a/webview-ui/src/i18n/locales/nl/chat.json b/webview-ui/src/i18n/locales/nl/chat.json index e123b5e8f2..2b605a148c 100644 --- a/webview-ui/src/i18n/locales/nl/chat.json +++ b/webview-ui/src/i18n/locales/nl/chat.json @@ -248,6 +248,11 @@ "errorHeader": "Context samenvatten mislukt", "tokens": "tokens" }, + "toolTimeout": { + "title": "Door gebruiker geconfigureerde tool timeout:", + "subtitle": "Alternatieven voorstellen...", + "backgroundWarning": "Let op: de bewerking kan nog steeds op de achtergrond worden uitgevoerd" + }, "followUpSuggest": { "copyToInput": "Kopiëren naar invoer (zelfde als shift + klik)", "autoSelectCountdown": "Automatische selectie in {{count}}s", diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index e184f4d85e..1ac90ca0c0 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -193,6 +193,18 @@ "description": "Voer automatisch dit aantal API-verzoeken uit voordat om goedkeuring wordt gevraagd om door te gaan met de taak.", "unlimited": "Onbeperkt" }, + "timeout": { + "label": "Timeout", + "description": "Configureer hoe Roo omgaat met tool-operaties die hun tijdslimieten overschrijden", + "timeoutFallbackEnabled": { + "label": "Timeout-afhandeling inschakelen", + "description": "Automatisch langlopende tool-operaties beëindigen en fallback-opties voorstellen." + }, + "toolExecutionTimeoutMs": { + "label": "Tool-uitvoering timeout (ms)", + "description": "Maximale wachttijd voor tool-operaties voordat timeout-afhandeling wordt geactiveerd (1000-1800000ms)" + } + }, "selectOptionsFirst": "Selecteer ten minste één optie hieronder om automatische goedkeuring in te schakelen" }, "providers": { diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json index f772256b10..d6d453edde 100644 --- a/webview-ui/src/i18n/locales/pl/chat.json +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -243,6 +243,11 @@ "autoSelectCountdown": "Automatyczny wybór za {{count}}s", "countdownDisplay": "{{count}}s" }, + "toolTimeout": { + "title": "Skonfigurowany przez użytkownika timeout narzędzia:", + "subtitle": "Sugerowanie alternatyw...", + "backgroundWarning": "Uwaga: operacja może nadal działać w tle" + }, "announcement": { "title": "🎉 Roo Code {{version}} wydany", "description": "Roo Code {{version}} wprowadza potężne nowe funkcje i znaczące ulepszenia, aby ulepszyć Twój przepływ pracy programistycznej.", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 23d3ce707d..5629ee9be7 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -193,6 +193,18 @@ "description": "Automatycznie wykonaj tyle żądań API przed poproszeniem o zgodę na kontynuowanie zadania.", "unlimited": "Bez limitu" }, + "timeout": { + "label": "Timeout", + "description": "Skonfiguruj sposób obsługi przez Roo operacji narzędzi, które przekraczają swoje limity czasowe", + "timeoutFallbackEnabled": { + "label": "Włącz obsługę timeout", + "description": "Automatycznie kończ długotrwałe operacje narzędzi i sugeruj opcje fallback." + }, + "toolExecutionTimeoutMs": { + "label": "Timeout wykonania narzędzia (ms)", + "description": "Maksymalny czas oczekiwania na operacje narzędzi przed uruchomieniem obsługi timeout (1000-1800000ms)" + } + }, "selectOptionsFirst": "Wybierz co najmniej jedną opcję poniżej, aby włączyć automatyczne zatwierdzanie" }, "providers": { diff --git a/webview-ui/src/i18n/locales/pt-BR/chat.json b/webview-ui/src/i18n/locales/pt-BR/chat.json index 08eb496d0a..6702a2f7c5 100644 --- a/webview-ui/src/i18n/locales/pt-BR/chat.json +++ b/webview-ui/src/i18n/locales/pt-BR/chat.json @@ -243,6 +243,11 @@ "autoSelectCountdown": "Seleção automática em {{count}}s", "countdownDisplay": "{{count}}s" }, + "toolTimeout": { + "title": "Timeout de Ferramenta Configurado pelo Usuário:", + "subtitle": "Sugerindo alternativas...", + "backgroundWarning": "Nota: a operação pode ainda estar executando em segundo plano" + }, "announcement": { "title": "🎉 Roo Code {{version}} Lançado", "description": "Roo Code {{version}} traz novos recursos poderosos e melhorias significativas para aprimorar seu fluxo de trabalho de desenvolvimento.", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 102036622c..d8dd74c52d 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -193,6 +193,18 @@ "description": "Fazer automaticamente este número de requisições à API antes de pedir aprovação para continuar com a tarefa.", "unlimited": "Ilimitado" }, + "timeout": { + "label": "Timeout", + "description": "Configure como o Roo lida com operações de ferramentas que excedem seus limites de tempo", + "timeoutFallbackEnabled": { + "label": "Ativar tratamento de timeout", + "description": "Terminar automaticamente operações de ferramentas de longa duração e sugerir opções de fallback." + }, + "toolExecutionTimeoutMs": { + "label": "Timeout de execução da ferramenta (ms)", + "description": "Tempo máximo de espera para operações de ferramentas antes de acionar o tratamento de timeout (1000-1800000ms)" + } + }, "selectOptionsFirst": "Selecione pelo menos uma opção abaixo para habilitar a aprovação automática" }, "providers": { diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json index 07e0501505..e6bd9e8cf3 100644 --- a/webview-ui/src/i18n/locales/ru/chat.json +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -248,6 +248,11 @@ "errorHeader": "Не удалось сжать контекст", "tokens": "токены" }, + "toolTimeout": { + "title": "Настроенный пользователем тайм-аут инструмента:", + "subtitle": "Предлагаем альтернативы...", + "backgroundWarning": "Примечание: операция может все еще выполняться в фоновом режиме" + }, "followUpSuggest": { "copyToInput": "Скопировать во ввод (то же, что shift + клик)", "autoSelectCountdown": "Автовыбор через {{count}}с", diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 5952dd8c89..91e2c8941a 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -193,6 +193,18 @@ "description": "Автоматически выполнять это количество API-запросов перед запросом разрешения на продолжение задачи.", "unlimited": "Без ограничений" }, + "timeout": { + "label": "Тайм-аут", + "description": "Настройте, как Roo обрабатывает операции инструментов, которые превышают свои временные лимиты", + "timeoutFallbackEnabled": { + "label": "Включить обработку тайм-аута", + "description": "Автоматически завершать долго выполняющиеся операции инструментов и предлагать резервные варианты." + }, + "toolExecutionTimeoutMs": { + "label": "Тайм-аут выполнения инструмента (мс)", + "description": "Максимальное время ожидания операций инструментов перед запуском обработки тайм-аута (1000-1800000мс)" + } + }, "selectOptionsFirst": "Выберите хотя бы один вариант ниже, чтобы включить автоодобрение" }, "providers": { diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json index ee16f56f72..f35e1d7957 100644 --- a/webview-ui/src/i18n/locales/tr/chat.json +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -243,6 +243,11 @@ "autoSelectCountdown": "{{count}}s içinde otomatik seçilecek", "countdownDisplay": "{{count}}sn" }, + "toolTimeout": { + "title": "Kullanıcı Tarafından Yapılandırılan Araç Zaman Aşımı:", + "subtitle": "Alternatifler öneriliyor...", + "backgroundWarning": "Not: işlem hala arka planda çalışıyor olabilir" + }, "announcement": { "title": "🎉 Roo Code {{version}} Yayınlandı", "description": "Roo Code {{version}}, geliştirme iş akışınızı geliştirmek için güçlü yeni özellikler ve önemli iyileştirmeler getiriyor.", diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 625ca4d5ea..95459f2120 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -193,6 +193,18 @@ "description": "Göreve devam etmek için onay istemeden önce bu sayıda API isteği otomatik olarak yap.", "unlimited": "Sınırsız" }, + "timeout": { + "label": "Zaman Aşımı", + "description": "Roo'nun zaman sınırlarını aşan araç işlemlerini nasıl ele aldığını yapılandırın", + "timeoutFallbackEnabled": { + "label": "Zaman aşımı işlemeyi etkinleştir", + "description": "Uzun süren araç işlemlerini otomatik olarak sonlandır ve yedek seçenekler öner." + }, + "toolExecutionTimeoutMs": { + "label": "Araç yürütme zaman aşımı (ms)", + "description": "Zaman aşımı işlemeyi tetiklemeden önce araç işlemleri için maksimum bekleme süresi (1000-1800000ms)" + } + }, "selectOptionsFirst": "Otomatik onayı etkinleştirmek için aşağıdan en az bir seçenek seçin" }, "providers": { diff --git a/webview-ui/src/i18n/locales/vi/chat.json b/webview-ui/src/i18n/locales/vi/chat.json index e56f63a91e..6be8018e49 100644 --- a/webview-ui/src/i18n/locales/vi/chat.json +++ b/webview-ui/src/i18n/locales/vi/chat.json @@ -243,6 +243,11 @@ "autoSelectCountdown": "Tự động chọn sau {{count}}s", "countdownDisplay": "{{count}}s" }, + "toolTimeout": { + "title": "Thời gian chờ công cụ do người dùng cấu hình:", + "subtitle": "Đang đề xuất các lựa chọn thay thế...", + "backgroundWarning": "Lưu ý: thao tác có thể vẫn đang chạy trong nền" + }, "announcement": { "title": "🎉 Roo Code {{version}} Đã phát hành", "description": "Roo Code {{version}} mang đến các tính năng mạnh mẽ mới và cải tiến đáng kể để nâng cao quy trình phát triển của bạn.", diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 52a9db5b93..275e0e493b 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -193,6 +193,18 @@ "description": "Tự động thực hiện số lượng API request này trước khi yêu cầu phê duyệt để tiếp tục với nhiệm vụ.", "unlimited": "Không giới hạn" }, + "timeout": { + "label": "Thời gian chờ", + "description": "Cấu hình cách Roo xử lý các thao tác công cụ vượt quá giới hạn thời gian", + "timeoutFallbackEnabled": { + "label": "Bật xử lý thời gian chờ", + "description": "Tự động kết thúc các thao tác công cụ chạy lâu và đề xuất các tùy chọn dự phòng." + }, + "toolExecutionTimeoutMs": { + "label": "Thời gian chờ thực thi công cụ (ms)", + "description": "Thời gian chờ tối đa cho các thao tác công cụ trước khi kích hoạt xử lý thời gian chờ (1000-1800000ms)" + } + }, "selectOptionsFirst": "Chọn ít nhất một tùy chọn bên dưới để bật tự động phê duyệt" }, "providers": { diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index d98dbe6f05..7746d1e2c5 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -243,6 +243,11 @@ "autoSelectCountdown": "{{count}}秒后自动选择", "countdownDisplay": "{{count}}秒" }, + "toolTimeout": { + "title": "用户配置的工具超时:", + "subtitle": "正在建议替代方案...", + "backgroundWarning": "注意: 操作可能仍在后台运行" + }, "announcement": { "title": "🎉 Roo Code {{version}} 已发布", "description": "Roo Code {{version}} 带来强大的新功能和重大改进,提升您的开发工作流程。", diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 151ee9e744..028ce0bcfb 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -193,6 +193,18 @@ "description": "在请求批准以继续执行任务之前,自动发出此数量的 API 请求。", "unlimited": "无限制" }, + "timeout": { + "label": "超时", + "description": "配置 Roo 如何处理超过时间限制的工具操作", + "timeoutFallbackEnabled": { + "label": "启用超时处理", + "description": "自动终止长时间运行的工具操作并建议备用选项。" + }, + "toolExecutionTimeoutMs": { + "label": "工具执行超时 (ms)", + "description": "触发超时处理前工具操作的最大等待时间 (1000-1800000ms)" + } + }, "selectOptionsFirst": "请至少选择以下一个选项以启用自动批准" }, "providers": { diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index e5dcd13a42..14cf7964d2 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -243,6 +243,11 @@ "autoSelectCountdown": "{{count}}秒後自動選擇", "countdownDisplay": "{{count}}秒" }, + "toolTimeout": { + "title": "使用者設定的工具逾時:", + "subtitle": "正在建議替代方案...", + "backgroundWarning": "注意: 操作可能仍在背景執行" + }, "announcement": { "title": "🎉 Roo Code {{version}} 已發布", "description": "Roo Code {{version}} 帶來強大的新功能和重大改進,提升您的開發工作流程。", diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index ab4caeea5b..8ea6ea4516 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -193,6 +193,18 @@ "description": "在請求批准以繼續執行工作之前,自動發出此數量的 API 請求。", "unlimited": "無限制" }, + "timeout": { + "label": "逾時", + "description": "設定 Roo 如何處理超過時間限制的工具操作", + "timeoutFallbackEnabled": { + "label": "啟用逾時處理", + "description": "自動終止長時間執行的工具操作並建議備用選項。" + }, + "toolExecutionTimeoutMs": { + "label": "工具執行逾時 (ms)", + "description": "觸發逾時處理前工具操作的最大等待時間 (1000-1800000ms)" + } + }, "selectOptionsFirst": "請至少選擇以下一個選項以啟用自動核准" }, "providers": {