Skip to content

Commit 47c21fb

Browse files
committed
feat: improve error display with custom titles and expandable UI
- Add metadata support to ClineMessage type for storing custom error titles - Update ChatRow component to display errors with expandable UI similar to diff_error - Add sayError helper method to Task class for convenient error reporting - Update existing error calls to use custom titles for better context - Fix test to match new error format with metadata
1 parent 0ce4e89 commit 47c21fb

File tree

7 files changed

+249
-11
lines changed

7 files changed

+249
-11
lines changed

implementation-plan.md

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# Implementation Plan: Improve Error Display in Webview
2+
3+
## Overview
4+
5+
Currently, `cline.say("error", ...)` displays error text in red. We want to improve this to look more like the "Edit Unsuccessful" display with:
6+
7+
1. A custom title that can be passed as metadata
8+
2. An expandable/collapsible UI pattern
9+
3. The error text shown when expanded
10+
11+
## Current Implementation Analysis
12+
13+
### 1. Error Display Flow
14+
15+
- **Backend (Task.ts)**: `say("error", text)` method sends error messages
16+
- **Frontend (ChatRow.tsx)**: Renders error messages with red text styling
17+
- **Diff Error Pattern**: Already implements the expandable UI pattern we want to replicate
18+
19+
### 2. Diff Error Implementation (lines 960-1048 in ChatRow.tsx)
20+
21+
The diff_error display has:
22+
23+
- Warning icon with yellow/orange color
24+
- Bold title ("Edit Unsuccessful")
25+
- Copy button
26+
- Expand/collapse chevron
27+
- Expandable content area showing the error details
28+
29+
## Implementation Steps
30+
31+
### Step 1: Extend the say method signature
32+
33+
**File**: `src/core/task/Task.ts`
34+
35+
The say method already accepts an `options` parameter with metadata support. We need to:
36+
37+
- Document that error messages can include a `title` in metadata
38+
- No changes needed to the method signature itself
39+
40+
### Step 2: Update ChatRow.tsx to handle enhanced error display
41+
42+
**File**: `webview-ui/src/components/chat/ChatRow.tsx`
43+
44+
Changes needed:
45+
46+
1. Add state for error expansion (similar to `isDiffErrorExpanded`)
47+
2. Extract metadata from error messages
48+
3. Render errors with the expandable UI pattern
49+
4. Use custom title from metadata or default to "Error"
50+
51+
### Step 3: Update translation files
52+
53+
**Files**: All files in `webview-ui/src/i18n/locales/*/chat.json`
54+
55+
Add new translation keys:
56+
57+
- `error.defaultTitle`: Default title when no custom title is provided
58+
- Keep existing `error` key for backward compatibility
59+
60+
### Step 4: Update existing error calls
61+
62+
Search for all `say("error", ...)` calls and optionally add metadata with custom titles where appropriate.
63+
64+
## Technical Details
65+
66+
### Message Structure
67+
68+
```typescript
69+
// When calling say with error and custom title:
70+
await this.say(
71+
"error",
72+
"Detailed error message here",
73+
undefined, // images
74+
false, // partial
75+
undefined, // checkpoint
76+
undefined, // progressStatus
77+
{
78+
metadata: {
79+
title: "Custom Error Title",
80+
},
81+
},
82+
)
83+
```
84+
85+
### Frontend Rendering Logic
86+
87+
```typescript
88+
// In ChatRow.tsx, for case "error":
89+
// 1. Extract title from metadata or use default
90+
// 2. Render expandable UI similar to diff_error
91+
// 3. Show error text in expanded section
92+
```
93+
94+
## Benefits
95+
96+
1. **Consistency**: Error display matches the existing "Edit Unsuccessful" pattern
97+
2. **Clarity**: Custom titles provide immediate context about the error type
98+
3. **User Experience**: Collapsible errors reduce visual clutter
99+
4. **Flexibility**: Backward compatible - existing error calls continue to work
100+
101+
## Testing Considerations
102+
103+
1. Test with errors that have custom titles
104+
2. Test with errors without custom titles (should use default)
105+
3. Test expand/collapse functionality
106+
4. Test copy button functionality
107+
5. Verify all translations work correctly
108+
109+
## Migration Strategy
110+
111+
- The implementation is backward compatible
112+
- Existing `say("error", ...)` calls will continue to work
113+
- We can gradually update error calls to include custom titles where beneficial

