import { VSCodeBadge, VSCodeButton, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react" import React from "react" import Markdown from "react-markdown" import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" import { ClaudeAsk, ClaudeMessage, ClaudeSay, ClaudeSayTool } from "../../../src/shared/ExtensionMessage" import { COMMAND_OUTPUT_STRING } from "../utils/combineCommandSequences" import { SyntaxHighlighterStyle } from "../utils/getSyntaxHighlighterStyleFromTheme" import CodeBlock from "./CodeBlock/CodeBlock" import Thumbnails from "./Thumbnails" interface ChatRowProps { message: ClaudeMessage syntaxHighlighterStyle: SyntaxHighlighterStyle isExpanded: boolean onToggleExpand: () => void lastModifiedMessage?: ClaudeMessage isLast: boolean } const ChatRow: React.FC = ({ message, syntaxHighlighterStyle, isExpanded, onToggleExpand, lastModifiedMessage, isLast, }) => { const cost = message.text != null && message.say === "api_req_started" ? JSON.parse(message.text).cost : undefined 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 getIconAndTitle = (type: ClaudeAsk | ClaudeSay | undefined): [JSX.Element | null, JSX.Element | null] => { const normalColor = "var(--vscode-foreground)" const errorColor = "var(--vscode-errorForeground)" const successColor = "var(--vscode-testing-iconPassed)" const ProgressIndicator = (
) switch (type) { case "request_limit_reached": return [ , Max Requests Reached, ] case "error": return [ , Error, ] case "command": return [ isCommandExecuting ? ( ProgressIndicator ) : ( ), Claude wants to execute this command: , ] case "completion_result": return [ , Task Completed, ] case "api_req_started": return [ cost ? ( ) : apiRequestFailedMessage ? ( ) : ( ProgressIndicator ), cost ? ( API Request Complete ) : apiRequestFailedMessage ? ( API Request Failed ) : ( Making API Request... ), ] case "followup": return [ , Claude has a question:, ] default: return [null, null] } } const renderMarkdown = (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 `__\n\n${content}\n\n__` }) return (
) }, ol(props) { const { style, ...rest } = props return (
    ) }, ul(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} ) }, }} />
) } const renderContent = () => { const [icon, title] = getIconAndTitle(message.type === "ask" ? message.ask : message.say) 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", } switch (message.type) { case "say": switch (message.say) { case "api_req_started": return ( <>
{icon} {title} {cost && ${Number(cost).toFixed(4)}}
{cost == null && apiRequestFailedMessage && (

{apiRequestFailedMessage}

)} ) case "api_req_finished": return null // we should never see this message type case "text": return
{renderMarkdown(message.text)}
case "user_feedback": return (
{message.text} {message.images && message.images.length > 0 && ( )}
) case "error": return ( <> {title && (
{icon} {title}
)}

{message.text}

) case "completion_result": return ( <>
{icon} {title}
{renderMarkdown(message.text)}
) default: return ( <> {title && (
{icon} {title}
)}
{renderMarkdown(message.text)}
) } case "ask": switch (message.ask) { case "tool": const tool = JSON.parse(message.text || "{}") as ClaudeSayTool 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")} Claude wants to read this file:
) case "listFilesTopLevel": return ( <>
{toolIcon("folder-opened")} Claude wants to view the top level files in this directory:
) case "listFilesRecursive": return ( <>
{toolIcon("folder-opened")} Claude wants to recursively view all files in this directory:
) case "viewSourceCodeDefinitionsTopLevel": return ( <>
{toolIcon("file-code")} Claude wants to view source code definitions in files at the top level of this directory:
) } break case "request_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(), } } const { command, output } = splitMessage(message.text || "") return ( <>
{icon} {title}
{output && ( <>

{COMMAND_OUTPUT_STRING}

)}
) case "completion_result": if (message.text) { return (
{icon} {title}
{renderMarkdown(message.text)}
) } else { return null // Don't render anything when we get a completion_result ask without text } case "followup": return ( <> {title && (
{icon} {title}
)}
{renderMarkdown(message.text)}
) } } } // NOTE: we cannot return null as virtuoso does not support it, so we must use a separate visibleMessages array to filter out messages that should not be rendered return (
{renderContent()} {isExpanded && message.say === "api_req_started" && (
)}
) } export default ChatRow