From 28ab1c20caf860ed3815c254963098a4d287ce97 Mon Sep 17 00:00:00 2001 From: Will Li Date: Tue, 8 Jul 2025 08:48:49 -0700 Subject: [PATCH] display error and refactor --- packages/types/src/message.ts | 2 + .../timeout-fallback-responses.spec.ts | 135 ++++++++++ .../__tests__/timeout-fallback.spec.ts | 134 +++++++++ .../prompts/instructions/timeout-fallback.ts | 106 ++++++++ src/core/prompts/responses.ts | 136 ++++++++++ src/core/timeout/TimeoutFallbackGenerator.ts | 254 +----------------- src/core/timeout/TimeoutFallbackHandler.ts | 14 +- .../__tests__/timeout-integration.spec.ts | 1 + src/core/timeout/index.ts | 3 +- webview-ui/src/components/chat/ChatRow.tsx | 5 + webview-ui/src/i18n/locales/en/chat.json | 1 + 11 files changed, 543 insertions(+), 248 deletions(-) create mode 100644 src/core/prompts/__tests__/timeout-fallback-responses.spec.ts create mode 100644 src/core/prompts/instructions/__tests__/timeout-fallback.spec.ts create mode 100644 src/core/prompts/instructions/timeout-fallback.ts diff --git a/packages/types/src/message.ts b/packages/types/src/message.ts index 49d5203c66..9493ada3c3 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/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..30b53f1581 --- /dev/null +++ b/src/core/prompts/__tests__/timeout-fallback-responses.spec.ts @@ -0,0 +1,135 @@ +import { describe, it, expect } from "vitest" +import { formatResponse } from "../responses" + +describe("timeout fallback responses", () => { + 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("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("click") + expect(suggestions[0].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") + }) + + 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") + }) + + 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 d2d513db57..f3c629101b 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.`, @@ -185,6 +186,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: `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`, + }, + { + text: `Increase the timeout setting if this operation legitimately needs more time`, + }, + ] + }, + + 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: `Simplify the "${action}" into smaller, more targeted steps`, + }, + { + text: `Wait for specific elements to load before proceeding`, + }, + { + text: `Use direct API calls instead of browser automation if possible`, + }, + { + 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/TimeoutFallbackGenerator.ts b/src/core/timeout/TimeoutFallbackGenerator.ts index 2a26fd9b25..ffc1c29390 100644 --- a/src/core/timeout/TimeoutFallbackGenerator.ts +++ b/src/core/timeout/TimeoutFallbackGenerator.ts @@ -1,19 +1,12 @@ import type { ToolName } from "@roo-code/types" import type { Task } from "../task/Task" import type { SingleCompletionHandler } from "../../api" - -export interface TimeoutFallbackContext { - toolName: ToolName - timeoutMs: number - executionTimeMs: number - toolParams?: Record - errorMessage?: string - taskContext?: { - currentStep?: string - previousActions?: string[] - workingDirectory?: string - } -} +import { + createTimeoutFallbackPrompt, + parseTimeoutFallbackResponse, + type TimeoutFallbackContext, +} from "../prompts/instructions/timeout-fallback" +import { formatResponse } from "../prompts/responses" export interface TimeoutFallbackResult { success: boolean @@ -67,11 +60,11 @@ export class TimeoutFallbackGenerator { apiHandler: SingleCompletionHandler, ): Promise { try { - const prompt = this.createAiPrompt(context) + const prompt = createTimeoutFallbackPrompt(context) const aiResponse = await apiHandler.completePrompt(prompt) // Parse the AI response to extract suggestions - const suggestions = this.parseAiResponse(aiResponse) + const suggestions = parseTimeoutFallbackResponse(aiResponse) if (suggestions.length === 0) { throw new Error("No valid suggestions generated by AI") @@ -105,103 +98,14 @@ export class TimeoutFallbackGenerator { } } - /** - * Create a prompt for the AI to generate contextual timeout fallback suggestions - */ - private static createAiPrompt(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 - */ - private static parseAiResponse(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 - } - /** * Generate static fallback suggestions when AI is unavailable */ private static generateStaticToolCall(context: TimeoutFallbackContext): TimeoutFallbackResult["toolCall"] { - const suggestions = this.generateContextualSuggestions(context) + const suggestions = formatResponse.timeoutFallbackSuggestions.generateContextualSuggestions( + context.toolName, + context.toolParams, + ) const question = `The ${context.toolName} operation timed out after ${Math.round(context.timeoutMs / 1000)} seconds. How would you like to proceed?` @@ -221,138 +125,4 @@ Keep each suggestion concise (under 80 characters) and actionable.` }, } } - - /** - * Generate contextual suggestions based on tool type and parameters - */ - private static generateContextualSuggestions( - context: TimeoutFallbackContext, - ): Array<{ text: string; mode?: string }> { - const { toolName, toolParams } = context - - switch (toolName) { - case "execute_command": - return this.generateCommandSuggestions(toolParams) - case "read_file": - return this.generateReadFileSuggestions(toolParams) - case "write_to_file": - return this.generateWriteFileSuggestions(toolParams) - case "browser_action": - return this.generateBrowserSuggestions(toolParams) - case "search_files": - return this.generateSearchSuggestions(toolParams) - default: - return this.generateGenericSuggestions(context) - } - } - - private static generateCommandSuggestions(params?: Record): Array<{ text: string; mode?: string }> { - const command = params?.command || "the command" - - return [ - { - 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`, - }, - { - text: `Increase the timeout setting if this operation legitimately needs more time`, - }, - ] - } - - private static 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`, - }, - ] - } - - private static 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`, - }, - ] - } - - private static generateBrowserSuggestions(params?: Record): Array<{ text: string; mode?: string }> { - const action = params?.action || "browser action" - - return [ - { - text: `Simplify the "${action}" into smaller, more targeted steps`, - }, - { - text: `Wait for specific elements to load before proceeding`, - }, - { - text: `Use direct API calls instead of browser automation if possible`, - }, - { - text: `Reset the browser session and try again`, - }, - ] - } - - private static 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`, - }, - ] - } - - private static generateGenericSuggestions(context: TimeoutFallbackContext): Array<{ text: string; mode?: string }> { - return [ - { - text: `Break the ${context.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`, - }, - ] - } } diff --git a/src/core/timeout/TimeoutFallbackHandler.ts b/src/core/timeout/TimeoutFallbackHandler.ts index 7e3044a3d7..0fd266c440 100644 --- a/src/core/timeout/TimeoutFallbackHandler.ts +++ b/src/core/timeout/TimeoutFallbackHandler.ts @@ -1,10 +1,7 @@ import type { ToolName } from "@roo-code/types" import { formatResponse } from "../prompts/responses" -import { - TimeoutFallbackGenerator, - type TimeoutFallbackContext, - type TimeoutFallbackResult, -} from "./TimeoutFallbackGenerator" +import type { TimeoutFallbackContext } from "../prompts/instructions/timeout-fallback" +import { TimeoutFallbackGenerator, type TimeoutFallbackResult } from "./TimeoutFallbackGenerator" import type { Task } from "../task/Task" import { parseAssistantMessage } from "../assistant-message/parseAssistantMessage" @@ -24,6 +21,13 @@ export class TimeoutFallbackHandler { ): Promise { const baseResponse = formatResponse.toolTimeout(toolName, timeoutMs, executionTimeMs) + // Create a timeout message for display in the chat + if (task) { + await task.say("tool_timeout", "", undefined, false, undefined, undefined, { + isNonInteractive: true, + }) + } + // Create context for AI fallback generation const aiContext: TimeoutFallbackContext = { toolName, diff --git a/src/core/timeout/__tests__/timeout-integration.spec.ts b/src/core/timeout/__tests__/timeout-integration.spec.ts index c31a6a4134..027677500a 100644 --- a/src/core/timeout/__tests__/timeout-integration.spec.ts +++ b/src/core/timeout/__tests__/timeout-integration.spec.ts @@ -106,6 +106,7 @@ describe("Timeout Integration Tests", () => { const mockTask = { assistantMessageContent: [], cwd: "/test/dir", + say: vitest.fn().mockResolvedValue(undefined), } as unknown as Task const response = await TimeoutFallbackHandler.createTimeoutResponse( diff --git a/src/core/timeout/index.ts b/src/core/timeout/index.ts index a13e152e60..8b1f080c71 100644 --- a/src/core/timeout/index.ts +++ b/src/core/timeout/index.ts @@ -5,4 +5,5 @@ export { TimeoutFallbackGenerator } from "./TimeoutFallbackGenerator" export type { TimeoutConfig, TimeoutResult, TimeoutEvent } from "./TimeoutManager" export type { ToolExecutionOptions } from "./ToolExecutionWrapper" -export type { TimeoutFallbackContext, TimeoutFallbackResult } from "./TimeoutFallbackGenerator" +export type { TimeoutFallbackResult } from "./TimeoutFallbackGenerator" +export type { TimeoutFallbackContext } from "../prompts/instructions/timeout-fallback" diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 9c90da503f..ae19d3bb2a 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -258,6 +258,11 @@ export const ChatRowContent = ({ />, {t("chat:questions.hasQuestion")}, ] + case "tool_timeout": + return [ + , + {t("chat:toolTimeout")}, + ] default: return [null, null] } diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index ea4f8920f5..15f3039d1e 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -230,6 +230,7 @@ "hasQuestion": "Roo has a question:" }, "taskCompleted": "Task Completed", + "toolTimeout": "Tool Timeout", "error": "Error", "diffError": { "title": "Edit Unsuccessful"