packages/types/src/message.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ export const clineMessageSchema = z.object({
224224
reasoning_summary: z.string().optional(),
225225
})
226226
.optional(),
227+
title: z.string().optional(), // Custom title for error messages
227228
})
228229
.optional(),
229230
})

src/core/task/Task.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1169,10 +1169,33 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
11691169
`Roo tried to use ${toolName}${
11701170
relPath ? ` for '${relPath.toPosix()}'` : ""
11711171
} without value for required parameter '${paramName}'. Retrying...`,
1172+
undefined,
1173+
undefined,
1174+
undefined,
1175+
undefined,
1176+
{
1177+
metadata: {
1178+
title: "Missing Parameter Error",
1179+
},
1180+
},
11721181
)
11731182
return formatResponse.toolError(formatResponse.missingToolParameterError(paramName))
11741183
}
11751184

1185+
/**
1186+
* Helper method to say an error with a custom title
1187+
* @param title - The title to display for the error
1188+
* @param text - The error message text
1189+
* @param images - Optional images to include
1190+
*/
1191+
async sayError(title: string, text: string, images?: string[]) {
1192+
await this.say("error", text, images, undefined, undefined, undefined, {
1193+
metadata: {
1194+
title,
1195+
},
1196+
})
1197+
}
1198+
11761199
// Lifecycle
11771200
// Start / Resume / Abort / Dispose
11781201

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,19 @@ 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+
expect.objectContaining({
237+
metadata: expect.objectContaining({
238+
title: "Invalid Line Number",
239+
}),
240+
}),
241+
)
230242
expect(mockCline.diffViewProvider.update).not.toHaveBeenCalled()
231243
expect(mockCline.diffViewProvider.pushToolWriteResult).not.toHaveBeenCalled()
232244
})

src/core/tools/applyDiffTool.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,9 @@ export async function applyDiffToolLegacy(
8383
cline.consecutiveMistakeCount++
8484
cline.recordToolError("apply_diff")
8585
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)
86+
await cline.say("error", formattedError, undefined, undefined, undefined, undefined, {
87+
metadata: { title: "File Not Found" },
88+
})
8789
pushToolResult(formattedError)
8890
return
8991
}

src/core/tools/insertContentTool.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,9 @@ export async function insertContentTool(
8787
cline.consecutiveMistakeCount++
8888
cline.recordToolError("insert_content")
8989
const formattedError = `Cannot insert content at line ${lineNumber} into a non-existent file. For new files, 'line' must be 0 (to append) or 1 (to insert at the beginning).`
90-
await cline.say("error", formattedError)
90+
await cline.say("error", formattedError, undefined, undefined, undefined, undefined, {
91+
metadata: { title: "Invalid Line Number" },
92+
})
9193
pushToolResult(formattedError)
9294
return
9395
}

webview-ui/src/components/chat/ChatRow.tsx

