|
@@ -1,13 +1,13 @@
|
|
|
import { VSCodeBadge, VSCodeButton, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
|
|
import { VSCodeBadge, VSCodeButton, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
|
|
|
-import React from "react"
|
|
|
|
|
-import Markdown from "react-markdown"
|
|
|
|
|
|
|
+import React, { memo, useMemo } from "react"
|
|
|
|
|
+import ReactMarkdown from "react-markdown"
|
|
|
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
|
|
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
|
|
|
-import { ClaudeAsk, ClaudeMessage, ClaudeSay, ClaudeSayTool } from "../../../src/shared/ExtensionMessage"
|
|
|
|
|
|
|
+import { ClaudeMessage, ClaudeSayTool } from "../../../src/shared/ExtensionMessage"
|
|
|
import { COMMAND_OUTPUT_STRING } from "../../../src/shared/combineCommandSequences"
|
|
import { COMMAND_OUTPUT_STRING } from "../../../src/shared/combineCommandSequences"
|
|
|
import { SyntaxHighlighterStyle } from "../utils/getSyntaxHighlighterStyleFromTheme"
|
|
import { SyntaxHighlighterStyle } from "../utils/getSyntaxHighlighterStyleFromTheme"
|
|
|
import CodeBlock from "./CodeBlock"
|
|
import CodeBlock from "./CodeBlock"
|
|
|
-import Thumbnails from "./Thumbnails"
|
|
|
|
|
import Terminal from "./Terminal"
|
|
import Terminal from "./Terminal"
|
|
|
|
|
+import Thumbnails from "./Thumbnails"
|
|
|
|
|
|
|
|
interface ChatRowProps {
|
|
interface ChatRowProps {
|
|
|
message: ClaudeMessage
|
|
message: ClaudeMessage
|
|
@@ -19,7 +19,21 @@ interface ChatRowProps {
|
|
|
handleSendStdin: (text: string) => void
|
|
handleSendStdin: (text: string) => void
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const ChatRow: React.FC<ChatRowProps> = ({
|
|
|
|
|
|
|
+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 (
|
|
|
|
|
+ <div
|
|
|
|
|
+ style={{
|
|
|
|
|
+ padding: "10px 6px 10px 15px",
|
|
|
|
|
+ }}>
|
|
|
|
|
+ <ChatRowContent {...props} />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+export default ChatRow
|
|
|
|
|
+
|
|
|
|
|
+const ChatRowContent = ({
|
|
|
message,
|
|
message,
|
|
|
syntaxHighlighterStyle,
|
|
syntaxHighlighterStyle,
|
|
|
isExpanded,
|
|
isExpanded,
|
|
@@ -27,35 +41,26 @@ const ChatRow: React.FC<ChatRowProps> = ({
|
|
|
lastModifiedMessage,
|
|
lastModifiedMessage,
|
|
|
isLast,
|
|
isLast,
|
|
|
handleSendStdin,
|
|
handleSendStdin,
|
|
|
-}) => {
|
|
|
|
|
- const cost = message.text != null && message.say === "api_req_started" ? JSON.parse(message.text).cost : undefined
|
|
|
|
|
|
|
+}: 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 =
|
|
const apiRequestFailedMessage =
|
|
|
isLast && lastModifiedMessage?.ask === "api_req_failed" // if request is retried then the latest message is a api_req_retried
|
|
isLast && lastModifiedMessage?.ask === "api_req_failed" // if request is retried then the latest message is a api_req_retried
|
|
|
? lastModifiedMessage?.text
|
|
? lastModifiedMessage?.text
|
|
|
: undefined
|
|
: undefined
|
|
|
const isCommandExecuting =
|
|
const isCommandExecuting =
|
|
|
isLast && lastModifiedMessage?.ask === "command" && lastModifiedMessage?.text?.includes(COMMAND_OUTPUT_STRING)
|
|
isLast && lastModifiedMessage?.ask === "command" && lastModifiedMessage?.text?.includes(COMMAND_OUTPUT_STRING)
|
|
|
|
|
+ const type = message.type === "ask" ? message.ask : message.say
|
|
|
|
|
|
|
|
- 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-charts-green)"
|
|
|
|
|
-
|
|
|
|
|
- const ProgressIndicator = (
|
|
|
|
|
- <div
|
|
|
|
|
- style={{
|
|
|
|
|
- width: "16px",
|
|
|
|
|
- height: "16px",
|
|
|
|
|
- display: "flex",
|
|
|
|
|
- alignItems: "center",
|
|
|
|
|
- justifyContent: "center",
|
|
|
|
|
- }}>
|
|
|
|
|
- <div style={{ transform: "scale(0.55)", transformOrigin: "center" }}>
|
|
|
|
|
- <VSCodeProgressRing />
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ const normalColor = "var(--vscode-foreground)"
|
|
|
|
|
+ const errorColor = "var(--vscode-errorForeground)"
|
|
|
|
|
+ const successColor = "var(--vscode-charts-green)"
|
|
|
|
|
|
|
|
|
|
+ const [icon, title] = useMemo(() => {
|
|
|
switch (type) {
|
|
switch (type) {
|
|
|
case "error":
|
|
case "error":
|
|
|
return [
|
|
return [
|
|
@@ -74,7 +79,7 @@ const ChatRow: React.FC<ChatRowProps> = ({
|
|
|
case "command":
|
|
case "command":
|
|
|
return [
|
|
return [
|
|
|
isCommandExecuting ? (
|
|
isCommandExecuting ? (
|
|
|
- ProgressIndicator
|
|
|
|
|
|
|
+ <ProgressIndicator />
|
|
|
) : (
|
|
) : (
|
|
|
<span
|
|
<span
|
|
|
className="codicon codicon-terminal"
|
|
className="codicon codicon-terminal"
|
|
@@ -102,7 +107,7 @@ const ChatRow: React.FC<ChatRowProps> = ({
|
|
|
className="codicon codicon-error"
|
|
className="codicon codicon-error"
|
|
|
style={{ color: errorColor, marginBottom: "-1.5px" }}></span>
|
|
style={{ color: errorColor, marginBottom: "-1.5px" }}></span>
|
|
|
) : (
|
|
) : (
|
|
|
- ProgressIndicator
|
|
|
|
|
|
|
+ <ProgressIndicator />
|
|
|
),
|
|
),
|
|
|
cost != null ? (
|
|
cost != null ? (
|
|
|
<span style={{ color: normalColor, fontWeight: "bold" }}>API Request Complete</span>
|
|
<span style={{ color: normalColor, fontWeight: "bold" }}>API Request Complete</span>
|
|
@@ -122,366 +127,30 @@ const ChatRow: React.FC<ChatRowProps> = ({
|
|
|
default:
|
|
default:
|
|
|
return [null, null]
|
|
return [null, null]
|
|
|
}
|
|
}
|
|
|
- }
|
|
|
|
|
|
|
+ }, [type, cost, apiRequestFailedMessage, isCommandExecuting])
|
|
|
|
|
|
|
|
- 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(/<thinking>([\s\S]*?)<\/thinking>/g, (match, content) => {
|
|
|
|
|
- return `_<thinking>_\n\n${content}\n\n_</thinking>_`
|
|
|
|
|
- })
|
|
|
|
|
- return (
|
|
|
|
|
- <div style={{ wordBreak: "break-word", overflowWrap: "anywhere" }}>
|
|
|
|
|
- <Markdown
|
|
|
|
|
- children={parsed}
|
|
|
|
|
- components={{
|
|
|
|
|
- p(props) {
|
|
|
|
|
- const { style, ...rest } = props
|
|
|
|
|
- return (
|
|
|
|
|
- <p
|
|
|
|
|
- style={{
|
|
|
|
|
- ...style,
|
|
|
|
|
- margin: 0,
|
|
|
|
|
- marginTop: 0,
|
|
|
|
|
- marginBottom: 0,
|
|
|
|
|
- whiteSpace: "pre-wrap",
|
|
|
|
|
- wordBreak: "break-word",
|
|
|
|
|
- overflowWrap: "anywhere",
|
|
|
|
|
- }}
|
|
|
|
|
- {...rest}
|
|
|
|
|
- />
|
|
|
|
|
- )
|
|
|
|
|
- },
|
|
|
|
|
- ol(props) {
|
|
|
|
|
- const { style, ...rest } = props
|
|
|
|
|
- return (
|
|
|
|
|
- <ol
|
|
|
|
|
- style={{
|
|
|
|
|
- ...style,
|
|
|
|
|
- padding: "0 0 0 20px",
|
|
|
|
|
- margin: "10px 0",
|
|
|
|
|
- wordBreak: "break-word",
|
|
|
|
|
- overflowWrap: "anywhere",
|
|
|
|
|
- }}
|
|
|
|
|
- {...rest}
|
|
|
|
|
- />
|
|
|
|
|
- )
|
|
|
|
|
- },
|
|
|
|
|
- ul(props) {
|
|
|
|
|
- const { style, ...rest } = props
|
|
|
|
|
- return (
|
|
|
|
|
- <ul
|
|
|
|
|
- style={{
|
|
|
|
|
- ...style,
|
|
|
|
|
- padding: "0 0 0 20px",
|
|
|
|
|
- margin: "10px 0",
|
|
|
|
|
- wordBreak: "break-word",
|
|
|
|
|
- overflowWrap: "anywhere",
|
|
|
|
|
- }}
|
|
|
|
|
- {...rest}
|
|
|
|
|
- />
|
|
|
|
|
- )
|
|
|
|
|
- },
|
|
|
|
|
- // 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 ? (
|
|
|
|
|
- <SyntaxHighlighter
|
|
|
|
|
- {...(rest as any)} // will be passed down to pre
|
|
|
|
|
- PreTag="div"
|
|
|
|
|
- children={String(children).replace(/\n$/, "")}
|
|
|
|
|
- language={match[1]}
|
|
|
|
|
- style={{
|
|
|
|
|
- ...syntaxHighlighterStyle,
|
|
|
|
|
- 'code[class*="language-"]': {
|
|
|
|
|
- background: "var(--vscode-editor-background)",
|
|
|
|
|
- },
|
|
|
|
|
- 'pre[class*="language-"]': {
|
|
|
|
|
- background: "var(--vscode-editor-background)",
|
|
|
|
|
- },
|
|
|
|
|
- }}
|
|
|
|
|
- customStyle={{
|
|
|
|
|
- overflowX: "auto",
|
|
|
|
|
- overflowY: "hidden",
|
|
|
|
|
- maxWidth: "100%",
|
|
|
|
|
- margin: 0,
|
|
|
|
|
- padding: "10px",
|
|
|
|
|
- // important to note that min-width: max-content is not required here how it is in CodeBlock.tsx
|
|
|
|
|
- borderRadius: 3,
|
|
|
|
|
- border: "1px solid var(--vscode-sideBar-border)",
|
|
|
|
|
- fontSize: "var(--vscode-editor-font-size)",
|
|
|
|
|
- lineHeight: "var(--vscode-editor-line-height)",
|
|
|
|
|
- fontFamily: "var(--vscode-editor-font-family)",
|
|
|
|
|
- }}
|
|
|
|
|
- />
|
|
|
|
|
- ) : (
|
|
|
|
|
- <code
|
|
|
|
|
- {...rest}
|
|
|
|
|
- className={className}
|
|
|
|
|
- style={{
|
|
|
|
|
- whiteSpace: "pre-line",
|
|
|
|
|
- wordBreak: "break-word",
|
|
|
|
|
- overflowWrap: "anywhere",
|
|
|
|
|
- }}>
|
|
|
|
|
- {children}
|
|
|
|
|
- </code>
|
|
|
|
|
- )
|
|
|
|
|
- },
|
|
|
|
|
- }}
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ const headerStyle: React.CSSProperties = {
|
|
|
|
|
+ display: "flex",
|
|
|
|
|
+ alignItems: "center",
|
|
|
|
|
+ gap: "10px",
|
|
|
|
|
+ marginBottom: "10px",
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- 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 (
|
|
|
|
|
- <>
|
|
|
|
|
- <div
|
|
|
|
|
- style={{
|
|
|
|
|
- ...headerStyle,
|
|
|
|
|
- marginBottom: cost == null && apiRequestFailedMessage ? 10 : 0,
|
|
|
|
|
- justifyContent: "space-between",
|
|
|
|
|
- }}>
|
|
|
|
|
- <div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
|
|
|
|
|
- {icon}
|
|
|
|
|
- {title}
|
|
|
|
|
- {cost != null && cost > 0 && (
|
|
|
|
|
- <VSCodeBadge>${Number(cost)?.toFixed(4)}</VSCodeBadge>
|
|
|
|
|
- )}
|
|
|
|
|
- </div>
|
|
|
|
|
- <VSCodeButton
|
|
|
|
|
- appearance="icon"
|
|
|
|
|
- aria-label="Toggle Details"
|
|
|
|
|
- onClick={onToggleExpand}>
|
|
|
|
|
- <span
|
|
|
|
|
- className={`codicon codicon-chevron-${isExpanded ? "up" : "down"}`}></span>
|
|
|
|
|
- </VSCodeButton>
|
|
|
|
|
- </div>
|
|
|
|
|
- {cost == null && apiRequestFailedMessage && (
|
|
|
|
|
- <>
|
|
|
|
|
- <p style={{ ...pStyle, color: "var(--vscode-errorForeground)" }}>
|
|
|
|
|
- {apiRequestFailedMessage}
|
|
|
|
|
- </p>
|
|
|
|
|
- {/* {apiProvider === "kodu" && (
|
|
|
|
|
- <div
|
|
|
|
|
- style={{
|
|
|
|
|
- display: "flex",
|
|
|
|
|
- alignItems: "center",
|
|
|
|
|
- backgroundColor:
|
|
|
|
|
- "color-mix(in srgb, var(--vscode-errorForeground) 20%, transparent)",
|
|
|
|
|
- color: "var(--vscode-editor-foreground)",
|
|
|
|
|
- padding: "6px 8px",
|
|
|
|
|
- borderRadius: "3px",
|
|
|
|
|
- margin: "10px 0 0 0",
|
|
|
|
|
- fontSize: "12px",
|
|
|
|
|
- }}>
|
|
|
|
|
- <i
|
|
|
|
|
- className="codicon codicon-warning"
|
|
|
|
|
- style={{
|
|
|
|
|
- marginRight: 6,
|
|
|
|
|
- fontSize: 16,
|
|
|
|
|
- color: "var(--vscode-errorForeground)",
|
|
|
|
|
- }}></i>
|
|
|
|
|
- <span>
|
|
|
|
|
- Uh-oh, this could be a problem on Kodu's end. We've been alerted and
|
|
|
|
|
- will resolve this ASAP. You can also{" "}
|
|
|
|
|
- <a
|
|
|
|
|
- href="https://discord.gg/claudedev"
|
|
|
|
|
- style={{ color: "inherit", textDecoration: "underline" }}>
|
|
|
|
|
- contact us on discord
|
|
|
|
|
- </a>
|
|
|
|
|
- .
|
|
|
|
|
- </span>
|
|
|
|
|
- </div>
|
|
|
|
|
- )} */}
|
|
|
|
|
- </>
|
|
|
|
|
- )}
|
|
|
|
|
- </>
|
|
|
|
|
- )
|
|
|
|
|
- case "api_req_finished":
|
|
|
|
|
- return null // we should never see this message type
|
|
|
|
|
- case "text":
|
|
|
|
|
- return <div>{renderMarkdown(message.text)}</div>
|
|
|
|
|
- case "user_feedback":
|
|
|
|
|
- return (
|
|
|
|
|
- <div
|
|
|
|
|
- style={{
|
|
|
|
|
- backgroundColor: "var(--vscode-badge-background)",
|
|
|
|
|
- color: "var(--vscode-badge-foreground)",
|
|
|
|
|
- borderRadius: "3px",
|
|
|
|
|
- padding: "8px",
|
|
|
|
|
- whiteSpace: "pre-line",
|
|
|
|
|
- wordWrap: "break-word",
|
|
|
|
|
- }}>
|
|
|
|
|
- <span style={{ display: "block" }}>{message.text}</span>
|
|
|
|
|
- {message.images && message.images.length > 0 && (
|
|
|
|
|
- <Thumbnails images={message.images} style={{ marginTop: "8px" }} />
|
|
|
|
|
- )}
|
|
|
|
|
- </div>
|
|
|
|
|
- )
|
|
|
|
|
- case "user_feedback_diff":
|
|
|
|
|
- const tool = JSON.parse(message.text || "{}") as ClaudeSayTool
|
|
|
|
|
- return (
|
|
|
|
|
- <div
|
|
|
|
|
- style={{
|
|
|
|
|
- backgroundColor: "var(--vscode-editor-inactiveSelectionBackground)",
|
|
|
|
|
- borderRadius: "3px",
|
|
|
|
|
- padding: "8px",
|
|
|
|
|
- whiteSpace: "pre-line",
|
|
|
|
|
- wordWrap: "break-word",
|
|
|
|
|
- }}>
|
|
|
|
|
- <span
|
|
|
|
|
- style={{
|
|
|
|
|
- display: "block",
|
|
|
|
|
- fontStyle: "italic",
|
|
|
|
|
- marginBottom: "8px",
|
|
|
|
|
- opacity: 0.8,
|
|
|
|
|
- }}>
|
|
|
|
|
- The user made the following changes:
|
|
|
|
|
- </span>
|
|
|
|
|
- <CodeBlock
|
|
|
|
|
- diff={tool.diff!}
|
|
|
|
|
- path={tool.path!}
|
|
|
|
|
- syntaxHighlighterStyle={syntaxHighlighterStyle}
|
|
|
|
|
- isExpanded={isExpanded}
|
|
|
|
|
- onToggleExpand={onToggleExpand}
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
- )
|
|
|
|
|
- case "error":
|
|
|
|
|
- return (
|
|
|
|
|
- <>
|
|
|
|
|
- {title && (
|
|
|
|
|
- <div style={headerStyle}>
|
|
|
|
|
- {icon}
|
|
|
|
|
- {title}
|
|
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
- <p style={{ ...pStyle, color: "var(--vscode-errorForeground)" }}>{message.text}</p>
|
|
|
|
|
- </>
|
|
|
|
|
- )
|
|
|
|
|
- case "completion_result":
|
|
|
|
|
- return (
|
|
|
|
|
- <>
|
|
|
|
|
- <div style={headerStyle}>
|
|
|
|
|
- {icon}
|
|
|
|
|
- {title}
|
|
|
|
|
- </div>
|
|
|
|
|
- <div style={{ color: "var(--vscode-charts-green)" }}>
|
|
|
|
|
- {renderMarkdown(message.text)}
|
|
|
|
|
- </div>
|
|
|
|
|
- </>
|
|
|
|
|
- )
|
|
|
|
|
- case "tool":
|
|
|
|
|
- return renderTool(message, headerStyle)
|
|
|
|
|
- default:
|
|
|
|
|
- return (
|
|
|
|
|
- <>
|
|
|
|
|
- {title && (
|
|
|
|
|
- <div style={headerStyle}>
|
|
|
|
|
- {icon}
|
|
|
|
|
- {title}
|
|
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
- <div>{renderMarkdown(message.text)}</div>
|
|
|
|
|
- </>
|
|
|
|
|
- )
|
|
|
|
|
- }
|
|
|
|
|
- case "ask":
|
|
|
|
|
- switch (message.ask) {
|
|
|
|
|
- case "tool":
|
|
|
|
|
- return renderTool(message, headerStyle)
|
|
|
|
|
- case "mistake_limit_reached":
|
|
|
|
|
- return (
|
|
|
|
|
- <>
|
|
|
|
|
- <div style={headerStyle}>
|
|
|
|
|
- {icon}
|
|
|
|
|
- {title}
|
|
|
|
|
- </div>
|
|
|
|
|
- <p style={{ ...pStyle, color: "var(--vscode-errorForeground)" }}>{message.text}</p>
|
|
|
|
|
- </>
|
|
|
|
|
- )
|
|
|
|
|
- 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 pStyle: React.CSSProperties = {
|
|
|
|
|
+ margin: 0,
|
|
|
|
|
+ whiteSpace: "pre-wrap",
|
|
|
|
|
+ wordBreak: "break-word",
|
|
|
|
|
+ overflowWrap: "anywhere",
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- const { command, output } = splitMessage(message.text || "")
|
|
|
|
|
- return (
|
|
|
|
|
- <>
|
|
|
|
|
- <div style={headerStyle}>
|
|
|
|
|
- {icon}
|
|
|
|
|
- {title}
|
|
|
|
|
- </div>
|
|
|
|
|
- <Terminal
|
|
|
|
|
- rawOutput={command + (output ? "\n" + output : "")}
|
|
|
|
|
- handleSendStdin={handleSendStdin}
|
|
|
|
|
- shouldAllowInput={!!isCommandExecuting && output.length > 0}
|
|
|
|
|
- />
|
|
|
|
|
- </>
|
|
|
|
|
- )
|
|
|
|
|
- case "completion_result":
|
|
|
|
|
- if (message.text) {
|
|
|
|
|
- return (
|
|
|
|
|
- <div>
|
|
|
|
|
- <div style={headerStyle}>
|
|
|
|
|
- {icon}
|
|
|
|
|
- {title}
|
|
|
|
|
- </div>
|
|
|
|
|
- <div style={{ color: "var(--vscode-charts-green)" }}>
|
|
|
|
|
- {renderMarkdown(message.text)}
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- )
|
|
|
|
|
- } else {
|
|
|
|
|
- return null // Don't render anything when we get a completion_result ask without text
|
|
|
|
|
- }
|
|
|
|
|
- case "followup":
|
|
|
|
|
- return (
|
|
|
|
|
- <>
|
|
|
|
|
- {title && (
|
|
|
|
|
- <div style={headerStyle}>
|
|
|
|
|
- {icon}
|
|
|
|
|
- {title}
|
|
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
- <div>{renderMarkdown(message.text)}</div>
|
|
|
|
|
- </>
|
|
|
|
|
- )
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ 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])
|
|
|
|
|
|
|
|
- const renderTool = (message: ClaudeMessage, headerStyle: React.CSSProperties) => {
|
|
|
|
|
- const tool = JSON.parse(message.text || "{}") as ClaudeSayTool
|
|
|
|
|
|
|
+ if (tool) {
|
|
|
const toolIcon = (name: string) => (
|
|
const toolIcon = (name: string) => (
|
|
|
<span
|
|
<span
|
|
|
className={`codicon codicon-${name}`}
|
|
className={`codicon codicon-${name}`}
|
|
@@ -633,27 +302,371 @@ const ChatRow: React.FC<ChatRowProps> = ({
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 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
|
|
|
|
|
|
|
+ switch (message.type) {
|
|
|
|
|
+ case "say":
|
|
|
|
|
+ switch (message.say) {
|
|
|
|
|
+ case "api_req_started":
|
|
|
|
|
+ return (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <div
|
|
|
|
|
+ style={{
|
|
|
|
|
+ ...headerStyle,
|
|
|
|
|
+ marginBottom: cost == null && apiRequestFailedMessage ? 10 : 0,
|
|
|
|
|
+ justifyContent: "space-between",
|
|
|
|
|
+ }}>
|
|
|
|
|
+ <div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
|
|
|
|
|
+ {icon}
|
|
|
|
|
+ {title}
|
|
|
|
|
+ {cost != null && cost > 0 && <VSCodeBadge>${Number(cost)?.toFixed(4)}</VSCodeBadge>}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <VSCodeButton appearance="icon" aria-label="Toggle Details" onClick={onToggleExpand}>
|
|
|
|
|
+ <span className={`codicon codicon-chevron-${isExpanded ? "up" : "down"}`}></span>
|
|
|
|
|
+ </VSCodeButton>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ {cost == null && apiRequestFailedMessage && (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <p style={{ ...pStyle, color: "var(--vscode-errorForeground)" }}>
|
|
|
|
|
+ {apiRequestFailedMessage}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ {/* {apiProvider === "kodu" && (
|
|
|
|
|
+ <div
|
|
|
|
|
+ style={{
|
|
|
|
|
+ display: "flex",
|
|
|
|
|
+ alignItems: "center",
|
|
|
|
|
+ backgroundColor:
|
|
|
|
|
+ "color-mix(in srgb, var(--vscode-errorForeground) 20%, transparent)",
|
|
|
|
|
+ color: "var(--vscode-editor-foreground)",
|
|
|
|
|
+ padding: "6px 8px",
|
|
|
|
|
+ borderRadius: "3px",
|
|
|
|
|
+ margin: "10px 0 0 0",
|
|
|
|
|
+ fontSize: "12px",
|
|
|
|
|
+ }}>
|
|
|
|
|
+ <i
|
|
|
|
|
+ className="codicon codicon-warning"
|
|
|
|
|
+ style={{
|
|
|
|
|
+ marginRight: 6,
|
|
|
|
|
+ fontSize: 16,
|
|
|
|
|
+ color: "var(--vscode-errorForeground)",
|
|
|
|
|
+ }}></i>
|
|
|
|
|
+ <span>
|
|
|
|
|
+ Uh-oh, this could be a problem on Kodu's end. We've been alerted and
|
|
|
|
|
+ will resolve this ASAP. You can also{" "}
|
|
|
|
|
+ <a
|
|
|
|
|
+ href="https://discord.gg/claudedev"
|
|
|
|
|
+ style={{ color: "inherit", textDecoration: "underline" }}>
|
|
|
|
|
+ contact us on discord
|
|
|
|
|
+ </a>
|
|
|
|
|
+ .
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )} */}
|
|
|
|
|
+ </>
|
|
|
|
|
+ )}
|
|
|
|
|
|
|
|
- return (
|
|
|
|
|
- <div
|
|
|
|
|
- style={{
|
|
|
|
|
- padding: "10px 6px 10px 15px",
|
|
|
|
|
- }}>
|
|
|
|
|
- {renderContent()}
|
|
|
|
|
- {isExpanded && message.say === "api_req_started" && (
|
|
|
|
|
- <div style={{ marginTop: "10px" }}>
|
|
|
|
|
- <CodeBlock
|
|
|
|
|
- code={JSON.stringify(JSON.parse(message.text || "{}").request, null, 2)}
|
|
|
|
|
- language="json"
|
|
|
|
|
- syntaxHighlighterStyle={syntaxHighlighterStyle}
|
|
|
|
|
- isExpanded={true}
|
|
|
|
|
- onToggleExpand={onToggleExpand}
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
- </div>
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ {isExpanded && (
|
|
|
|
|
+ <div style={{ marginTop: "10px" }}>
|
|
|
|
|
+ <CodeBlock
|
|
|
|
|
+ code={JSON.stringify(JSON.parse(message.text || "{}").request, null, 2)}
|
|
|
|
|
+ language="json"
|
|
|
|
|
+ syntaxHighlighterStyle={syntaxHighlighterStyle}
|
|
|
|
|
+ isExpanded={true}
|
|
|
|
|
+ onToggleExpand={onToggleExpand}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </>
|
|
|
|
|
+ )
|
|
|
|
|
+ case "api_req_finished":
|
|
|
|
|
+ return null // we should never see this message type
|
|
|
|
|
+ case "text":
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <Markdown syntaxHighlighterStyle={syntaxHighlighterStyle} markdown={message.text} />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )
|
|
|
|
|
+ case "user_feedback":
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div
|
|
|
|
|
+ style={{
|
|
|
|
|
+ backgroundColor: "var(--vscode-badge-background)",
|
|
|
|
|
+ color: "var(--vscode-badge-foreground)",
|
|
|
|
|
+ borderRadius: "3px",
|
|
|
|
|
+ padding: "8px",
|
|
|
|
|
+ whiteSpace: "pre-line",
|
|
|
|
|
+ wordWrap: "break-word",
|
|
|
|
|
+ }}>
|
|
|
|
|
+ <span style={{ display: "block" }}>{message.text}</span>
|
|
|
|
|
+ {message.images && message.images.length > 0 && (
|
|
|
|
|
+ <Thumbnails images={message.images} style={{ marginTop: "8px" }} />
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )
|
|
|
|
|
+ case "user_feedback_diff":
|
|
|
|
|
+ const tool = JSON.parse(message.text || "{}") as ClaudeSayTool
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div
|
|
|
|
|
+ style={{
|
|
|
|
|
+ backgroundColor: "var(--vscode-editor-inactiveSelectionBackground)",
|
|
|
|
|
+ borderRadius: "3px",
|
|
|
|
|
+ padding: "8px",
|
|
|
|
|
+ whiteSpace: "pre-line",
|
|
|
|
|
+ wordWrap: "break-word",
|
|
|
|
|
+ }}>
|
|
|
|
|
+ <span
|
|
|
|
|
+ style={{
|
|
|
|
|
+ display: "block",
|
|
|
|
|
+ fontStyle: "italic",
|
|
|
|
|
+ marginBottom: "8px",
|
|
|
|
|
+ opacity: 0.8,
|
|
|
|
|
+ }}>
|
|
|
|
|
+ The user made the following changes:
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <CodeBlock
|
|
|
|
|
+ diff={tool.diff!}
|
|
|
|
|
+ path={tool.path!}
|
|
|
|
|
+ syntaxHighlighterStyle={syntaxHighlighterStyle}
|
|
|
|
|
+ isExpanded={isExpanded}
|
|
|
|
|
+ onToggleExpand={onToggleExpand}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )
|
|
|
|
|
+ case "error":
|
|
|
|
|
+ return (
|
|
|
|
|
+ <>
|
|
|
|
|
+ {title && (
|
|
|
|
|
+ <div style={headerStyle}>
|
|
|
|
|
+ {icon}
|
|
|
|
|
+ {title}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ <p style={{ ...pStyle, color: "var(--vscode-errorForeground)" }}>{message.text}</p>
|
|
|
|
|
+ </>
|
|
|
|
|
+ )
|
|
|
|
|
+ case "completion_result":
|
|
|
|
|
+ return (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <div style={headerStyle}>
|
|
|
|
|
+ {icon}
|
|
|
|
|
+ {title}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div style={{ color: "var(--vscode-charts-green)" }}>
|
|
|
|
|
+ <Markdown syntaxHighlighterStyle={syntaxHighlighterStyle} markdown={message.text} />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </>
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ default:
|
|
|
|
|
+ return (
|
|
|
|
|
+ <>
|
|
|
|
|
+ {title && (
|
|
|
|
|
+ <div style={headerStyle}>
|
|
|
|
|
+ {icon}
|
|
|
|
|
+ {title}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <Markdown syntaxHighlighterStyle={syntaxHighlighterStyle} markdown={message.text} />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </>
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ case "ask":
|
|
|
|
|
+ switch (message.ask) {
|
|
|
|
|
+ case "mistake_limit_reached":
|
|
|
|
|
+ return (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <div style={headerStyle}>
|
|
|
|
|
+ {icon}
|
|
|
|
|
+ {title}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <p style={{ ...pStyle, color: "var(--vscode-errorForeground)" }}>{message.text}</p>
|
|
|
|
|
+ </>
|
|
|
|
|
+ )
|
|
|
|
|
+ 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 (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <div style={headerStyle}>
|
|
|
|
|
+ {icon}
|
|
|
|
|
+ {title}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <Terminal
|
|
|
|
|
+ rawOutput={command + (output ? "\n" + output : "")}
|
|
|
|
|
+ handleSendStdin={handleSendStdin}
|
|
|
|
|
+ shouldAllowInput={!!isCommandExecuting && output.length > 0}
|
|
|
|
|
+ />
|
|
|
|
|
+ </>
|
|
|
|
|
+ )
|
|
|
|
|
+ case "completion_result":
|
|
|
|
|
+ if (message.text) {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <div style={headerStyle}>
|
|
|
|
|
+ {icon}
|
|
|
|
|
+ {title}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div style={{ color: "var(--vscode-charts-green)" }}>
|
|
|
|
|
+ <Markdown syntaxHighlighterStyle={syntaxHighlighterStyle} markdown={message.text} />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )
|
|
|
|
|
+ } else {
|
|
|
|
|
+ return null // Don't render anything when we get a completion_result ask without text
|
|
|
|
|
+ }
|
|
|
|
|
+ case "followup":
|
|
|
|
|
+ return (
|
|
|
|
|
+ <>
|
|
|
|
|
+ {title && (
|
|
|
|
|
+ <div style={headerStyle}>
|
|
|
|
|
+ {icon}
|
|
|
|
|
+ {title}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <Markdown syntaxHighlighterStyle={syntaxHighlighterStyle} markdown={message.text} />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </>
|
|
|
|
|
+ )
|
|
|
|
|
+ default:
|
|
|
|
|
+ return null
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-export default ChatRow
|
|
|
|
|
|
|
+const ProgressIndicator = () => (
|
|
|
|
|
+ <div
|
|
|
|
|
+ style={{
|
|
|
|
|
+ width: "16px",
|
|
|
|
|
+ height: "16px",
|
|
|
|
|
+ display: "flex",
|
|
|
|
|
+ alignItems: "center",
|
|
|
|
|
+ justifyContent: "center",
|
|
|
|
|
+ }}>
|
|
|
|
|
+ <div style={{ transform: "scale(0.55)", transformOrigin: "center" }}>
|
|
|
|
|
+ <VSCodeProgressRing />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+const Markdown = memo(
|
|
|
|
|
+ ({ syntaxHighlighterStyle, markdown }: { syntaxHighlighterStyle: SyntaxHighlighterStyle; 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(/<thinking>([\s\S]*?)<\/thinking>/g, (match, content) => {
|
|
|
|
|
+ return `_<thinking>_\n\n${content}\n\n_</thinking>_`
|
|
|
|
|
+ })
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div style={{ wordBreak: "break-word", overflowWrap: "anywhere" }}>
|
|
|
|
|
+ <ReactMarkdown
|
|
|
|
|
+ children={parsed}
|
|
|
|
|
+ components={{
|
|
|
|
|
+ p(props) {
|
|
|
|
|
+ const { style, ...rest } = props
|
|
|
|
|
+ return (
|
|
|
|
|
+ <p
|
|
|
|
|
+ style={{
|
|
|
|
|
+ ...style,
|
|
|
|
|
+ margin: 0,
|
|
|
|
|
+ marginTop: 0,
|
|
|
|
|
+ marginBottom: 0,
|
|
|
|
|
+ whiteSpace: "pre-wrap",
|
|
|
|
|
+ wordBreak: "break-word",
|
|
|
|
|
+ overflowWrap: "anywhere",
|
|
|
|
|
+ }}
|
|
|
|
|
+ {...rest}
|
|
|
|
|
+ />
|
|
|
|
|
+ )
|
|
|
|
|
+ },
|
|
|
|
|
+ ol(props) {
|
|
|
|
|
+ const { style, ...rest } = props
|
|
|
|
|
+ return (
|
|
|
|
|
+ <ol
|
|
|
|
|
+ style={{
|
|
|
|
|
+ ...style,
|
|
|
|
|
+ padding: "0 0 0 20px",
|
|
|
|
|
+ margin: "10px 0",
|
|
|
|
|
+ wordBreak: "break-word",
|
|
|
|
|
+ overflowWrap: "anywhere",
|
|
|
|
|
+ }}
|
|
|
|
|
+ {...rest}
|
|
|
|
|
+ />
|
|
|
|
|
+ )
|
|
|
|
|
+ },
|
|
|
|
|
+ ul(props) {
|
|
|
|
|
+ const { style, ...rest } = props
|
|
|
|
|
+ return (
|
|
|
|
|
+ <ul
|
|
|
|
|
+ style={{
|
|
|
|
|
+ ...style,
|
|
|
|
|
+ padding: "0 0 0 20px",
|
|
|
|
|
+ margin: "10px 0",
|
|
|
|
|
+ wordBreak: "break-word",
|
|
|
|
|
+ overflowWrap: "anywhere",
|
|
|
|
|
+ }}
|
|
|
|
|
+ {...rest}
|
|
|
|
|
+ />
|
|
|
|
|
+ )
|
|
|
|
|
+ },
|
|
|
|
|
+ // 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 ? (
|
|
|
|
|
+ <SyntaxHighlighter
|
|
|
|
|
+ {...(rest as any)} // will be passed down to pre
|
|
|
|
|
+ PreTag="div"
|
|
|
|
|
+ children={String(children).replace(/\n$/, "")}
|
|
|
|
|
+ language={match[1]}
|
|
|
|
|
+ style={{
|
|
|
|
|
+ ...syntaxHighlighterStyle,
|
|
|
|
|
+ 'code[class*="language-"]': {
|
|
|
|
|
+ background: "var(--vscode-editor-background)",
|
|
|
|
|
+ },
|
|
|
|
|
+ 'pre[class*="language-"]': {
|
|
|
|
|
+ background: "var(--vscode-editor-background)",
|
|
|
|
|
+ },
|
|
|
|
|
+ }}
|
|
|
|
|
+ customStyle={{
|
|
|
|
|
+ overflowX: "auto",
|
|
|
|
|
+ overflowY: "hidden",
|
|
|
|
|
+ maxWidth: "100%",
|
|
|
|
|
+ margin: 0,
|
|
|
|
|
+ padding: "10px",
|
|
|
|
|
+ // important to note that min-width: max-content is not required here how it is in CodeBlock.tsx
|
|
|
|
|
+ borderRadius: 3,
|
|
|
|
|
+ border: "1px solid var(--vscode-sideBar-border)",
|
|
|
|
|
+ fontSize: "var(--vscode-editor-font-size)",
|
|
|
|
|
+ lineHeight: "var(--vscode-editor-line-height)",
|
|
|
|
|
+ fontFamily: "var(--vscode-editor-font-family)",
|
|
|
|
|
+ }}
|
|
|
|
|
+ />
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <code
|
|
|
|
|
+ {...rest}
|
|
|
|
|
+ className={className}
|
|
|
|
|
+ style={{
|
|
|
|
|
+ whiteSpace: "pre-line",
|
|
|
|
|
+ wordBreak: "break-word",
|
|
|
|
|
+ overflowWrap: "anywhere",
|
|
|
|
|
+ }}>
|
|
|
|
|
+ {children}
|
|
|
|
|
+ </code>
|
|
|
|
|
+ )
|
|
|
|
|
+ },
|
|
|
|
|
+ }}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+)
|