Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
9d3c026
feat: improve error display with collapsible UI and contextual titles
hannesrudolph Aug 25, 2025
446bafb
fix: resolve failing tests and add missing translations
hannesrudolph Aug 25, 2025
44e4fee
refactor: address code review feedback for error display
hannesrudolph Aug 25, 2025
a0d37f9
fix: correct i18n key format and remove dead error icon/title code
hannesrudolph Aug 26, 2025
3ff0823
fix: convert inline styles to Tailwind classes and expand i18n error …
hannesrudolph Aug 26, 2025
b9ccbe5
fix: add missing error translations to all locales
hannesrudolph Aug 26, 2025
fb0296c
chore: remove accidentally committed temp files
hannesrudolph Aug 26, 2025
12d07db
staged changes after revert to 4517eb7
hannesrudolph Sep 3, 2025
7d006ba
chore(i18n): sentence case error titles ('File not found', 'File alre…
hannesrudolph Sep 3, 2025
04db47b
chore(i18n): sentence case for error titles (tool and generic); align…
hannesrudolph Sep 3, 2025
bacedd6
chore(i18n): add missing en/tools keys (executeCommand, fetchInstruct…
hannesrudolph Sep 3, 2025
72c79ce
chore(i18n): use t() for tool messages; add generic noChanges/changes…
hannesrudolph Sep 3, 2025
e2592f0
test(i18n): align en/tools messages with tests (noChanges includes pa…
hannesrudolph Sep 3, 2025
87b777a
fix(i18n): parameterized noChanges; title-case error titles; update t…
hannesrudolph Sep 4, 2025
9b0cdcd
fix(i18n): unify keys at tools root; use t('tools:noChanges'|'tools:c…
hannesrudolph Sep 4, 2025
d900b77
test: align insertContentTool error title with spec
hannesrudolph Sep 5, 2025
1734b67
fix: align error strings with unit tests (insertContent/attemptComple…
hannesrudolph Sep 5, 2025
b5a93fe
test(webview-ui): wrap ChatRow tests with QueryClientProvider to fix …
hannesrudolph Sep 5, 2025
3c60c6e
fix(prompts): title-case tool error headers and align en i18n keys to…
hannesrudolph Sep 5, 2025
d938fb6
fix(list-files): surface top-level symlinked files in non-recursive m…
hannesrudolph Sep 5, 2025
b20c4d8
fix(list-files): always pass --follow to ripgrep and include top-leve…
hannesrudolph Sep 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/types/src/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ export const clineMessageSchema = z.object({
ask: clineAskSchema.optional(),
say: clineSaySchema.optional(),
text: z.string().optional(),
title: z.string().optional(), // Custom title for error messages and other displays
images: z.array(z.string()).optional(),
partial: z.boolean().optional(),
reasoning: z.string().optional(),
Expand Down
11 changes: 9 additions & 2 deletions src/core/assistant-message/presentAssistantMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { Task } from "../task/Task"
import { codebaseSearchTool } from "../tools/codebaseSearchTool"
import { experiments, EXPERIMENT_IDS } from "../../shared/experiments"
import { applyDiffToolLegacy } from "../tools/applyDiffTool"
import { t } from "../../i18n"

/**
* Processes and presents assistant message content to the user interface.
Expand Down Expand Up @@ -323,9 +324,14 @@ export async function presentAssistantMessage(cline: Task) {
await cline.say(
"error",
`Error ${action}:\n${error.message ?? JSON.stringify(serializeError(error), null, 2)}`,
undefined, // images
undefined, // partial
undefined, // checkpoint
undefined, // progressStatus
{ title: t("tools:errors.toolCallError", { toolName: block.name }) }, // Custom title with tool name
)

pushToolResult(formatResponse.toolError(errorString))
pushToolResult(formatResponse.toolError(errorString, block.name))
}

// If block is partial, remove partial closing tag so its not
Expand Down Expand Up @@ -377,7 +383,7 @@ export async function presentAssistantMessage(cline: Task) {
)
} catch (error) {
cline.consecutiveMistakeCount++
pushToolResult(formatResponse.toolError(error.message))
pushToolResult(formatResponse.toolError(error.message, block.name))
break
}

Expand Down Expand Up @@ -416,6 +422,7 @@ export async function presentAssistantMessage(cline: Task) {
pushToolResult(
formatResponse.toolError(
`Tool call repetition limit reached for ${block.name}. Please try a different approach.`,
block.name,
),
)
break
Expand Down
51 changes: 51 additions & 0 deletions src/core/prompts/__tests__/responses-tool-error.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { describe, it, expect } from "vitest"
import { formatResponse } from "../responses"

describe("formatResponse.toolError", () => {
it("should format error without tool name when not provided", () => {
const error = "Something went wrong"
const result = formatResponse.toolError(error)

expect(result).toBe("Tool Execution Error\n<error>\nSomething went wrong\n</error>")
})

it("should format error with tool name when provided", () => {
const error = "Invalid mode: test_mode"
const toolName = "switch_mode"
const result = formatResponse.toolError(error, toolName)

expect(result).toBe("Tool Call Error: switch_mode\n<error>\nInvalid mode: test_mode\n</error>")
})

it("should handle undefined error message", () => {
const result = formatResponse.toolError(undefined, "new_task")

expect(result).toBe("Tool Call Error: new_task\n<error>\nundefined\n</error>")
})

it("should work with various tool names", () => {
const testCases = [
{ toolName: "write_to_file", expected: "Tool Call Error: write_to_file" },
{ toolName: "execute_command", expected: "Tool Call Error: execute_command" },
{ toolName: "apply_diff", expected: "Tool Call Error: apply_diff" },
{ toolName: "new_task", expected: "Tool Call Error: new_task" },
{ toolName: "use_mcp_tool", expected: "Tool Call Error: use_mcp_tool" },
]

testCases.forEach(({ toolName, expected }) => {
const result = formatResponse.toolError("Test error", toolName)
expect(result).toContain(expected)
})
})

it("should maintain backward compatibility when tool name is not provided", () => {
// This ensures existing code that doesn't pass toolName still works
const error = "Legacy error"
const result = formatResponse.toolError(error)

// Should not contain "Tool Call Error:" prefix
expect(result).not.toContain("Tool Call Error:")
// Should contain generic title
expect(result).toContain("Tool Execution Error")
})
})
8 changes: 7 additions & 1 deletion src/core/prompts/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { t } from "../../i18n"

export const formatResponse = {
toolDenied: () => `The user denied this operation.`,
Expand All @@ -13,7 +14,12 @@ export const formatResponse = {
toolApprovedWithFeedback: (feedback?: string) =>
`The user approved this operation and provided the following context:\n<feedback>\n${feedback}\n</feedback>`,

toolError: (error?: string) => `The tool execution failed with the following error:\n<error>\n${error}\n</error>`,
toolError: (error?: string, toolName?: string) => {
const title = toolName
? t("tools:errors.toolCallError", { toolName, defaultValue: `Tool Call Error: ${toolName}` })
: t("tools:errors.toolExecutionError", { defaultValue: "Tool Execution Error" })
return `${title}\n<error>\n${error}\n</error>`
},

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.`,
Expand Down
28 changes: 24 additions & 4 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1052,6 +1052,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
options: {
isNonInteractive?: boolean
metadata?: Record<string, unknown>
title?: string // Optional custom title for error messages
} = {},
contextCondense?: ContextCondense,
): Promise<undefined> {
Expand Down Expand Up @@ -1086,6 +1087,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
type: "say",
say: type,
text,
title: options.title, // Include custom title if provided
images,
partial,
contextCondense,
Expand Down Expand Up @@ -1133,6 +1135,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
type: "say",
say: type,
text,
title: options.title, // Include custom title if provided
images,
contextCondense,
metadata: options.metadata,
Expand All @@ -1156,6 +1159,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
type: "say",
say: type,
text,
title: options.title, // Include custom title if provided
images,
checkpoint,
contextCondense,
Expand All @@ -1164,13 +1168,24 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
}

async sayAndCreateMissingParamError(toolName: ToolName, paramName: string, relPath?: string) {
const message = relPath
? t("tools:errors.missingParamForToolWithPath", {
toolName,
relPath: relPath.toPosix(),
paramName,
})
: t("tools:errors.missingParamForTool", { toolName, paramName })

await this.say(
"error",
`Roo tried to use ${toolName}${
relPath ? ` for '${relPath.toPosix()}'` : ""
} without value for required parameter '${paramName}'. Retrying...`,
message,
undefined, // images
undefined, // partial
undefined, // checkpoint
undefined, // progressStatus
{ title: t("tools:errors.toolCallError", { toolName }) }, // Custom title for the error
)
return formatResponse.toolError(formatResponse.missingToolParameterError(paramName))
return formatResponse.toolError(formatResponse.missingToolParameterError(paramName), toolName)
}

// Lifecycle
Expand Down Expand Up @@ -2314,6 +2329,11 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
await this.say(
"error",
"Unexpected API Response: The language model did not provide any assistant messages. This may indicate an issue with the API or the model's output.",
undefined,
undefined,
undefined,
undefined,
{ title: "API Response Error" },
)

await this.addToApiConversationHistory({
Expand Down
10 changes: 9 additions & 1 deletion src/core/tools/__tests__/insertContentTool.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,15 @@ describe("insertContentTool", () => {
expect(mockedFsReadFile).not.toHaveBeenCalled()
expect(mockCline.consecutiveMistakeCount).toBe(1)
expect(mockCline.recordToolError).toHaveBeenCalledWith("insert_content")
expect(mockCline.say).toHaveBeenCalledWith("error", expect.stringContaining("non-existent file"))
expect(mockCline.say).toHaveBeenCalledWith(
"error",
expect.stringContaining("non-existent file"),
undefined,
undefined,
undefined,
undefined,
{ title: "Invalid Line Number" },
)
expect(mockCline.diffViewProvider.update).not.toHaveBeenCalled()
expect(mockCline.diffViewProvider.pushToolWriteResult).not.toHaveBeenCalled()
})
Expand Down
21 changes: 18 additions & 3 deletions src/core/tools/__tests__/useMcpToolTool.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ import { ToolUse } from "../../../shared/tools"
vi.mock("../../prompts/responses", () => ({
formatResponse: {
toolResult: vi.fn((result: string) => `Tool result: ${result}`),
toolError: vi.fn((error: string) => `Tool error: ${error}`),
toolError: vi.fn((error: string, toolName?: string) => {
if (toolName) {
return `Tool Call Error: ${toolName}\n<error>\n${error}\n</error>`
}
return `Tool Execution Error\n<error>\n${error}\n</error>`
}),
invalidMcpToolArgumentError: vi.fn((server: string, tool: string) => `Invalid args for ${server}:${tool}`),
unknownMcpToolError: vi.fn((server: string, tool: string, availableTools: string[]) => {
const toolsList = availableTools.length > 0 ? availableTools.join(", ") : "No tools available"
Expand Down Expand Up @@ -151,8 +156,18 @@ describe("useMcpToolTool", () => {

expect(mockTask.consecutiveMistakeCount).toBe(1)
expect(mockTask.recordToolError).toHaveBeenCalledWith("use_mcp_tool")
expect(mockTask.say).toHaveBeenCalledWith("error", expect.stringContaining("invalid JSON argument"))
expect(mockPushToolResult).toHaveBeenCalledWith("Tool error: Invalid args for test_server:test_tool")
expect(mockTask.say).toHaveBeenCalledWith(
"error",
expect.stringContaining("invalid JSON argument"),
undefined,
undefined,
undefined,
undefined,
{ title: "tools:errors.invalidInput" },
)
expect(mockPushToolResult).toHaveBeenCalledWith(
"Tool Call Error: use_mcp_tool\n<error>\nInvalid args for test_server:test_tool\n</error>",
)
})
})

Expand Down
7 changes: 5 additions & 2 deletions src/core/tools/applyDiffTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { fileExistsAtPath } from "../../utils/fs"
import { RecordSource } from "../context-tracking/FileContextTrackerTypes"
import { unescapeHtmlEntities } from "../../utils/text-normalization"
import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
import { t } from "../../i18n"

export async function applyDiffToolLegacy(
cline: Task,
Expand Down Expand Up @@ -72,7 +73,7 @@ export async function applyDiffToolLegacy(

if (!accessAllowed) {
await cline.say("rooignore_error", relPath)
pushToolResult(formatResponse.toolError(formatResponse.rooIgnoreError(relPath)))
pushToolResult(formatResponse.toolError(formatResponse.rooIgnoreError(relPath), "apply_diff"))
return
}

Expand All @@ -83,7 +84,9 @@ export async function applyDiffToolLegacy(
cline.consecutiveMistakeCount++
cline.recordToolError("apply_diff")
const formattedError = `File does not exist at path: ${absolutePath}\n\n<error_details>\nThe specified file could not be found. Please verify the file path and try again.\n</error_details>`
await cline.say("error", formattedError)
await cline.say("error", formattedError, undefined, undefined, undefined, undefined, {
title: t("tools:errors.fileNotFound"),
})
pushToolResult(formattedError)
return
}
Expand Down
13 changes: 11 additions & 2 deletions src/core/tools/askFollowupQuestionTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Task } from "../task/Task"
import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools"
import { formatResponse } from "../prompts/responses"
import { parseXml } from "../../utils/xml"
import { t } from "../../i18n"

export async function askFollowupQuestionTool(
cline: Task,
Expand Down Expand Up @@ -48,8 +49,16 @@ export async function askFollowupQuestionTool(
} catch (error) {
cline.consecutiveMistakeCount++
cline.recordToolError("ask_followup_question")
await cline.say("error", `Failed to parse operations: ${error.message}`)
pushToolResult(formatResponse.toolError("Invalid operations xml format"))
await cline.say(
"error",
`Failed to parse operations: ${error.message}`,
undefined,
undefined,
undefined,
undefined,
{ title: t("tools:errors.parseError") },
)
pushToolResult(formatResponse.toolError("Invalid operations xml format", "ask_followup_question"))
return
}

Expand Down
6 changes: 4 additions & 2 deletions src/core/tools/attemptCompletionTool.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Anthropic from "@anthropic-ai/sdk"
import * as vscode from "vscode"
import { t } from "../../i18n"

import { RooCodeEventName } from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
Expand Down Expand Up @@ -45,7 +46,8 @@ export async function attemptCompletionTool(

pushToolResult(
formatResponse.toolError(
"Cannot complete task while there are incomplete todos. Please finish all todos before attempting completion.",
"Cannot complete task while there are incomplete todos. Please complete all todos before attempting completion.",
"attempt_completion",
),
)

Expand Down Expand Up @@ -125,7 +127,7 @@ export async function attemptCompletionTool(

toolResults.push({
type: "text",
text: `The user has provided feedback on the results. Consider their input to continue the task, and then attempt completion again.\n<feedback>\n${text}\n</feedback>`,
text: `${t("tools:attemptCompletion.userFeedbackLead")}\n<feedback>\n${text}\n</feedback>`,
})

toolResults.push(...formatResponse.imageBlocks(images))
Expand Down
19 changes: 16 additions & 3 deletions src/core/tools/executeCommandTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,12 @@ export async function executeCommandTool(

if (ignoredFileAttemptedToAccess) {
await task.say("rooignore_error", ignoredFileAttemptedToAccess)
pushToolResult(formatResponse.toolError(formatResponse.rooIgnoreError(ignoredFileAttemptedToAccess)))
pushToolResult(
formatResponse.toolError(
formatResponse.rooIgnoreError(ignoredFileAttemptedToAccess),
"execute_command",
),
)
return
}

Expand Down Expand Up @@ -121,7 +126,7 @@ export async function executeCommandTool(

pushToolResult(result)
} else {
pushToolResult(`Command failed to execute in terminal due to a shell integration error.`)
pushToolResult(t("tools:executeCommand.shellIntegrationGenericError"))
}
}

Expand Down Expand Up @@ -271,7 +276,15 @@ export async function executeCommand(
if (isTimedOut) {
const status: CommandExecutionStatus = { executionId, status: "timeout" }
provider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) })
await task.say("error", t("common:errors:command_timeout", { seconds: commandExecutionTimeoutSeconds }))
await task.say(
"error",
t("common:errors.command_timeout", { seconds: commandExecutionTimeoutSeconds }),
undefined,
undefined,
undefined,
undefined,
{ title: t("tools:errors.commandTimeout") },
)
task.terminalProcess = undefined

return [
Expand Down
8 changes: 7 additions & 1 deletion src/core/tools/fetchInstructionsTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { fetchInstructions } from "../prompts/instructions/instructions"
import { ClineSayTool } from "../../shared/ExtensionMessage"
import { formatResponse } from "../prompts/responses"
import { ToolUse, AskApproval, HandleError, PushToolResult } from "../../shared/tools"
import { t } from "../../i18n"

export async function fetchInstructionsTool(
cline: Task,
Expand Down Expand Up @@ -49,7 +50,12 @@ export async function fetchInstructionsTool(
const content = await fetchInstructions(task, { mcpHub, diffStrategy, context })

if (!content) {
pushToolResult(formatResponse.toolError(`Invalid instructions request: ${task}`))
pushToolResult(
formatResponse.toolError(
t("tools:fetchInstructions.errors.invalidRequest", { defaultValue: "Invalid request" }),
"fetch_instructions",
),
)
return
}

Expand Down
Loading
Loading