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 { 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" interface ChatRowProps { message: ClineMessage isExpanded: boolean onToggleExpand: () => void lastModifiedMessage?: ClineMessage isLast: boolean onHeightChange: (isTaller: boolean) => void isStreaming: boolean } 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, isExpanded, onToggleExpand, lastModifiedMessage, isLast, isStreaming, }: ChatRowContentProps) => { const { mcpServers } = useExtensionState() const [reasoningCollapsed, setReasoningCollapsed] = useState(false) // Auto-collapse reasoning when new messages arrive useEffect(() => { if (!isLast && message.say === "reasoning") { setReasoningCollapsed(true) } }, [isLast, message.say]) const [cost, apiReqCancelReason, apiReqStreamingFailedMessage] = useMemo(() => { if (message.text != null && 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 [ , Error, ] case "mistake_limit_reached": return [ , Roo is having trouble..., ] case "command": return [ isCommandExecuting ? ( ) : ( ), Roo wants to execute this command:, ] case "use_mcp_server": const mcpServerUse = JSON.parse(message.text || "{}") as ClineAskUseMcpServer return [ isMcpServerResponding ? ( ) : ( ), Roo wants to {mcpServerUse.type === "use_mcp_tool" ? "use a tool" : "access a resource"} on the{" "} {mcpServerUse.serverName} MCP server: , ] case "completion_result": return [ , Task Completed, ] case "api_req_retry_delayed": return [] case "api_req_started": const getIconSpan = (iconName: string, color: string) => (
) return [ apiReqCancelReason != null ? ( apiReqCancelReason === "user_cancelled" ? ( getIconSpan("error", cancelledColor) ) : ( getIconSpan("error", errorColor) ) ) : cost != null ? ( getIconSpan("check", successColor) ) : apiRequestFailedMessage ? ( getIconSpan("error", errorColor) ) : ( ), apiReqCancelReason != null ? ( apiReqCancelReason === "user_cancelled" ? ( API Request Cancelled ) : ( API Streaming Failed ) ) : cost != null ? ( API Request ) : apiRequestFailedMessage ? ( API Request Failed ) : ( API Request... ), ] case "followup": return [ , Roo has a question:, ] default: return [null, null] } }, [type, isCommandExecuting, message, isMcpServerResponding, apiReqCancelReason, cost, apiRequestFailedMessage]) 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]) if (tool) { const toolIcon = (name: string) => ( ) switch (tool.tool) { case "editedExistingFile": case "appliedDiff": return ( <>
{toolIcon(tool.tool === "appliedDiff" ? "diff" : "edit")} Roo wants to edit this file:
) case "newFileCreated": return ( <>
{toolIcon("new-file")} Roo wants to create a new file:
) case "readFile": return ( <>
{toolIcon("file-code")} {message.type === "ask" ? "Roo wants to read this file:" : "Roo read this file:"}
{/* */}
{ vscode.postMessage({ type: "openFile", text: tool.content }) }}> {tool.path?.startsWith(".") && .} {removeLeadingNonAlphanumeric(tool.path ?? "") + "\u200E"}
) case "listFilesTopLevel": return ( <>
{toolIcon("folder-opened")} {message.type === "ask" ? "Roo wants to view the top level files in this directory:" : "Roo viewed the top level files in this directory:"}
) case "listFilesRecursive": return ( <>
{toolIcon("folder-opened")} {message.type === "ask" ? "Roo wants to recursively view all files in this directory:" : "Roo recursively viewed all files in this directory:"}
) case "listCodeDefinitionNames": return ( <>
{toolIcon("file-code")} {message.type === "ask" ? "Roo wants to view source code definition names used in this directory:" : "Roo viewed source code definition names used in this directory:"}
) case "searchFiles": return ( <>
{toolIcon("search")} {message.type === "ask" ? ( <> Roo wants to search this directory for {tool.regex}: ) : ( <> Roo searched this directory for {tool.regex}: )}
) // case "inspectSite": // const isInspecting = // isLast && lastModifiedMessage?.say === "inspect_site_result" && !lastModifiedMessage?.images // return ( // <> //
// {isInspecting ? : toolIcon("inspect")} // // {message.type === "ask" ? ( // <>Roo wants to inspect this website: // ) : ( // <>Roo is inspecting this website: // )} // //
//
// //
// // ) case "switchMode": return ( <>
{toolIcon("symbol-enum")} {message.type === "ask" ? ( <> Roo wants to switch to {tool.mode} mode {tool.reason ? ` because: ${tool.reason}` : ""} ) : ( <> Roo switched to {tool.mode} mode {tool.reason ? ` because: ${tool.reason}` : ""} )}
) 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 && apiRequestFailedMessage) || apiReqStreamingFailedMessage) && ( <>

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

It seems like you're having Windows PowerShell issues, please see this{" "} 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 ( <>
Shell Integration Unavailable
Roo won't be able to view the command's output. 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").{" "} Still having trouble?
) case "mcp_server_response": return ( <>
Response
) 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 && (
Command Output
{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} />
{useMcpServer.arguments && useMcpServer.arguments !== "{}" && (
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) return (
setIsHovering(true)} onMouseLeave={() => setIsHovering(false)} style={{ position: "relative" }}>
{markdown && !partial && isHovering && (
{ navigator.clipboard.writeText(markdown) // Flash the button background briefly to indicate 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">
)}
) })