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 && (

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 (
)
} 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}
)
},
}}
/>
)
})