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 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. 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-line", wordBreak: "break-word", } 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}
      ) 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 "listFiles": return ( <>
      {toolIcon("folder-opened")} Claude wants to view this directory:
      ) case "analyzeProject": return ( <>
      {toolIcon("file-code")} Claude wants to analyze this project:
      ) } 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