import { ClaudeAsk, ClaudeMessage, ClaudeSay, ClaudeSayTool } from "@shared/ExtensionMessage" import { VSCodeBadge, VSCodeButton, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react" import React from "react" import { COMMAND_OUTPUT_STRING } from "../utilities/combineCommandSequences" import { SyntaxHighlighterStyle } from "../utilities/getSyntaxHighlighterStyleFromTheme" import CodeBlock from "./CodeBlock/CodeBlock" import Markdown from "react-markdown" import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" interface ChatRowProps { message: ClaudeMessage syntaxHighlighterStyle: SyntaxHighlighterStyle isExpanded: boolean onToggleExpand: () => void apiRequestFailedMessage?: string } const ChatRow: React.FC = ({ message, syntaxHighlighterStyle, isExpanded, onToggleExpand, apiRequestFailedMessage, }) => { const cost = message.text != null && message.say === "api_req_started" ? JSON.parse(message.text).cost : undefined 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)" switch (type) { case "request_limit_reached": return [ , Max Requests Reached, ] case "error": return [ , Error, ] case "command": return [ , Claude wants to execute this command: , ] case "completion_result": return [ , Task Completed, ] case "api_req_started": return [ cost ? ( ) : apiRequestFailedMessage ? ( ) : (
), cost ? ( API Request Complete ) : apiRequestFailedMessage ? ( API Request Failed ) : ( Making API Request... ), ] default: return [null, null] } } const convertToMarkdown = (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. return ( }, //p: "span", // 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-line", } 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
{convertToMarkdown(message.text)}
case "user_feedback": return (
{message.text}
) case "error": return ( <> {title && (
{icon} {title}
)}

{message.text}

) case "completion_result": return ( <>
{icon} {title}
{convertToMarkdown(message.text)}
) default: return ( <> {title && (
{icon} {title}
)}
{convertToMarkdown(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 "listFiles": return ( <>
{toolIcon("folder-opened")} Claude wants to view 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}
{convertToMarkdown(message.text)}
) } else { return null // Don't render anything when we get a completion_result ask without text } case "followup": return ( <> {title && (
{icon} {title}
)}
{convertToMarkdown(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