import { VSCodeBadge, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react" import deepEqual from "fast-deep-equal" import React, { memo, useEffect, useMemo, useRef } from "react" import { ClaudeApiReqInfo, ClaudeMessage, ClaudeSayTool } from "../../../../src/shared/ExtensionMessage" import { COMMAND_OUTPUT_STRING } from "../../../../src/shared/combineCommandSequences" 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 Thumbnails from "../common/Thumbnails" import { highlightMentions } from "./TaskHeader" import { useSize } from "react-use" interface ChatRowProps { message: ClaudeMessage isExpanded: boolean onToggleExpand: () => void lastModifiedMessage?: ClaudeMessage isLast: boolean onHeightChange: (height: number) => 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) { prevHeightRef.current = height if (!isInitialRender) { onHeightChange(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 const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifiedMessage, isLast }: ChatRowContentProps) => { const [cost, apiReqCancelReason, apiReqStreamingFailedMessage] = useMemo(() => { if (message.text != null && message.say === "api_req_started") { const info: ClaudeApiReqInfo = JSON.parse(message.text) return [info.cost, info.cancelReason, info.streamingFailedMessage] } return [undefined, undefined, 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 cancelledColor = "var(--vscode-descriptionForeground)" 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": const getIconSpan = (iconName: string, color: string) => (
) return [ cost != null ? ( apiReqCancelReason != null ? ( apiReqCancelReason === "user_cancelled" ? ( getIconSpan("error", cancelledColor) ) : ( getIconSpan("error", errorColor) ) ) : ( getIconSpan("check", successColor) ) ) : apiRequestFailedMessage ? ( getIconSpan("error", errorColor) ) : ( ), cost != null ? ( apiReqCancelReason != null ? ( apiReqCancelReason === "user_cancelled" ? ( API Request Cancelled ) : ( API Streaming Failed ) ) : ( API Request ) ) : apiRequestFailedMessage ? ( API Request Failed ) : ( API Request... ), ] case "followup": return [ , Claude has a question:, ] default: return [null, null] } }, [type, cost, apiRequestFailedMessage, isCommandExecuting, apiReqCancelReason]) 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} {/* Need to render this everytime since it affects height of row by 2px */} 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 === "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 }) => { const withoutThinkingTags = useMemo(() => { if (!markdown) return "" let processed = markdown // Remove end substrings of (with optional line break after) and (with optional line break before) // Needs to be separate since we dont want to remove the line break before the first tag processed = processed.replace(/\s?/g, "") processed = processed.replace(/\s?<\/thinking>/g, "") return processed }, [markdown]) return (
) })