import { VSCodeBadge, VSCodeButton, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react" import deepEqual from "fast-deep-equal" import React, { memo, useEffect, useMemo, useRef, useState } from "react" import { useSize } from "react-use" import { useCopyToClipboard } from "../../utils/clipboard" import { useTranslation, Trans } from "react-i18next" import { ClineApiReqInfo, ClineAskUseMcpServer, ClineMessage, ClineSayTool, } from "../../../../src/shared/ExtensionMessage" import { COMMAND_OUTPUT_STRING } from "../../../../src/shared/combineCommandSequences" import { useExtensionState } from "../../context/ExtensionStateContext" import { findMatchingResourceOrTemplate } from "../../utils/mcp" import { vscode } from "../../utils/vscode" import CodeAccordian, { removeLeadingNonAlphanumeric } from "../common/CodeAccordian" import CodeBlock, { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock" import MarkdownBlock from "../common/MarkdownBlock" import { ReasoningBlock } from "./ReasoningBlock" import Thumbnails from "../common/Thumbnails" import McpResourceRow from "../mcp/McpResourceRow" import McpToolRow from "../mcp/McpToolRow" import { highlightMentions } from "./TaskHeader" import { CheckpointSaved } from "./checkpoints/CheckpointSaved" import FollowUpSuggest from "./FollowUpSuggest" interface ChatRowProps { message: ClineMessage lastModifiedMessage?: ClineMessage isExpanded: boolean isLast: boolean isStreaming: boolean onToggleExpand: () => void onHeightChange: (isTaller: boolean) => void onSuggestionClick?: (answer: string) => void } interface ChatRowContentProps extends Omit {} const ChatRow = memo( (props: ChatRowProps) => { const { isLast, onHeightChange, message } = props // Store the previous height to compare with the current height // This allows us to detect changes without causing re-renders const prevHeightRef = useRef(0) const [chatrow, { height }] = useSize(
, ) useEffect(() => { // used for partials, command output, etc. // NOTE: it's important we don't distinguish between partial or complete here since our scroll effects in chatview need to handle height change during partial -> complete const isInitialRender = prevHeightRef.current === 0 // prevents scrolling when new element is added since we already scroll for that // height starts off at Infinity if (isLast && height !== 0 && height !== Infinity && height !== prevHeightRef.current) { if (!isInitialRender) { onHeightChange(height > prevHeightRef.current) } prevHeightRef.current = height } }, [height, isLast, onHeightChange, message]) // we cannot return null as virtuoso does not support it, so we use a separate visibleMessages array to filter out messages that should not be rendered return chatrow }, // memo does shallow comparison of props, so we need to do deep comparison of arrays/objects whose properties might change deepEqual, ) export default ChatRow export const ChatRowContent = ({ message, lastModifiedMessage, isExpanded, isLast, isStreaming, onToggleExpand, onSuggestionClick, }: ChatRowContentProps) => { const { t } = useTranslation() const { mcpServers, alwaysAllowMcp, currentCheckpoint } = useExtensionState() const [reasoningCollapsed, setReasoningCollapsed] = useState(true) const [cost, apiReqCancelReason, apiReqStreamingFailedMessage] = useMemo(() => { if (message.text !== null && message.text !== undefined && message.say === "api_req_started") { const info: ClineApiReqInfo = JSON.parse(message.text) return [info.cost, info.cancelReason, info.streamingFailedMessage] } return [undefined, undefined, undefined] }, [message.text, message.say]) // When resuming task, last wont be api_req_failed but a resume_task // message, so api_req_started will show loading spinner. That's why we just // remove the last api_req_started that failed without streaming anything. const apiRequestFailedMessage = isLast && lastModifiedMessage?.ask === "api_req_failed" // if request is retried then the latest message is a api_req_retried ? lastModifiedMessage?.text : undefined const isCommandExecuting = isLast && lastModifiedMessage?.ask === "command" && lastModifiedMessage?.text?.includes(COMMAND_OUTPUT_STRING) const isMcpServerResponding = isLast && lastModifiedMessage?.say === "mcp_server_request_started" const type = message.type === "ask" ? message.ask : message.say const normalColor = "var(--vscode-foreground)" const errorColor = "var(--vscode-errorForeground)" const successColor = "var(--vscode-charts-green)" const cancelledColor = "var(--vscode-descriptionForeground)" const [icon, title] = useMemo(() => { switch (type) { case "error": return [ , {t("chat:error")}, ] case "mistake_limit_reached": return [ , {t("chat:troubleMessage")}, ] case "command": return [ isCommandExecuting ? ( ) : ( ), {t("chat:runCommand.title")}:, ] case "use_mcp_server": const mcpServerUse = JSON.parse(message.text || "{}") as ClineAskUseMcpServer return [ isMcpServerResponding ? ( ) : ( ), {mcpServerUse.type === "use_mcp_tool" ? t("chat:mcp.wantsToUseTool", { serverName: mcpServerUse.serverName }) : t("chat:mcp.wantsToAccessResource", { serverName: mcpServerUse.serverName })} , ] case "completion_result": return [ , {t("chat:taskCompleted")}, ] case "api_req_retry_delayed": return [] case "api_req_started": const getIconSpan = (iconName: string, color: string) => (
) return [ apiReqCancelReason !== null && apiReqCancelReason !== undefined ? ( apiReqCancelReason === "user_cancelled" ? ( getIconSpan("error", cancelledColor) ) : ( getIconSpan("error", errorColor) ) ) : cost !== null && cost !== undefined ? ( getIconSpan("check", successColor) ) : apiRequestFailedMessage ? ( getIconSpan("error", errorColor) ) : ( ), apiReqCancelReason !== null && apiReqCancelReason !== undefined ? ( apiReqCancelReason === "user_cancelled" ? ( {t("chat:apiRequest.cancelled")} ) : ( {t("chat:apiRequest.streamingFailed")} ) ) : cost !== null && cost !== undefined ? ( {t("chat:apiRequest.title")} ) : apiRequestFailedMessage ? ( {t("chat:apiRequest.failed")} ) : ( {t("chat:apiRequest.streaming")} ), ] case "followup": return [ , {t("chat:questions.hasQuestion")}, ] default: return [null, null] } }, [type, isCommandExecuting, message, isMcpServerResponding, apiReqCancelReason, cost, apiRequestFailedMessage, t]) const headerStyle: React.CSSProperties = { display: "flex", alignItems: "center", gap: "10px", marginBottom: "10px", } const pStyle: React.CSSProperties = { margin: 0, whiteSpace: "pre-wrap", wordBreak: "break-word", overflowWrap: "anywhere", } const tool = useMemo(() => { if (message.ask === "tool" || message.say === "tool") { return JSON.parse(message.text || "{}") as ClineSayTool } return null }, [message.ask, message.say, message.text]) const followUpData = useMemo(() => { if (message.type === "ask" && message.ask === "followup" && !message.partial) { return JSON.parse(message.text || "{}") } return null }, [message.type, message.ask, message.partial, message.text]) if (tool) { const toolIcon = (name: string) => ( ) switch (tool.tool) { case "editedExistingFile": case "appliedDiff": return ( <>
{toolIcon(tool.tool === "appliedDiff" ? "diff" : "edit")} {tool.isOutsideWorkspace ? t("chat:fileOperations.wantsToEditOutsideWorkspace") : t("chat:fileOperations.wantsToEdit")}
) case "newFileCreated": return ( <>
{toolIcon("new-file")} {t("chat:fileOperations.wantsToCreate")}
) case "readFile": return ( <>
{toolIcon("file-code")} {message.type === "ask" ? tool.isOutsideWorkspace ? t("chat:fileOperations.wantsToReadOutsideWorkspace") : t("chat:fileOperations.wantsToRead") : t("chat:fileOperations.didRead")}
{/* */}
{ vscode.postMessage({ type: "openFile", text: tool.content }) }}> {tool.path?.startsWith(".") && .} {removeLeadingNonAlphanumeric(tool.path ?? "") + "\u200E"}
) case "fetchInstructions": return ( <>
{toolIcon("file-code")} {t("chat:instructions.wantsToFetch")}
) case "listFilesTopLevel": return ( <>
{toolIcon("folder-opened")} {message.type === "ask" ? t("chat:directoryOperations.wantsToViewTopLevel") : t("chat:directoryOperations.didViewTopLevel")}
) case "listFilesRecursive": return ( <>
{toolIcon("folder-opened")} {message.type === "ask" ? t("chat:directoryOperations.wantsToViewRecursive") : t("chat:directoryOperations.didViewRecursive")}
) case "listCodeDefinitionNames": return ( <>
{toolIcon("file-code")} {message.type === "ask" ? t("chat:directoryOperations.wantsToViewDefinitions") : t("chat:directoryOperations.didViewDefinitions")}
) case "searchFiles": return ( <>
{toolIcon("search")} {message.type === "ask" ? ( {tool.regex} }} values={{ regex: tool.regex }} /> ) : ( {tool.regex} }} values={{ regex: tool.regex }} /> )}
) case "switchMode": return ( <>
{toolIcon("symbol-enum")} {message.type === "ask" ? ( <> {tool.reason ? ( {tool.mode} }} values={{ mode: tool.mode, reason: tool.reason }} /> ) : ( {tool.mode} }} values={{ mode: tool.mode }} /> )} ) : ( <> {tool.reason ? ( {tool.mode} }} values={{ mode: tool.mode, reason: tool.reason }} /> ) : ( {tool.mode} }} values={{ mode: tool.mode }} /> )} )}
) case "newTask": return ( <>
{toolIcon("new-file")} {tool.mode} }} values={{ mode: tool.mode }} />
{tool.content}
) case "finishTask": return ( <>
{toolIcon("checklist")} {t("chat:subtasks.wantsToFinish")}
{tool.content}
) default: return null } } switch (message.type) { case "say": switch (message.say) { case "reasoning": return ( setReasoningCollapsed(!reasoningCollapsed)} /> ) case "api_req_started": return ( <>
{icon} {title} 0 ? 1 : 0 }}> ${Number(cost || 0)?.toFixed(4)}
{(((cost === null || cost === undefined) && apiRequestFailedMessage) || apiReqStreamingFailedMessage) && ( <>

{apiRequestFailedMessage || apiReqStreamingFailedMessage} {apiRequestFailedMessage?.toLowerCase().includes("powershell") && ( <>

{t("chat:powershell.issues")}{" "} troubleshooting guide . )}

{/* {apiProvider === "" && (
Uh-oh, this could be a problem on end. We've been alerted and will resolve this ASAP. You can also{" "} contact us .
)} */} )} {isExpanded && (
)} ) case "api_req_finished": return null // we should never see this message type case "text": return (
) case "user_feedback": return (
{highlightMentions(message.text)} { e.stopPropagation() vscode.postMessage({ type: "deleteMessage", value: message.ts, }) }}>
{message.images && message.images.length > 0 && ( )}
) case "user_feedback_diff": const tool = JSON.parse(message.text || "{}") as ClineSayTool return (
) case "error": return ( <> {title && (
{icon} {title}
)}

{message.text}

) case "completion_result": return ( <>
{icon} {title}
) case "shell_integration_warning": return ( <>
{t("chat:shellIntegration.unavailable")}
{message.text}

Please update VSCode (CMD/CTRL + Shift + P → "Update") and make sure you're using a supported shell: zsh, bash, fish, or PowerShell ( CMD/CTRL + Shift + P → "Terminal: Select Default Profile").{" "} {t("chat:shellIntegration.troubleshooting")}
) case "mcp_server_response": return ( <>
{t("chat:response")}
) case "checkpoint_saved": return ( ) default: return ( <> {title && (
{icon} {title}
)}
) } case "ask": switch (message.ask) { case "mistake_limit_reached": return ( <>
{icon} {title}

{message.text}

) case "command": const splitMessage = (text: string) => { const outputIndex = text.indexOf(COMMAND_OUTPUT_STRING) if (outputIndex === -1) { return { command: text, output: "" } } return { command: text.slice(0, outputIndex).trim(), output: text .slice(outputIndex + COMMAND_OUTPUT_STRING.length) .trim() .split("") .map((char) => { switch (char) { case "\t": return "→ " case "\b": return "⌫" case "\f": return "⏏" case "\v": return "⇳" default: return char } }) .join(""), } } const { command, output } = splitMessage(message.text || "") return ( <>
{icon} {title}
{/* 0} /> */}
{output.length > 0 && (
{t("chat:commandOutput")}
{isExpanded && }
)}
) case "use_mcp_server": const useMcpServer = JSON.parse(message.text || "{}") as ClineAskUseMcpServer const server = mcpServers.find((server) => server.name === useMcpServer.serverName) return ( <>
{icon} {title}
{useMcpServer.type === "access_mcp_resource" && ( )} {useMcpServer.type === "use_mcp_tool" && ( <>
e.stopPropagation()}> tool.name === useMcpServer.toolName, )?.description || "", alwaysAllow: server?.tools?.find( (tool) => tool.name === useMcpServer.toolName, )?.alwaysAllow || false, }} serverName={useMcpServer.serverName} alwaysAllowMcp={alwaysAllowMcp} />
{useMcpServer.arguments && useMcpServer.arguments !== "{}" && (
{t("chat:arguments")}
)} )}
) case "completion_result": if (message.text) { return (
{icon} {title}
) } else { return null // Don't render anything when we get a completion_result ask without text } case "followup": return ( <> {title && (
{icon} {title}
)}
) default: return null } } } export const ProgressIndicator = () => (
) const Markdown = memo(({ markdown, partial }: { markdown?: string; partial?: boolean }) => { const [isHovering, setIsHovering] = useState(false) const { copyWithFeedback } = useCopyToClipboard(200) // shorter feedback duration for copy button flash return (
setIsHovering(true)} onMouseLeave={() => setIsHovering(false)} style={{ position: "relative" }}>
{markdown && !partial && isHovering && (
{ const success = await copyWithFeedback(markdown) if (success) { const button = document.activeElement as HTMLElement if (button) { button.style.background = "var(--vscode-button-background)" setTimeout(() => { button.style.background = "" }, 200) } } }} title="Copy as markdown">
)}
) })