Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
117 changes: 16 additions & 101 deletions webview-ui/src/components/chat/ChatRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { McpExecution } from "./McpExecution"
import { useSize } from "react-use"
import { useTranslation, Trans } from "react-i18next"
import deepEqual from "fast-deep-equal"
import { VSCodeBadge, VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { VSCodeBadge } from "@vscode/webview-ui-toolkit/react"

import type { ClineMessage } from "@roo-code/types"
import { Mode } from "@roo/modes"
Expand All @@ -14,7 +14,6 @@ import { COMMAND_OUTPUT_STRING } from "@roo/combineCommandSequences"
import { safeJsonParse } from "@roo/safeJsonParse"
import { FollowUpData, SuggestionItem } from "@roo-code/types"

import { useCopyToClipboard } from "@src/utils/clipboard"
import { useExtensionState } from "@src/context/ExtensionStateContext"
import { findMatchingResourceOrTemplate } from "@src/utils/mcp"
import { vscode } from "@src/utils/vscode"
Expand All @@ -28,8 +27,8 @@ import { MAX_IMAGES_PER_MESSAGE } from "./ChatView"
import { ToolUseBlock, ToolUseBlockHeader } from "../common/ToolUseBlock"
import UpdateTodoListToolBlock from "./UpdateTodoListToolBlock"
import CodeAccordian from "../common/CodeAccordian"
import CodeBlock from "../common/CodeBlock"
import MarkdownBlock from "../common/MarkdownBlock"
import { ErrorBanner } from "../common/ErrorBanner"
import { ReasoningBlock } from "./ReasoningBlock"
import Thumbnails from "../common/Thumbnails"
import McpResourceRow from "../mcp/McpResourceRow"
Expand Down Expand Up @@ -116,13 +115,10 @@ export const ChatRowContent = ({
const { t } = useTranslation()
const { mcpServers, alwaysAllowMcp, currentCheckpoint, mode } = useExtensionState()
const [reasoningCollapsed, setReasoningCollapsed] = useState(true)
const [isDiffErrorExpanded, setIsDiffErrorExpanded] = useState(false)
const [showCopySuccess, setShowCopySuccess] = useState(false)
const [isEditing, setIsEditing] = useState(false)
const [editedContent, setEditedContent] = useState("")
const [editMode, setEditMode] = useState<Mode>(mode || "code")
const [editImages, setEditImages] = useState<string[]>([])
const { copyWithFeedback } = useCopyToClipboard()

// Handle message events for image selection during edit mode
useEffect(() => {
Expand Down Expand Up @@ -854,92 +850,13 @@ export const ChatRowContent = ({
switch (message.say) {
case "diff_error":
return (
<div>
<div
style={{
marginTop: "0px",
overflow: "hidden",
marginBottom: "8px",
}}>
<div
style={{
borderBottom: isDiffErrorExpanded
? "1px solid var(--vscode-editorGroup-border)"
: "none",
fontWeight: "normal",
fontSize: "var(--vscode-font-size)",
color: "var(--vscode-editor-foreground)",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
cursor: "pointer",
}}
onClick={() => setIsDiffErrorExpanded(!isDiffErrorExpanded)}>
<div
style={{
display: "flex",
alignItems: "center",
gap: "10px",
flexGrow: 1,
}}>
<span
className="codicon codicon-warning"
style={{
color: "var(--vscode-editorWarning-foreground)",
opacity: 0.8,
fontSize: 16,
marginBottom: "-1.5px",
}}></span>
<span style={{ fontWeight: "bold" }}>{t("chat:diffError.title")}</span>
</div>
<div style={{ display: "flex", alignItems: "center" }}>
<VSCodeButton
appearance="icon"
style={{
padding: "3px",
height: "24px",
marginRight: "4px",
color: "var(--vscode-editor-foreground)",
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "transparent",
}}
onClick={(e) => {
e.stopPropagation()

// Call copyWithFeedback and handle the Promise
copyWithFeedback(message.text || "").then((success) => {
if (success) {
// Show checkmark
setShowCopySuccess(true)

// Reset after a brief delay
setTimeout(() => {
setShowCopySuccess(false)
}, 1000)
}
})
}}>
<span
className={`codicon codicon-${showCopySuccess ? "check" : "copy"}`}></span>
</VSCodeButton>
<span
className={`codicon codicon-chevron-${isDiffErrorExpanded ? "up" : "down"}`}></span>
</div>
</div>
{isDiffErrorExpanded && (
<div
style={{
padding: "8px",
backgroundColor: "var(--vscode-editor-background)",
borderTop: "none",
}}>
<CodeBlock source={message.text || ""} language="xml" />
</div>
)}
</div>
</div>
<ErrorBanner
title={t("chat:diffError.title")}
variant="warning"
details={message.text || ""}
defaultExpanded={false}
detailsLanguage="xml"
/>
)
case "subtask_result":
return (
Expand Down Expand Up @@ -1132,15 +1049,13 @@ export const ChatRowContent = ({
)
case "error":
return (
<>
{title && (
<div style={headerStyle}>
{icon}
{title}
</div>
)}
<p style={{ ...pStyle, color: "var(--vscode-errorForeground)" }}>{message.text}</p>
</>
<ErrorBanner
title={t("chat:error")}
variant="error"
details={message.text || ""}
defaultExpanded={false}
detailsLanguage="text"
/>
)
case "completion_result":
return (
Expand Down
156 changes: 156 additions & 0 deletions webview-ui/src/components/common/ErrorBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import React, { useState, useCallback } from "react"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { useCopyToClipboard } from "@src/utils/clipboard"
import CodeBlock from "./CodeBlock"

export type ErrorBannerVariant = "warning" | "error" | "info"

export interface ErrorBannerProps {
/** The title text to display in the banner */
title: string
/** The variant/severity of the banner */
variant?: ErrorBannerVariant
/** The codicon name for the icon (e.g., "warning", "error", "info") */
icon?: string
/** The detailed content to show when expanded */
details?: string
/** Whether the banner should be expanded by default */
defaultExpanded?: boolean
/** Optional callback when copy button is clicked */
onCopy?: () => void
/** Optional additional actions to display on the right side */
actions?: React.ReactNode
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The actions prop type is quite broad. If we know the specific use cases, could we define a more specific type? For example:

actions?: React.ReactElement<ButtonProps> | React.ReactElement<ButtonProps>[]

/** The language for syntax highlighting in the details section */
detailsLanguage?: string
}

/**
* ErrorBanner component provides a consistent, collapsible banner for displaying
* errors, warnings, and informational messages. It follows the same visual pattern
* as the diff_error implementation with a subtle, less jarring appearance.
*/
export const ErrorBanner: React.FC<ErrorBannerProps> = ({
title,
variant = "warning",
icon,
details,
defaultExpanded = false,
onCopy,
actions,
detailsLanguage = "xml",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is xml always a valid language for the CodeBlock component? Consider validating that the detailsLanguage prop value is supported, or documenting the valid options in the prop interface.

}) => {
const [isExpanded, setIsExpanded] = useState(defaultExpanded)
const [showCopySuccess, setShowCopySuccess] = useState(false)
const { copyWithFeedback } = useCopyToClipboard()

// Determine the icon to use based on variant if not explicitly provided
const iconName = icon || (variant === "error" ? "error" : variant === "info" ? "info" : "warning")

// Determine the color based on variant
const iconColor =
variant === "error"
? "var(--vscode-errorForeground)"
: variant === "info"
? "var(--vscode-charts-blue)"
: "var(--vscode-editorWarning-foreground)"

const handleToggleExpand = useCallback(() => {
setIsExpanded(!isExpanded)
}, [isExpanded])
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance optimization opportunity: Instead of including isExpanded in the dependency array, could we use the functional update pattern?

Suggested change
}, [isExpanded])
const handleToggleExpand = useCallback(() => {
setIsExpanded(prev => !prev)
}, [])

This removes the dependency and prevents unnecessary re-creation of the callback.


const handleCopy = useCallback(
async (e: React.MouseEvent) => {
e.stopPropagation()

if (details) {
const success = await copyWithFeedback(details)
if (success) {
setShowCopySuccess(true)
setTimeout(() => {
setShowCopySuccess(false)
}, 1000)
}
}

onCopy?.()
},
[details, copyWithFeedback, onCopy],
)

return (
<div
style={{
marginTop: "0px",
overflow: "hidden",
marginBottom: "8px",
}}>
<div
style={{
borderBottom: isExpanded && details ? "1px solid var(--vscode-editorGroup-border)" : "none",
fontWeight: "normal",
fontSize: "var(--vscode-font-size)",
color: "var(--vscode-editor-foreground)",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
cursor: details ? "pointer" : "default",
}}
onClick={details ? handleToggleExpand : undefined}>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we enhance accessibility here? Consider adding:

  • aria-expanded={isExpanded} to the clickable div
  • role="button" and tabIndex={0} for keyboard navigation
  • Keyboard event handlers (onKeyDown for Enter/Space)
  • aria-live="polite" for the copy success feedback

This would make the component more accessible to screen reader users.

<div
style={{
display: "flex",
alignItems: "center",
gap: "10px",
flexGrow: 1,
}}>
<span
className={`codicon codicon-${iconName}`}
style={{
color: iconColor,
opacity: 0.8,
fontSize: 16,
marginBottom: "-1.5px",
}}
/>
<span style={{ fontWeight: "bold" }}>{title}</span>
</div>
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
{actions}
{details && (
<>
<VSCodeButton
appearance="icon"
style={{
padding: "3px",
height: "24px",
color: "var(--vscode-editor-foreground)",
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "transparent",
}}
onClick={handleCopy}
aria-label="Copy details">
<span className={`codicon codicon-${showCopySuccess ? "check" : "copy"}`} />
</VSCodeButton>
<span
className={`codicon codicon-chevron-${isExpanded ? "up" : "down"}`}
aria-hidden="true"
/>
</>
)}
</div>
</div>
{isExpanded && details && (
<div
style={{
padding: "8px",
backgroundColor: "var(--vscode-editor-background)",
borderTop: "none",
}}>
<CodeBlock source={details} language={detailsLanguage} />
</div>
)}
</div>
)
}
Loading
Loading