import { VSCodeBadge, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react" import deepEqual from "fast-deep-equal" import React, { memo, useMemo } from "react" import ReactMarkdown from "react-markdown" import { ClaudeMessage, ClaudeSayTool } from "../../../src/shared/ExtensionMessage" import { COMMAND_OUTPUT_STRING } from "../../../src/shared/combineCommandSequences" import { vscode } from "../utils/vscode" import CodeAccordian, { removeLeadingNonAlphanumeric } from "./CodeAccordian" import CodeBlock, { CODE_BLOCK_BG_COLOR } from "./CodeBlock" import { highlightMentions } from "./TaskHeader" import Thumbnails from "./Thumbnails" interface ChatRowProps { message: ClaudeMessage isExpanded: boolean onToggleExpand: () => void lastModifiedMessage?: ClaudeMessage isLast: boolean } const ChatRow = memo( (props: ChatRowProps) => { // 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 (
) }, // memo does shallow comparison of props, so we need to do deep comparison of arrays/objects whose properties might change deepEqual ) export default ChatRow const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifiedMessage, isLast }: ChatRowProps) => { const cost = useMemo(() => { if (message.text != null && message.say === "api_req_started") { return JSON.parse(message.text).cost } return undefined }, [message.text, message.say]) 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 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 [icon, title] = useMemo(() => { switch (type) { case "error": return [ , Error, ] case "mistake_limit_reached": return [ , Claude is having trouble..., ] case "command": return [ isCommandExecuting ? ( ) : ( ), Claude wants to execute this command: , ] case "completion_result": return [ , Task Completed, ] case "api_req_started": return [ cost != null ? ( ) : apiRequestFailedMessage ? ( ) : ( ), cost != null ? ( API Request ) : apiRequestFailedMessage ? ( API Request Failed ) : ( API Request... ), ] case "followup": return [ , Claude has a question:, ] default: return [null, null] } }, [type, cost, apiRequestFailedMessage, isCommandExecuting]) 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 ClaudeSayTool } return null }, [message.ask, message.say, message.text]) if (tool) { const toolIcon = (name: string) => ( ) switch (tool.tool) { case "editedExistingFile": return ( <>
{toolIcon("edit")} Claude wants to edit this file:
) case "newFileCreated": return ( <>
{toolIcon("new-file")} Claude wants to create a new file:
) case "readFile": return ( <>
{toolIcon("file-code")} {message.type === "ask" ? "Claude wants to read this file:" : "Claude 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" ? "Claude wants to view the top level files in this directory:" : "Claude viewed the top level files in this directory:"}
) case "listFilesRecursive": return ( <>
{toolIcon("folder-opened")} {message.type === "ask" ? "Claude wants to recursively view all files in this directory:" : "Claude recursively viewed all files in this directory:"}
) case "listCodeDefinitionNames": return ( <>
{toolIcon("file-code")} {message.type === "ask" ? "Claude wants to view source code definition names used in this directory:" : "Claude viewed source code definition names used in this directory:"}
) case "searchFiles": return ( <>
{toolIcon("search")} {message.type === "ask" ? ( <> Claude wants to search this directory for {tool.regex}: ) : ( <> Claude searched this directory for {tool.regex}: )}
) case "inspectSite": const isInspecting = lastModifiedMessage?.say === "inspect_site_result" && !lastModifiedMessage?.images return ( <>
{isInspecting ? : toolIcon("inspect")} {message.type === "ask" ? ( <>Claude wants to inspect this website: ) : ( <>Claude is inspecting this website: )}
) default: return null } } switch (message.type) { case "say": switch (message.say) { case "api_req_started": return ( <>
{icon} {title} {cost != null && cost > 0 && ${Number(cost)?.toFixed(4)}}
{cost == null && apiRequestFailedMessage && ( <>

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

It seems like you're having Windows PowerShell issues, please see this{" "} troubleshooting guide . )}

{/* {apiProvider === "kodu" && (
Uh-oh, this could be a problem on Kodu's end. We've been alerted and will resolve this ASAP. You can also{" "} contact us on discord .
)} */} )} {isExpanded && (
)} ) case "api_req_finished": return null // we should never see this message type case "text": return (
) case "user_feedback": return (
{highlightMentions(message.text)} {message.images && message.images.length > 0 && ( )}
) case "user_feedback_diff": const tool = JSON.parse(message.text || "{}") as ClaudeSayTool return (
) case "inspect_site_result": const logs = message.text || "" const screenshot = message.images?.[0] return (
{screenshot && ( Inspect screenshot vscode.postMessage({ type: "openImage", text: screenshot })} /> )} {logs && ( )}
) case "error": return ( <> {title && (
{icon} {title}
)}

{message.text}

) case "completion_result": return ( <>
{icon} {title}
) case "shell_integration_warning": return ( <>
Shell Integration Unavailable
Claude 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?
) 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 "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 } } } const ProgressIndicator = () => (
) const Markdown = memo(({ markdown }: { markdown?: string }) => { // react-markdown lets us customize elements, so here we're using their example of replacing code blocks with SyntaxHighlighter. However when there are no language matches (` or ``` without a language specifier) then we default to a normal code element for inline code. Code blocks without a language specifier shouldn't be a common occurrence as we prompt Claude to always use a language specifier. // when claude wraps text in thinking tags, he doesnt use line breaks so we need to insert those ourselves to render markdown correctly const parsed = markdown?.replace(/([\s\S]*?)<\/thinking>/g, (match, content) => { return content // return `__\n\n${content}\n\n__` }) return (
) }, ol(props) { const { style, ...rest } = props return (
    ) }, ul(props) { const { style, ...rest } = props return (
      ) }, // pre always surrounds a code, and we custom handle code blocks below. Pre has some non-10 margin, while all other elements in markdown have a 10 top/bottom margin and the outer div has a -10 top/bottom margin to counteract this between chat rows. However we render markdown in a completion_result row so make sure to add padding as necessary when used within other rows. pre(props) { const { style, ...rest } = props return (
      						)
      					},
      					// https://github.com/remarkjs/react-markdown?tab=readme-ov-file#use-custom-components-syntax-highlight
      					code(props) {
      						const { children, className, node, ...rest } = props
      						const match = /language-(\w+)/.exec(className || "")
      						return match ? (
      							
      ) : ( {children} ) }, }} />
) })