Skip to content
Closed
151 changes: 74 additions & 77 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 @@ -20,6 +20,7 @@ import { findMatchingResourceOrTemplate } from "@src/utils/mcp"
import { vscode } from "@src/utils/vscode"
import { removeLeadingNonAlphanumeric } from "@src/utils/removeLeadingNonAlphanumeric"
import { getLanguageFromPath } from "@src/utils/getLanguageFromPath"
import { extractErrorTitle } from "@src/utils/errorTitleExtractor"
import { Button } from "@src/components/ui"

import ChatTextArea from "./ChatTextArea"
Expand All @@ -34,6 +35,8 @@ import { ReasoningBlock } from "./ReasoningBlock"
import Thumbnails from "../common/Thumbnails"
import McpResourceRow from "../mcp/McpResourceRow"

import { DisclosureHeader } from "./DisclosureHeader"

import { Mention } from "./Mention"
import { CheckpointSaved } from "./checkpoints/CheckpointSaved"
import { FollowUpSuggest } from "./FollowUpSuggest"
Expand Down Expand Up @@ -118,6 +121,8 @@ export const ChatRowContent = ({
const [reasoningCollapsed, setReasoningCollapsed] = useState(true)
const [isDiffErrorExpanded, setIsDiffErrorExpanded] = useState(false)
const [showCopySuccess, setShowCopySuccess] = useState(false)
const [isErrorExpanded, setIsErrorExpanded] = useState(false)
const [showErrorCopySuccess, setShowErrorCopySuccess] = useState(false)
const [isEditing, setIsEditing] = useState(false)
const [editedContent, setEditedContent] = useState("")
const [editMode, setEditMode] = useState<Mode>(mode || "code")
Expand Down Expand Up @@ -861,75 +866,29 @@ export const ChatRowContent = ({
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",
<DisclosureHeader
contentId={`diff-error-${message.ts}`}
iconClass="codicon-warning"
iconStyle={{ color: "var(--vscode-editorWarning-foreground)", opacity: 0.8 }}
title={<span style={{ fontWeight: "bold" }}>{t("chat:diffError.title")}</span>}
expanded={isDiffErrorExpanded}
onToggle={() => setIsDiffErrorExpanded(!isDiffErrorExpanded)}
onCopy={() => {
copyWithFeedback(message.text || "").then((success) => {
if (success) {
setShowCopySuccess(true)
setTimeout(() => {
setShowCopySuccess(false)
}, 1000)
}
})
}}
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>
copyTitle={t("chat:codeblock.tooltips.copy_code")}
copyIconClass={showCopySuccess ? "codicon-check" : "codicon-copy"}
/>
{isDiffErrorExpanded && (
<div
id={`diff-error-${message.ts}`}
style={{
padding: "8px",
backgroundColor: "var(--vscode-editor-background)",
Expand Down Expand Up @@ -1130,18 +1089,56 @@ export const ChatRowContent = ({
/>
</div>
)
case "error":
case "error": {
// Extract error title from the message text using the comprehensive extractor
const errorContent = message.text || ""
const errorTitle = extractErrorTitle(errorContent, t)

return (
<>
{title && (
<div style={headerStyle}>
{icon}
{title}
</div>
)}
<p style={{ ...pStyle, color: "var(--vscode-errorForeground)" }}>{message.text}</p>
</>
<div>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I notice there's significant code duplication between the error and diff_error implementations. Both share nearly identical structure for the collapsible UI, copy functionality, and expand/collapse behavior. Could we consider extracting a reusable component like to reduce this duplication and make future maintenance easier?

<div
style={{
marginTop: "0px",
overflow: "hidden",
marginBottom: "8px",
}}>
<DisclosureHeader
contentId={`error-${message.ts}`}
iconClass="codicon-warning"
iconStyle={{ color: "var(--vscode-editorWarning-foreground)", opacity: 0.8 }}
title={<span style={{ fontWeight: "bold" }}>{errorTitle}</span>}
expanded={isErrorExpanded}
onToggle={() => setIsErrorExpanded(!isErrorExpanded)}
onCopy={() => {
copyWithFeedback(message.text || "").then((success) => {
if (success) {
setShowErrorCopySuccess(true)
setTimeout(() => {
setShowErrorCopySuccess(false)
}, 1000)
}
})
}}
copyTitle={t("chat:codeblock.tooltips.copy_code")}
copyIconClass={showErrorCopySuccess ? "codicon-check" : "codicon-copy"}
/>
{isErrorExpanded && (
<div
id={`error-${message.ts}`}
style={{
padding: "8px",
backgroundColor: "var(--vscode-editor-background)",
borderTop: "none",
}}>
<p style={{ ...pStyle, color: "var(--vscode-errorForeground)" }}>
{errorContent}
</p>
</div>
)}
</div>
</div>
)
}
case "completion_result":
return (
<>
Expand Down
73 changes: 73 additions & 0 deletions webview-ui/src/components/chat/DisclosureHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React from "react"
import { cn } from "@/lib/utils"
import { IconButton } from "./IconButton"

interface DisclosureHeaderProps {
contentId: string
iconClass: string
iconStyle?: React.CSSProperties
title: React.ReactNode
expanded: boolean
onToggle: () => void
onCopy?: (e: React.MouseEvent) => void
copyTitle?: string
copyIconClass?: string // e.g. "codicon-copy" | "codicon-check"
className?: string
}

export const DisclosureHeader: React.FC<DisclosureHeaderProps> = ({
contentId,
iconClass,
iconStyle,
title,
expanded,
onToggle,
onCopy,
copyTitle,
copyIconClass,
className,
}) => {
return (
<div
className={cn("flex items-center justify-between", className)}
style={{
fontWeight: "normal",
fontSize: "var(--vscode-font-size)",
color: "var(--vscode-editor-foreground)",
borderBottom: expanded ? "1px solid var(--vscode-editorGroup-border)" : "none",
}}>
<button
type="button"
aria-expanded={expanded}
aria-controls={contentId}
onClick={onToggle}
className={cn(
"flex items-center justify-between gap-2",
"bg-transparent border-0 p-0 m-0 cursor-pointer",
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
)}
style={{ width: "100%", textAlign: "left" }}>
<div style={{ display: "flex", alignItems: "center", gap: "10px", flexGrow: 1 }}>
<span
className={cn("codicon", iconClass)}
style={{ fontSize: 16, marginBottom: "-1.5px", ...iconStyle }}
/>
<span>{title}</span>
</div>
<span className={`codicon codicon-chevron-${expanded ? "down" : "right"}`} />
</button>

{onCopy && (
<IconButton
iconClass={copyIconClass ?? "codicon-copy"}
title={copyTitle ?? "Copy"}
Copy link
Contributor

Choose a reason for hiding this comment

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

The default copy title is hardcoded as 'Copy'. Consider using a translatable string (via an i18n function) instead of a literal string to adhere to internationalization guidelines.

Suggested change
title={copyTitle ?? "Copy"}
title={copyTitle ?? t("copy")}

This comment was generated because it violated a code review rule: irule_C0ez7Rji6ANcGkkX.

onClick={(e) => {
e.stopPropagation()
onCopy(e)
}}
style={{ marginLeft: 4 }}
/>
)}
</div>
)
}
Loading
Loading