mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 12:21:13 -05:00
Performance optimizations with memoization and useCallbacks
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
|
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
|
||||||
import { ApiConfiguration } from "../../../src/shared/api"
|
import { ApiConfiguration } from "../../../src/shared/api"
|
||||||
|
import { memo } from "react"
|
||||||
// import VSCodeButtonLink from "./VSCodeButtonLink"
|
// import VSCodeButtonLink from "./VSCodeButtonLink"
|
||||||
// import { getOpenRouterAuthUrl } from "./ApiOptions"
|
// import { getOpenRouterAuthUrl } from "./ApiOptions"
|
||||||
// import { vscode } from "../utils/vscode"
|
// import { vscode } from "../utils/vscode"
|
||||||
@@ -89,4 +90,4 @@ const Announcement = ({ version, hideAnnouncement, apiConfiguration, vscodeUriSc
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Announcement
|
export default memo(Announcement)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
VSCodeRadioGroup,
|
VSCodeRadioGroup,
|
||||||
VSCodeTextField,
|
VSCodeTextField,
|
||||||
} from "@vscode/webview-ui-toolkit/react"
|
} from "@vscode/webview-ui-toolkit/react"
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from "react"
|
import { memo, useCallback, useEffect, useMemo, useState } from "react"
|
||||||
import {
|
import {
|
||||||
ApiConfiguration,
|
ApiConfiguration,
|
||||||
ModelInfo,
|
ModelInfo,
|
||||||
@@ -31,7 +31,7 @@ interface ApiOptionsProps {
|
|||||||
apiErrorMessage?: string
|
apiErrorMessage?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const ApiOptions: React.FC<ApiOptionsProps> = ({ showModelOptions, apiErrorMessage }) => {
|
const ApiOptions = ({ showModelOptions, apiErrorMessage }: ApiOptionsProps) => {
|
||||||
const { apiConfiguration, setApiConfiguration, uriScheme } = useExtensionState()
|
const { apiConfiguration, setApiConfiguration, uriScheme } = useExtensionState()
|
||||||
const [ollamaModels, setOllamaModels] = useState<string[]>([])
|
const [ollamaModels, setOllamaModels] = useState<string[]>([])
|
||||||
|
|
||||||
@@ -550,4 +550,4 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ApiOptions
|
export default memo(ApiOptions)
|
||||||
|
|||||||
@@ -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 React, { memo, useMemo } from "react"
|
||||||
import Markdown from "react-markdown"
|
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,
|
||||||
}) => {
|
}: ChatRowProps) => {
|
||||||
const cost = message.text != null && message.say === "api_req_started" ? JSON.parse(message.text).cost : undefined
|
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 normalColor = "var(--vscode-foreground)"
|
||||||
const errorColor = "var(--vscode-errorForeground)"
|
const errorColor = "var(--vscode-errorForeground)"
|
||||||
const successColor = "var(--vscode-charts-green)"
|
const successColor = "var(--vscode-charts-green)"
|
||||||
|
|
||||||
const ProgressIndicator = (
|
const [icon, title] = useMemo(() => {
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: "16px",
|
|
||||||
height: "16px",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}>
|
|
||||||
<div style={{ transform: "scale(0.55)", transformOrigin: "center" }}>
|
|
||||||
<VSCodeProgressRing />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
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,120 +127,7 @@ 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 renderContent = () => {
|
|
||||||
const [icon, title] = getIconAndTitle(message.type === "ask" ? message.ask : message.say)
|
|
||||||
|
|
||||||
const headerStyle: React.CSSProperties = {
|
const headerStyle: React.CSSProperties = {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -251,237 +143,14 @@ const ChatRow: React.FC<ChatRowProps> = ({
|
|||||||
overflowWrap: "anywhere",
|
overflowWrap: "anywhere",
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (message.type) {
|
const tool = useMemo(() => {
|
||||||
case "say":
|
if (message.ask === "tool" || message.say === "tool") {
|
||||||
switch (message.say) {
|
return JSON.parse(message.text || "{}") as ClaudeSayTool
|
||||||
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() + " ",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
|
}, [message.ask, message.say, message.text])
|
||||||
|
|
||||||
const { command, output } = splitMessage(message.text || "")
|
if (tool) {
|
||||||
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 renderTool = (message: ClaudeMessage, headerStyle: React.CSSProperties) => {
|
|
||||||
const tool = JSON.parse(message.text || "{}") as ClaudeSayTool
|
|
||||||
const toolIcon = (name: string) => (
|
const toolIcon = (name: string) => (
|
||||||
<span
|
<span
|
||||||
className={`codicon codicon-${name}`}
|
className={`codicon codicon-${name}`}
|
||||||
@@ -633,15 +302,68 @@ 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 (
|
return (
|
||||||
|
<>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
padding: "10px 6px 10px 15px",
|
...headerStyle,
|
||||||
|
marginBottom: cost == null && apiRequestFailedMessage ? 10 : 0,
|
||||||
|
justifyContent: "space-between",
|
||||||
}}>
|
}}>
|
||||||
{renderContent()}
|
<div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
|
||||||
{isExpanded && message.say === "api_req_started" && (
|
{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>
|
||||||
|
)} */}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isExpanded && (
|
||||||
<div style={{ marginTop: "10px" }}>
|
<div style={{ marginTop: "10px" }}>
|
||||||
<CodeBlock
|
<CodeBlock
|
||||||
code={JSON.stringify(JSON.parse(message.text || "{}").request, null, 2)}
|
code={JSON.stringify(JSON.parse(message.text || "{}").request, null, 2)}
|
||||||
@@ -652,8 +374,299 @@ const ChatRow: React.FC<ChatRowProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
)
|
||||||
export default ChatRow
|
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ const ChatView = ({
|
|||||||
}
|
}
|
||||||
}, [messages.length])
|
}, [messages.length])
|
||||||
|
|
||||||
const handleSendMessage = () => {
|
const handleSendMessage = useCallback(() => {
|
||||||
const text = inputValue.trim()
|
const text = inputValue.trim()
|
||||||
if (text || selectedImages.length > 0) {
|
if (text || selectedImages.length > 0) {
|
||||||
if (messages.length === 0) {
|
if (messages.length === 0) {
|
||||||
@@ -248,9 +248,10 @@ const ChatView = ({
|
|||||||
// setPrimaryButtonText(undefined)
|
// setPrimaryButtonText(undefined)
|
||||||
// setSecondaryButtonText(undefined)
|
// setSecondaryButtonText(undefined)
|
||||||
}
|
}
|
||||||
}
|
}, [inputValue, selectedImages, messages.length, claudeAsk])
|
||||||
|
|
||||||
const handleSendStdin = (text: string) => {
|
const handleSendStdin = useCallback(
|
||||||
|
(text: string) => {
|
||||||
if (claudeAsk === "command_output") {
|
if (claudeAsk === "command_output") {
|
||||||
vscode.postMessage({
|
vscode.postMessage({
|
||||||
type: "askResponse",
|
type: "askResponse",
|
||||||
@@ -262,12 +263,18 @@ const ChatView = ({
|
|||||||
// setTextAreaDisabled(true)
|
// setTextAreaDisabled(true)
|
||||||
// setEnableButtons(false)
|
// setEnableButtons(false)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
[claudeAsk]
|
||||||
|
)
|
||||||
|
|
||||||
|
const startNewTask = useCallback(() => {
|
||||||
|
vscode.postMessage({ type: "clearTask" })
|
||||||
|
}, [])
|
||||||
|
|
||||||
/*
|
/*
|
||||||
This logic depends on the useEffect[messages] above to set claudeAsk, after which buttons are shown and we then send an askResponse to the extension.
|
This logic depends on the useEffect[messages] above to set claudeAsk, after which buttons are shown and we then send an askResponse to the extension.
|
||||||
*/
|
*/
|
||||||
const handlePrimaryButtonClick = () => {
|
const handlePrimaryButtonClick = useCallback(() => {
|
||||||
switch (claudeAsk) {
|
switch (claudeAsk) {
|
||||||
case "api_req_failed":
|
case "api_req_failed":
|
||||||
case "command":
|
case "command":
|
||||||
@@ -288,9 +295,9 @@ const ChatView = ({
|
|||||||
setEnableButtons(false)
|
setEnableButtons(false)
|
||||||
// setPrimaryButtonText(undefined)
|
// setPrimaryButtonText(undefined)
|
||||||
// setSecondaryButtonText(undefined)
|
// setSecondaryButtonText(undefined)
|
||||||
}
|
}, [claudeAsk, startNewTask])
|
||||||
|
|
||||||
const handleSecondaryButtonClick = () => {
|
const handleSecondaryButtonClick = useCallback(() => {
|
||||||
switch (claudeAsk) {
|
switch (claudeAsk) {
|
||||||
case "api_req_failed":
|
case "api_req_failed":
|
||||||
case "mistake_limit_reached":
|
case "mistake_limit_reached":
|
||||||
@@ -307,29 +314,32 @@ const ChatView = ({
|
|||||||
setEnableButtons(false)
|
setEnableButtons(false)
|
||||||
// setPrimaryButtonText(undefined)
|
// setPrimaryButtonText(undefined)
|
||||||
// setSecondaryButtonText(undefined)
|
// setSecondaryButtonText(undefined)
|
||||||
}
|
}, [claudeAsk, startNewTask])
|
||||||
|
|
||||||
const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
const handleKeyDown = useCallback(
|
||||||
|
(event: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
const isComposing = event.nativeEvent?.isComposing ?? false
|
const isComposing = event.nativeEvent?.isComposing ?? false
|
||||||
if (event.key === "Enter" && !event.shiftKey && !isComposing) {
|
if (event.key === "Enter" && !event.shiftKey && !isComposing) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
handleSendMessage()
|
handleSendMessage()
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
[handleSendMessage]
|
||||||
|
)
|
||||||
|
|
||||||
const handleTaskCloseButtonClick = () => {
|
const handleTaskCloseButtonClick = useCallback(() => {
|
||||||
startNewTask()
|
startNewTask()
|
||||||
}
|
}, [startNewTask])
|
||||||
|
|
||||||
const startNewTask = () => {
|
const selectImages = useCallback(() => {
|
||||||
vscode.postMessage({ type: "clearTask" })
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectImages = () => {
|
|
||||||
vscode.postMessage({ type: "selectImages" })
|
vscode.postMessage({ type: "selectImages" })
|
||||||
}
|
}, [])
|
||||||
|
|
||||||
const handlePaste = async (e: React.ClipboardEvent) => {
|
const shouldDisableImages =
|
||||||
|
!selectedModelSupportsImages || textAreaDisabled || selectedImages.length >= MAX_IMAGES_PER_MESSAGE
|
||||||
|
|
||||||
|
const handlePaste = useCallback(
|
||||||
|
async (e: React.ClipboardEvent) => {
|
||||||
const items = e.clipboardData.items
|
const items = e.clipboardData.items
|
||||||
const acceptedTypes = ["png", "jpeg", "webp"] // supported by anthropic and openrouter (jpg is just a file extension but the image will be recognized as jpeg)
|
const acceptedTypes = ["png", "jpeg", "webp"] // supported by anthropic and openrouter (jpg is just a file extension but the image will be recognized as jpeg)
|
||||||
const imageItems = Array.from(items).filter((item) => {
|
const imageItems = Array.from(items).filter((item) => {
|
||||||
@@ -367,7 +377,9 @@ const ChatView = ({
|
|||||||
console.warn("No valid images were processed")
|
console.warn("No valid images were processed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
[shouldDisableImages, setSelectedImages]
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedImages.length === 0) {
|
if (selectedImages.length === 0) {
|
||||||
@@ -469,8 +481,21 @@ const ChatView = ({
|
|||||||
return text
|
return text
|
||||||
}, [task])
|
}, [task])
|
||||||
|
|
||||||
const shouldDisableImages =
|
const itemContent = useCallback(
|
||||||
!selectedModelSupportsImages || textAreaDisabled || selectedImages.length >= MAX_IMAGES_PER_MESSAGE
|
(index: number, message: any) => (
|
||||||
|
<ChatRow
|
||||||
|
key={message.ts}
|
||||||
|
message={message}
|
||||||
|
syntaxHighlighterStyle={syntaxHighlighterStyle}
|
||||||
|
isExpanded={expandedRows[message.ts] || false}
|
||||||
|
onToggleExpand={() => toggleRowExpansion(message.ts)}
|
||||||
|
lastModifiedMessage={modifiedMessages.at(-1)}
|
||||||
|
isLast={index === visibleMessages.length - 1}
|
||||||
|
handleSendStdin={handleSendStdin}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[expandedRows, syntaxHighlighterStyle, modifiedMessages, visibleMessages.length, handleSendStdin]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -540,18 +565,7 @@ const ChatView = ({
|
|||||||
// }}
|
// }}
|
||||||
increaseViewportBy={{ top: 0, bottom: Number.MAX_SAFE_INTEGER }} // hack to make sure the last message is always rendered to get truly perfect scroll to bottom animation when new messages are added (Number.MAX_SAFE_INTEGER is safe for arithmetic operations, which is all virtuoso uses this value for in src/sizeRangeSystem.ts)
|
increaseViewportBy={{ top: 0, bottom: Number.MAX_SAFE_INTEGER }} // hack to make sure the last message is always rendered to get truly perfect scroll to bottom animation when new messages are added (Number.MAX_SAFE_INTEGER is safe for arithmetic operations, which is all virtuoso uses this value for in src/sizeRangeSystem.ts)
|
||||||
data={visibleMessages} // messages is the raw format returned by extension, modifiedMessages is the manipulated structure that combines certain messages of related type, and visibleMessages is the filtered structure that removes messages that should not be rendered
|
data={visibleMessages} // messages is the raw format returned by extension, modifiedMessages is the manipulated structure that combines certain messages of related type, and visibleMessages is the filtered structure that removes messages that should not be rendered
|
||||||
itemContent={(index, message) => (
|
itemContent={itemContent}
|
||||||
<ChatRow
|
|
||||||
key={message.ts}
|
|
||||||
message={message}
|
|
||||||
syntaxHighlighterStyle={syntaxHighlighterStyle}
|
|
||||||
isExpanded={expandedRows[message.ts] || false}
|
|
||||||
onToggleExpand={() => toggleRowExpansion(message.ts)}
|
|
||||||
lastModifiedMessage={modifiedMessages.at(-1)}
|
|
||||||
isLast={index === visibleMessages.length - 1}
|
|
||||||
handleSendStdin={handleSendStdin}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useMemo } from "react"
|
import { memo, useMemo } from "react"
|
||||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
|
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
|
||||||
import { getLanguageFromPath } from "../utils/getLanguageFromPath"
|
import { getLanguageFromPath } from "../utils/getLanguageFromPath"
|
||||||
import { SyntaxHighlighterStyle } from "../utils/getSyntaxHighlighterStyleFromTheme"
|
import { SyntaxHighlighterStyle } from "../utils/getSyntaxHighlighterStyleFromTheme"
|
||||||
@@ -67,6 +67,14 @@ interface CodeBlockProps {
|
|||||||
onToggleExpand: () => void
|
onToggleExpand: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
We need to remove leading non-alphanumeric characters from the path in order for our leading ellipses trick to work.
|
||||||
|
^: Anchors the match to the start of the string.
|
||||||
|
[^a-zA-Z0-9]+: Matches one or more characters that are not alphanumeric.
|
||||||
|
The replace method removes these matched characters, effectively trimming the string up to the first alphanumeric character.
|
||||||
|
*/
|
||||||
|
const removeLeadingNonAlphanumeric = (path: string): string => path.replace(/^[^a-zA-Z0-9]+/, "")
|
||||||
|
|
||||||
const CodeBlock = ({
|
const CodeBlock = ({
|
||||||
code,
|
code,
|
||||||
diff,
|
diff,
|
||||||
@@ -76,15 +84,6 @@ const CodeBlock = ({
|
|||||||
isExpanded,
|
isExpanded,
|
||||||
onToggleExpand,
|
onToggleExpand,
|
||||||
}: CodeBlockProps) => {
|
}: CodeBlockProps) => {
|
||||||
/*
|
|
||||||
We need to remove leading non-alphanumeric characters from the path in order for our leading ellipses trick to work.
|
|
||||||
|
|
||||||
^: Anchors the match to the start of the string.
|
|
||||||
[^a-zA-Z0-9]+: Matches one or more characters that are not alphanumeric.
|
|
||||||
The replace method removes these matched characters, effectively trimming the string up to the first alphanumeric character.
|
|
||||||
*/
|
|
||||||
const removeLeadingNonAlphanumeric = (path: string): string => path.replace(/^[^a-zA-Z0-9]+/, "")
|
|
||||||
|
|
||||||
const inferredLanguage = useMemo(
|
const inferredLanguage = useMemo(
|
||||||
() => code && (language ?? (path ? getLanguageFromPath(path) : undefined)),
|
() => code && (language ?? (path ? getLanguageFromPath(path) : undefined)),
|
||||||
[path, language, code]
|
[path, language, code]
|
||||||
@@ -168,5 +167,5 @@ const CodeBlock = ({
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// memo does shallow comparison of props, so if you need it to re-render when a nested object changes, you need to pass a custom comparison function
|
||||||
export default CodeBlock
|
export default memo(CodeBlock)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||||
import { useExtensionState } from "../context/ExtensionStateContext"
|
import { useExtensionState } from "../context/ExtensionStateContext"
|
||||||
import { vscode } from "../utils/vscode"
|
import { vscode } from "../utils/vscode"
|
||||||
|
import { memo } from "react"
|
||||||
|
|
||||||
type HistoryPreviewProps = {
|
type HistoryPreviewProps = {
|
||||||
showHistoryView: () => void
|
showHistoryView: () => void
|
||||||
@@ -148,4 +149,4 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default HistoryPreview
|
export default memo(HistoryPreview)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { VSCodeButton, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
|||||||
import { useExtensionState } from "../context/ExtensionStateContext"
|
import { useExtensionState } from "../context/ExtensionStateContext"
|
||||||
import { vscode } from "../utils/vscode"
|
import { vscode } from "../utils/vscode"
|
||||||
import { Virtuoso } from "react-virtuoso"
|
import { Virtuoso } from "react-virtuoso"
|
||||||
import { useMemo, useState } from "react"
|
import { memo, useMemo, useState } from "react"
|
||||||
|
|
||||||
type HistoryViewProps = {
|
type HistoryViewProps = {
|
||||||
onDone: () => void
|
onDone: () => void
|
||||||
@@ -20,10 +20,6 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
|||||||
vscode.postMessage({ type: "deleteTaskWithId", text: id })
|
vscode.postMessage({ type: "deleteTaskWithId", text: id })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleExportMd = (id: string) => {
|
|
||||||
vscode.postMessage({ type: "exportTaskWithId", text: id })
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatDate = (timestamp: number) => {
|
const formatDate = (timestamp: number) => {
|
||||||
const date = new Date(timestamp)
|
const date = new Date(timestamp)
|
||||||
return date
|
return date
|
||||||
@@ -63,18 +59,6 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const ExportButton = ({ itemId }: { itemId: string }) => (
|
|
||||||
<VSCodeButton
|
|
||||||
className="export-button"
|
|
||||||
appearance="icon"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
handleExportMd(itemId)
|
|
||||||
}}>
|
|
||||||
<div style={{ fontSize: "11px", fontWeight: 500, opacity: 1 }}>EXPORT</div>
|
|
||||||
</VSCodeButton>
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<style>
|
<style>
|
||||||
@@ -369,4 +353,16 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default HistoryView
|
const ExportButton = ({ itemId }: { itemId: string }) => (
|
||||||
|
<VSCodeButton
|
||||||
|
className="export-button"
|
||||||
|
appearance="icon"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
vscode.postMessage({ type: "exportTaskWithId", text: itemId })
|
||||||
|
}}>
|
||||||
|
<div style={{ fontSize: "11px", fontWeight: 500, opacity: 1 }}>EXPORT</div>
|
||||||
|
</VSCodeButton>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default memo(HistoryView)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { VSCodeButton, VSCodeCheckbox, VSCodeLink, VSCodeTextArea } from "@vscode/webview-ui-toolkit/react"
|
import { VSCodeButton, VSCodeCheckbox, VSCodeLink, VSCodeTextArea } from "@vscode/webview-ui-toolkit/react"
|
||||||
import { useEffect, useState } from "react"
|
import { memo, useEffect, useState } from "react"
|
||||||
import { useExtensionState } from "../context/ExtensionStateContext"
|
import { useExtensionState } from "../context/ExtensionStateContext"
|
||||||
import { validateApiConfiguration } from "../utils/validate"
|
import { validateApiConfiguration } from "../utils/validate"
|
||||||
import { vscode } from "../utils/vscode"
|
import { vscode } from "../utils/vscode"
|
||||||
@@ -162,4 +162,4 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SettingsView
|
export default memo(SettingsView)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||||
import React, { useEffect, useRef, useState } from "react"
|
import React, { memo, useEffect, useRef, useState } from "react"
|
||||||
import { useWindowSize } from "react-use"
|
import { useWindowSize } from "react-use"
|
||||||
import { ClaudeMessage } from "../../../src/shared/ExtensionMessage"
|
import { ClaudeMessage } from "../../../src/shared/ExtensionMessage"
|
||||||
import { useExtensionState } from "../context/ExtensionStateContext"
|
import { useExtensionState } from "../context/ExtensionStateContext"
|
||||||
@@ -89,24 +89,6 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
|||||||
}
|
}
|
||||||
}, [task.text, windowWidth])
|
}, [task.text, windowWidth])
|
||||||
|
|
||||||
const toggleExpand = () => setIsExpanded(!isExpanded)
|
|
||||||
|
|
||||||
const handleDownload = () => {
|
|
||||||
vscode.postMessage({ type: "exportCurrentTask" })
|
|
||||||
}
|
|
||||||
|
|
||||||
const ExportButton = () => (
|
|
||||||
<VSCodeButton
|
|
||||||
appearance="icon"
|
|
||||||
onClick={handleDownload}
|
|
||||||
style={{
|
|
||||||
marginBottom: "-2px",
|
|
||||||
marginRight: "-2.5px",
|
|
||||||
}}>
|
|
||||||
<div style={{ fontSize: "10.5px", fontWeight: "bold", opacity: 0.6 }}>EXPORT</div>
|
|
||||||
</VSCodeButton>
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: "10px 13px 10px 13px" }}>
|
<div style={{ padding: "10px 13px 10px 13px" }}>
|
||||||
<div
|
<div
|
||||||
@@ -182,7 +164,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
|||||||
paddingLeft: 3,
|
paddingLeft: 3,
|
||||||
backgroundColor: "var(--vscode-badge-background)",
|
backgroundColor: "var(--vscode-badge-background)",
|
||||||
}}
|
}}
|
||||||
onClick={toggleExpand}>
|
onClick={() => setIsExpanded(!isExpanded)}>
|
||||||
See more
|
See more
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -197,7 +179,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
|||||||
textAlign: "right",
|
textAlign: "right",
|
||||||
paddingRight: 0,
|
paddingRight: 0,
|
||||||
}}
|
}}
|
||||||
onClick={toggleExpand}>
|
onClick={() => setIsExpanded(!isExpanded)}>
|
||||||
See less
|
See less
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -298,4 +280,16 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TaskHeader
|
const ExportButton = () => (
|
||||||
|
<VSCodeButton
|
||||||
|
appearance="icon"
|
||||||
|
onClick={() => vscode.postMessage({ type: "exportCurrentTask" })}
|
||||||
|
style={{
|
||||||
|
marginBottom: "-2px",
|
||||||
|
marginRight: "-2.5px",
|
||||||
|
}}>
|
||||||
|
<div style={{ fontSize: "10.5px", fontWeight: "bold", opacity: 0.6 }}>EXPORT</div>
|
||||||
|
</VSCodeButton>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default memo(TaskHeader)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect, useRef, useMemo } from "react"
|
import React, { useState, useEffect, useRef, useMemo, memo } from "react"
|
||||||
import DynamicTextArea from "react-textarea-autosize"
|
import DynamicTextArea from "react-textarea-autosize"
|
||||||
import stripAnsi from "strip-ansi"
|
import stripAnsi from "strip-ansi"
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ Inspired by https://phuoc.ng/collection/mirror-a-text-area/create-your-own-custo
|
|||||||
Note: Even though vscode exposes var(--vscode-terminalCursor-foreground) it does not render in front of a color that isn't var(--vscode-terminal-background), and it turns out a lot of themes don't even define some/any of these terminal color variables. Very odd behavior, so try changing themes/color variables if you don't see the caret.
|
Note: Even though vscode exposes var(--vscode-terminalCursor-foreground) it does not render in front of a color that isn't var(--vscode-terminal-background), and it turns out a lot of themes don't even define some/any of these terminal color variables. Very odd behavior, so try changing themes/color variables if you don't see the caret.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const Terminal: React.FC<TerminalProps> = ({ rawOutput, handleSendStdin, shouldAllowInput }) => {
|
const Terminal = ({ rawOutput, handleSendStdin, shouldAllowInput }: TerminalProps) => {
|
||||||
const [userInput, setUserInput] = useState("")
|
const [userInput, setUserInput] = useState("")
|
||||||
const [isFocused, setIsFocused] = useState(false) // Initially not focused
|
const [isFocused, setIsFocused] = useState(false) // Initially not focused
|
||||||
const textAreaRef = useRef<HTMLTextAreaElement>(null)
|
const textAreaRef = useRef<HTMLTextAreaElement>(null)
|
||||||
@@ -348,4 +348,4 @@ const Terminal: React.FC<TerminalProps> = ({ rawOutput, handleSendStdin, shouldA
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Terminal
|
export default memo(Terminal)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useRef, useLayoutEffect } from "react"
|
import React, { useState, useRef, useLayoutEffect, memo } from "react"
|
||||||
import { useWindowSize } from "react-use"
|
import { useWindowSize } from "react-use"
|
||||||
|
|
||||||
interface ThumbnailsProps {
|
interface ThumbnailsProps {
|
||||||
@@ -8,7 +8,7 @@ interface ThumbnailsProps {
|
|||||||
onHeightChange?: (height: number) => void
|
onHeightChange?: (height: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const Thumbnails: React.FC<ThumbnailsProps> = ({ images, style, setImages, onHeightChange }) => {
|
const Thumbnails = ({ images, style, setImages, onHeightChange }: ThumbnailsProps) => {
|
||||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null)
|
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null)
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
const { width } = useWindowSize()
|
const { width } = useWindowSize()
|
||||||
@@ -88,4 +88,4 @@ const Thumbnails: React.FC<ThumbnailsProps> = ({ images, style, setImages, onHei
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Thumbnails
|
export default memo(Thumbnails)
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ import { validateApiConfiguration } from "../utils/validate"
|
|||||||
import { vscode } from "../utils/vscode"
|
import { vscode } from "../utils/vscode"
|
||||||
import ApiOptions from "./ApiOptions"
|
import ApiOptions from "./ApiOptions"
|
||||||
|
|
||||||
interface WelcomeViewProps {}
|
const WelcomeView = () => {
|
||||||
|
|
||||||
const WelcomeView: React.FC<WelcomeViewProps> = () => {
|
|
||||||
const { apiConfiguration } = useExtensionState()
|
const { apiConfiguration } = useExtensionState()
|
||||||
|
|
||||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
|
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
|
||||||
|
|||||||
Reference in New Issue
Block a user