Skip to content

Commit f4238a3

Browse files
committed
feat: improve error display with collapsible UI and contextual titles
- Add collapsible error display in ChatRow component with expand/collapse functionality - Implement contextual error titles for 13 different tool types - Add i18n support for error titles with fallback to generic message - Include comprehensive test coverage for new error display features - Preserve full error details in collapsed state for debugging - Add visual indicators (chevron icons) for error state management
1 parent 91f3dd9 commit f4238a3

21 files changed

+706
-86
lines changed

packages/types/src/message.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ export const clineMessageSchema = z.object({
204204
ask: clineAskSchema.optional(),
205205
say: clineSaySchema.optional(),
206206
text: z.string().optional(),
207+
title: z.string().optional(), // Custom title for error messages and other displays
207208
images: z.array(z.string()).optional(),
208209
partial: z.boolean().optional(),
209210
reasoning: z.string().optional(),

src/core/assistant-message/presentAssistantMessage.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -317,9 +317,14 @@ export async function presentAssistantMessage(cline: Task) {
317317
await cline.say(
318318
"error",
319319
`Error ${action}:\n${error.message ?? JSON.stringify(serializeError(error), null, 2)}`,
320+
undefined, // images
321+
undefined, // partial
322+
undefined, // checkpoint
323+
undefined, // progressStatus
324+
{ title: `Tool Call Error: ${block.name}` }, // Custom title with tool name
320325
)
321326

322-
pushToolResult(formatResponse.toolError(errorString))
327+
pushToolResult(formatResponse.toolError(errorString, block.name))
323328
}
324329

325330
// If block is partial, remove partial closing tag so its not
@@ -371,7 +376,7 @@ export async function presentAssistantMessage(cline: Task) {
371376
)
372377
} catch (error) {
373378
cline.consecutiveMistakeCount++
374-
pushToolResult(formatResponse.toolError(error.message))
379+
pushToolResult(formatResponse.toolError(error.message, block.name))
375380
break
376381
}
377382

@@ -410,6 +415,7 @@ export async function presentAssistantMessage(cline: Task) {
410415
pushToolResult(
411416
formatResponse.toolError(
412417
`Tool call repetition limit reached for ${block.name}. Please try a different approach.`,
418+
block.name,
413419
),
414420
)
415421
break
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { describe, it, expect } from "vitest"
2+
import { formatResponse } from "../responses"
3+
4+
describe("formatResponse.toolError", () => {
5+
it("should format error without tool name when not provided", () => {
6+
const error = "Something went wrong"
7+
const result = formatResponse.toolError(error)
8+
9+
expect(result).toBe("Tool Execution Error\n<error>\nSomething went wrong\n</error>")
10+
})
11+
12+
it("should format error with tool name when provided", () => {
13+
const error = "Invalid mode: test_mode"
14+
const toolName = "switch_mode"
15+
const result = formatResponse.toolError(error, toolName)
16+
17+
expect(result).toBe("Tool Call Error: switch_mode\n<error>\nInvalid mode: test_mode\n</error>")
18+
})
19+
20+
it("should handle undefined error message", () => {
21+
const result = formatResponse.toolError(undefined, "new_task")
22+
23+
expect(result).toBe("Tool Call Error: new_task\n<error>\nundefined\n</error>")
24+
})
25+
26+
it("should work with various tool names", () => {
27+
const testCases = [
28+
{ toolName: "write_to_file", expected: "Tool Call Error: write_to_file" },
29+
{ toolName: "execute_command", expected: "Tool Call Error: execute_command" },
30+
{ toolName: "apply_diff", expected: "Tool Call Error: apply_diff" },
31+
{ toolName: "new_task", expected: "Tool Call Error: new_task" },
32+
{ toolName: "use_mcp_tool", expected: "Tool Call Error: use_mcp_tool" },
33+
]
34+
35+
testCases.forEach(({ toolName, expected }) => {
36+
const result = formatResponse.toolError("Test error", toolName)
37+
expect(result).toContain(expected)
38+
})
39+
})
40+
41+
it("should maintain backward compatibility when tool name is not provided", () => {
42+
// This ensures existing code that doesn't pass toolName still works
43+
const error = "Legacy error"
44+
const result = formatResponse.toolError(error)
45+
46+
// Should not contain "Tool Call Error:" prefix
47+
expect(result).not.toContain("Tool Call Error:")
48+
// Should contain generic title
49+
expect(result).toContain("Tool Execution Error")
50+
})
51+
})

src/core/prompts/responses.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ export const formatResponse = {
1313
toolApprovedWithFeedback: (feedback?: string) =>
1414
`The user approved this operation and provided the following context:\n<feedback>\n${feedback}\n</feedback>`,
1515

16-
toolError: (error?: string) => `The tool execution failed with the following error:\n<error>\n${error}\n</error>`,
16+
toolError: (error?: string, toolName?: string) => {
17+
const title = toolName ? `Tool Call Error: ${toolName}` : "Tool Execution Error"
18+
return `${title}\n<error>\n${error}\n</error>`
19+
},
1720

1821
rooIgnoreError: (path: string) =>
1922
`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.`,

src/core/task/Task.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -946,6 +946,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
946946
options: {
947947
isNonInteractive?: boolean
948948
metadata?: Record<string, unknown>
949+
title?: string // Optional custom title for error messages
949950
} = {},
950951
contextCondense?: ContextCondense,
951952
): Promise<undefined> {
@@ -980,6 +981,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
980981
type: "say",
981982
say: type,
982983
text,
984+
title: options.title, // Include custom title if provided
983985
images,
984986
partial,
985987
contextCondense,
@@ -1022,6 +1024,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
10221024
type: "say",
10231025
say: type,
10241026
text,
1027+
title: options.title, // Include custom title if provided
10251028
images,
10261029
contextCondense,
10271030
metadata: options.metadata,
@@ -1045,6 +1048,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
10451048
type: "say",
10461049
say: type,
10471050
text,
1051+
title: options.title, // Include custom title if provided
10481052
images,
10491053
checkpoint,
10501054
contextCondense,
@@ -1058,8 +1062,13 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
10581062
`Roo tried to use ${toolName}${
10591063
relPath ? ` for '${relPath.toPosix()}'` : ""
10601064
} without value for required parameter '${paramName}'. Retrying...`,
1065+
undefined, // images
1066+
undefined, // partial
1067+
undefined, // checkpoint
1068+
undefined, // progressStatus
1069+
{ title: `Tool Call Error: ${toolName}` }, // Custom title for the error
10611070
)
1062-
return formatResponse.toolError(formatResponse.missingToolParameterError(paramName))
1071+
return formatResponse.toolError(formatResponse.missingToolParameterError(paramName), toolName)
10631072
}
10641073

10651074
// Start / Abort / Resume
@@ -2138,6 +2147,11 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
21382147
await this.say(
21392148
"error",
21402149
"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.",
2150+
undefined,
2151+
undefined,
2152+
undefined,
2153+
undefined,
2154+
{ title: "API Response Error" },
21412155
)
21422156

21432157
await this.addToApiConversationHistory({

src/core/tools/__tests__/insertContentTool.spec.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,15 @@ describe("insertContentTool", () => {
226226
expect(mockedFsReadFile).not.toHaveBeenCalled()
227227
expect(mockCline.consecutiveMistakeCount).toBe(1)
228228
expect(mockCline.recordToolError).toHaveBeenCalledWith("insert_content")
229-
expect(mockCline.say).toHaveBeenCalledWith("error", expect.stringContaining("non-existent file"))
229+
expect(mockCline.say).toHaveBeenCalledWith(
230+
"error",
231+
expect.stringContaining("non-existent file"),
232+
undefined,
233+
undefined,
234+
undefined,
235+
undefined,
236+
{ title: "Invalid Line Number" },
237+
)
230238
expect(mockCline.diffViewProvider.update).not.toHaveBeenCalled()
231239
expect(mockCline.diffViewProvider.pushToolWriteResult).not.toHaveBeenCalled()
232240
})

src/core/tools/applyDiffTool.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { fileExistsAtPath } from "../../utils/fs"
1313
import { RecordSource } from "../context-tracking/FileContextTrackerTypes"
1414
import { unescapeHtmlEntities } from "../../utils/text-normalization"
1515
import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
16+
import { t } from "../../i18n"
1617

1718
export async function applyDiffToolLegacy(
1819
cline: Task,
@@ -72,7 +73,7 @@ export async function applyDiffToolLegacy(
7273

7374
if (!accessAllowed) {
7475
await cline.say("rooignore_error", relPath)
75-
pushToolResult(formatResponse.toolError(formatResponse.rooIgnoreError(relPath)))
76+
pushToolResult(formatResponse.toolError(formatResponse.rooIgnoreError(relPath), "apply_diff"))
7677
return
7778
}
7879

@@ -83,7 +84,9 @@ export async function applyDiffToolLegacy(
8384
cline.consecutiveMistakeCount++
8485
cline.recordToolError("apply_diff")
8586
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>`
86-
await cline.say("error", formattedError)
87+
await cline.say("error", formattedError, undefined, undefined, undefined, undefined, {
88+
title: t("tools:errors.fileNotFound"),
89+
})
8790
pushToolResult(formattedError)
8891
return
8992
}

src/core/tools/askFollowupQuestionTool.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,16 @@ export async function askFollowupQuestionTool(
4848
} catch (error) {
4949
cline.consecutiveMistakeCount++
5050
cline.recordToolError("ask_followup_question")
51-
await cline.say("error", `Failed to parse operations: ${error.message}`)
52-
pushToolResult(formatResponse.toolError("Invalid operations xml format"))
51+
await cline.say(
52+
"error",
53+
`Failed to parse operations: ${error.message}`,
54+
undefined,
55+
undefined,
56+
undefined,
57+
undefined,
58+
{ title: "Parse Error" },
59+
)
60+
pushToolResult(formatResponse.toolError("Invalid operations xml format", "ask_followup_question"))
5361
return
5462
}
5563

src/core/tools/attemptCompletionTool.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export async function attemptCompletionTool(
4646
pushToolResult(
4747
formatResponse.toolError(
4848
"Cannot complete task while there are incomplete todos. Please finish all todos before attempting completion.",
49+
"attempt_completion",
4950
),
5051
)
5152

src/core/tools/executeCommandTool.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,12 @@ export async function executeCommandTool(
4747

4848
if (ignoredFileAttemptedToAccess) {
4949
await task.say("rooignore_error", ignoredFileAttemptedToAccess)
50-
pushToolResult(formatResponse.toolError(formatResponse.rooIgnoreError(ignoredFileAttemptedToAccess)))
50+
pushToolResult(
51+
formatResponse.toolError(
52+
formatResponse.rooIgnoreError(ignoredFileAttemptedToAccess),
53+
"execute_command",
54+
),
55+
)
5156
return
5257
}
5358

@@ -271,7 +276,15 @@ export async function executeCommand(
271276
if (isTimedOut) {
272277
const status: CommandExecutionStatus = { executionId, status: "timeout" }
273278
provider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) })
274-
await task.say("error", t("common:errors:command_timeout", { seconds: commandExecutionTimeoutSeconds }))
279+
await task.say(
280+
"error",
281+
t("common:errors:command_timeout", { seconds: commandExecutionTimeoutSeconds }),
282+
undefined,
283+
undefined,
284+
undefined,
285+
undefined,
286+
{ title: t("tools:errors.commandTimeout") },
287+
)
275288
task.terminalProcess = undefined
276289

277290
return [

0 commit comments

Comments
 (0)