Lines changed: 93 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,9 @@ export const ChatRowContent = ({
120120
const { info: model } = useSelectedModel(apiConfiguration)
121121
const [reasoningCollapsed, setReasoningCollapsed] = useState(true)
122122
const [isDiffErrorExpanded, setIsDiffErrorExpanded] = useState(false)
123+
const [isErrorExpanded, setIsErrorExpanded] = useState(false)
123124
const [showCopySuccess, setShowCopySuccess] = useState(false)
125+
const [showErrorCopySuccess, setShowErrorCopySuccess] = useState(false)
124126
const [isEditing, setIsEditing] = useState(false)
125127
const [editedContent, setEditedContent] = useState("")
126128
const [editMode, setEditMode] = useState<Mode>(mode || "code")
@@ -1243,16 +1245,99 @@ export const ChatRowContent = ({
12431245
</div>
12441246
)
12451247
case "error":
1248+
// Extract custom title from metadata if available
1249+
const errorTitle = (message as any).metadata?.title || t("chat:error")
1250+
12461251
return (
1247-
<>
1248-
{title && (
1249-
<div style={headerStyle}>
1250-
{icon}
1251-
{title}
1252+
<div>
1253+
<div
1254+
style={{
1255+
marginTop: "0px",
1256+
overflow: "hidden",
1257+
marginBottom: "8px",
1258+
}}>
1259+
<div
1260+
style={{
1261+
borderBottom: isErrorExpanded
1262+
? "1px solid var(--vscode-editorGroup-border)"
1263+
: "none",
1264+
fontWeight: "normal",
1265+
fontSize: "var(--vscode-font-size)",
1266+
color: "var(--vscode-editor-foreground)",
1267+
display: "flex",
1268+
alignItems: "center",
1269+
justifyContent: "space-between",
1270+
cursor: "pointer",
1271+
}}
1272+
onClick={() => setIsErrorExpanded(!isErrorExpanded)}>
1273+
<div
1274+
style={{
1275+
display: "flex",
1276+
alignItems: "center",
1277+
gap: "10px",
1278+
flexGrow: 1,
1279+
}}>
1280+
<span
1281+
className="codicon codicon-error"
1282+
style={{
1283+
color: "var(--vscode-errorForeground)",
1284+
fontSize: 16,
1285+
marginBottom: "-1.5px",
1286+
}}></span>
1287+
<span style={{ fontWeight: "bold", color: "var(--vscode-errorForeground)" }}>
1288+
{errorTitle}
1289+
</span>
1290+
</div>
1291+
<div style={{ display: "flex", alignItems: "center" }}>
1292+
<VSCodeButton
1293+
appearance="icon"
1294+
style={{
1295+
padding: "3px",
1296+
height: "24px",
1297+
marginRight: "4px",
1298+
color: "var(--vscode-editor-foreground)",
1299+
display: "flex",
1300+
alignItems: "center",
1301+
justifyContent: "center",
1302+
background: "transparent",
1303+
}}
1304+
onClick={(e) => {
1305+
e.stopPropagation()
1306+
1307+
// Call copyWithFeedback and handle the Promise
1308+
copyWithFeedback(message.text || "").then((success) => {
1309+
if (success) {
1310+
// Show checkmark
1311+
setShowErrorCopySuccess(true)
1312+
1313+
// Reset after a brief delay
1314+
setTimeout(() => {
1315+
setShowErrorCopySuccess(false)
1316+
}, 1000)
1317+
}
1318+
})
1319+
}}>
1320+
<span
1321+
className={`codicon codicon-${showErrorCopySuccess ? "check" : "copy"}`}></span>
1322+
</VSCodeButton>
1323+
<span
1324+
className={`codicon codicon-chevron-${isErrorExpanded ? "up" : "down"}`}></span>
1325+
</div>
12521326
</div>
1253-
)}
1254-
<p style={{ ...pStyle, color: "var(--vscode-errorForeground)" }}>{message.text}</p>
1255-
</>
1327+
{isErrorExpanded && (
1328+
<div
1329+
style={{
1330+
padding: "8px",
1331+
backgroundColor: "var(--vscode-editor-background)",
1332+
borderTop: "none",
1333+
}}>
1334+
<p style={{ ...pStyle, color: "var(--vscode-errorForeground)", margin: 0 }}>
1335+
{message.text}
1336+
</p>
1337+
</div>
1338+
)}
1339+
</div>
1340+
</div>
12561341
)
12571342
case "completion_result":
12581343
return (

0 commit comments

Comments
 (0)