mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 04:11:10 -05:00
Add memory optimizations, retry failed requests, markdown support
- Move isExpanded state up into ChatView to fix issue where virtualized list would reset ChatRow state - Add ability to retry failed requests - Add markdown rendering
This commit is contained in:
@@ -50,6 +50,7 @@ RULES
|
||||
- Your goal is to try to accomplish the user's task, NOT engage in a back and forth conversation.
|
||||
- NEVER end completion_attempt with a question or request to engage in further conversation! Formulate the end of your result in a way that is final and does not require further input from the user.
|
||||
- NEVER start your responses with affirmations like "Certaintly", "Okay", "Sure", "Great", etc. You should NOT be conversational in your responses, but rather direct and to the point.
|
||||
- Feel free to use markdown as much as you'd like in your responses. When using code blocks, always include a language specifier.
|
||||
|
||||
====
|
||||
|
||||
@@ -222,7 +223,7 @@ export class ClaudeDev {
|
||||
return result
|
||||
}
|
||||
|
||||
async say(type: ClaudeSay, text: string): Promise<undefined> {
|
||||
async say(type: ClaudeSay, text?: string): Promise<undefined> {
|
||||
if (this.abort) {
|
||||
throw new Error("ClaudeDev instance aborted")
|
||||
}
|
||||
@@ -502,6 +503,38 @@ export class ClaudeDev {
|
||||
return `The user is not pleased with the results. Use the feedback they provided to successfully complete the task, and then attempt completion again.\nUser's feedback:\n\"${text}\"`
|
||||
}
|
||||
|
||||
async attemptApiRequest(): Promise<Anthropic.Messages.Message> {
|
||||
try {
|
||||
const response = await this.client.messages.create(
|
||||
{
|
||||
model: "claude-3-5-sonnet-20240620", // https://docs.anthropic.com/en/docs/about-claude/models
|
||||
// beta max tokens
|
||||
max_tokens: 8192,
|
||||
system: SYSTEM_PROMPT,
|
||||
messages: (await this.providerRef.deref()?.getApiConversationHistory()) || [],
|
||||
tools: tools,
|
||||
tool_choice: { type: "auto" },
|
||||
},
|
||||
{
|
||||
// https://github.com/anthropics/anthropic-sdk-typescript?tab=readme-ov-file#default-headers
|
||||
headers: { "anthropic-beta": "max-tokens-3-5-sonnet-2024-07-15" },
|
||||
}
|
||||
)
|
||||
return response
|
||||
} catch (error) {
|
||||
const { response } = await this.ask(
|
||||
"api_req_failed",
|
||||
error.message ?? JSON.stringify(serializeError(error), null, 2)
|
||||
)
|
||||
if (response !== "yesButtonTapped") {
|
||||
// this will never happen since if noButtonTapped, we will clear current task, aborting this instance
|
||||
throw new Error("API request failed")
|
||||
}
|
||||
await this.say("api_req_retried")
|
||||
return this.attemptApiRequest()
|
||||
}
|
||||
}
|
||||
|
||||
async recursivelyMakeClaudeRequests(
|
||||
userContent: Array<
|
||||
| Anthropic.TextBlockParam
|
||||
@@ -537,37 +570,22 @@ export class ClaudeDev {
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// what the user sees in the webview
|
||||
await this.say(
|
||||
"api_req_started",
|
||||
JSON.stringify({
|
||||
request: {
|
||||
model: "claude-3-5-sonnet-20240620",
|
||||
max_tokens: 8192,
|
||||
system: "(see SYSTEM_PROMPT in https://github.com/saoudrizwan/claude-dev/blob/main/src/ClaudeDev.ts)",
|
||||
messages: [{ conversation_history: "..." }, { role: "user", content: userContent }],
|
||||
tools: "(see tools in https://github.com/saoudrizwan/claude-dev/blob/main/src/ClaudeDev.ts)",
|
||||
tool_choice: { type: "auto" },
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
const response = await this.client.messages.create(
|
||||
{
|
||||
model: "claude-3-5-sonnet-20240620", // https://docs.anthropic.com/en/docs/about-claude/models
|
||||
// beta max tokens
|
||||
// what the user sees in the webview
|
||||
await this.say(
|
||||
"api_req_started",
|
||||
JSON.stringify({
|
||||
request: {
|
||||
model: "claude-3-5-sonnet-20240620",
|
||||
max_tokens: 8192,
|
||||
system: SYSTEM_PROMPT,
|
||||
messages: (await this.providerRef.deref()?.getApiConversationHistory()) || [],
|
||||
tools: tools,
|
||||
system: "(see SYSTEM_PROMPT in https://github.com/saoudrizwan/claude-dev/blob/main/src/ClaudeDev.ts)",
|
||||
messages: [{ conversation_history: "..." }, { role: "user", content: userContent }],
|
||||
tools: "(see tools in https://github.com/saoudrizwan/claude-dev/blob/main/src/ClaudeDev.ts)",
|
||||
tool_choice: { type: "auto" },
|
||||
},
|
||||
{
|
||||
// https://github.com/anthropics/anthropic-sdk-typescript?tab=readme-ov-file#default-headers
|
||||
headers: { "anthropic-beta": "max-tokens-3-5-sonnet-2024-07-15" },
|
||||
}
|
||||
)
|
||||
})
|
||||
)
|
||||
try {
|
||||
const response = await this.attemptApiRequest()
|
||||
this.requestCount++
|
||||
|
||||
let assistantResponses: Anthropic.Messages.ContentBlock[] = []
|
||||
@@ -674,8 +692,7 @@ export class ClaudeDev {
|
||||
|
||||
return { didEndLoop, inputTokens, outputTokens }
|
||||
} catch (error) {
|
||||
// only called if the API request fails (executeTool errors are returned back to claude)
|
||||
this.say("error", `API request failed:\n${error.message ?? JSON.stringify(serializeError(error), null, 2)}`)
|
||||
// this should never happen since the only thing that can throw an error is the attemptApiRequest, which is wrapped in a try catch that sends an ask where if noButtonTapped, will clear current task and destroy this instance. However to avoid unhandled promise rejection, we will end this loop which will end execution of this instance (see startTask)
|
||||
return { didEndLoop: true, inputTokens: 0, outputTokens: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,14 @@ export interface ClaudeMessage {
|
||||
text?: string
|
||||
}
|
||||
|
||||
export type ClaudeAsk = "request_limit_reached" | "followup" | "command" | "completion_result" | "tool"
|
||||
export type ClaudeAsk =
|
||||
| "request_limit_reached"
|
||||
| "followup"
|
||||
| "command"
|
||||
| "completion_result"
|
||||
| "tool"
|
||||
| "api_req_failed"
|
||||
|
||||
export type ClaudeSay =
|
||||
| "task"
|
||||
| "error"
|
||||
@@ -34,6 +41,7 @@ export type ClaudeSay =
|
||||
| "command_output"
|
||||
| "completion_result"
|
||||
| "user_feedback"
|
||||
| "api_req_retried"
|
||||
|
||||
export interface ClaudeSayTool {
|
||||
tool: "editedExistingFile" | "newFileCreated" | "readFile" | "listFiles"
|
||||
|
||||
1224
webview-ui/package-lock.json
generated
1224
webview-ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,7 @@
|
||||
"@vscode/webview-ui-toolkit": "^1.4.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react-text-truncate": "^0.19.0",
|
||||
|
||||
@@ -1,17 +1,27 @@
|
||||
import { ClaudeAsk, ClaudeMessage, ClaudeSay, ClaudeSayTool } from "@shared/ExtensionMessage"
|
||||
import { VSCodeBadge, VSCodeButton, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
|
||||
import React, { useState } from "react"
|
||||
import React from "react"
|
||||
import { COMMAND_OUTPUT_STRING } from "../utilities/combineCommandSequences"
|
||||
import { SyntaxHighlighterStyle } from "../utilities/getSyntaxHighlighterStyleFromTheme"
|
||||
import CodeBlock from "./CodeBlock/CodeBlock"
|
||||
import Markdown from "react-markdown"
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
|
||||
|
||||
interface ChatRowProps {
|
||||
message: ClaudeMessage
|
||||
syntaxHighlighterStyle: SyntaxHighlighterStyle
|
||||
isExpanded: boolean
|
||||
onToggleExpand: () => void
|
||||
apiRequestFailedMessage?: string
|
||||
}
|
||||
|
||||
const ChatRow: React.FC<ChatRowProps> = ({ message, syntaxHighlighterStyle }) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const ChatRow: React.FC<ChatRowProps> = ({
|
||||
message,
|
||||
syntaxHighlighterStyle,
|
||||
isExpanded,
|
||||
onToggleExpand,
|
||||
apiRequestFailedMessage,
|
||||
}) => {
|
||||
const cost = message.text != null && message.say === "api_req_started" ? JSON.parse(message.text).cost : undefined
|
||||
|
||||
const getIconAndTitle = (type: ClaudeAsk | ClaudeSay | undefined): [JSX.Element | null, JSX.Element | null] => {
|
||||
@@ -56,6 +66,10 @@ const ChatRow: React.FC<ChatRowProps> = ({ message, syntaxHighlighterStyle }) =>
|
||||
<span
|
||||
className="codicon codicon-check"
|
||||
style={{ color: successColor, marginBottom: "-1.5px" }}></span>
|
||||
) : apiRequestFailedMessage ? (
|
||||
<span
|
||||
className="codicon codicon-error"
|
||||
style={{ color: errorColor, marginBottom: "-1.5px" }}></span>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
@@ -70,15 +84,73 @@ const ChatRow: React.FC<ChatRowProps> = ({ message, syntaxHighlighterStyle }) =>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
<span style={{ color: normalColor, fontWeight: "bold" }}>
|
||||
{cost ? "API Request Complete" : "Making API Request..."}
|
||||
</span>,
|
||||
cost ? (
|
||||
<span style={{ color: normalColor, fontWeight: "bold" }}>API Request Complete</span>
|
||||
) : apiRequestFailedMessage ? (
|
||||
<span style={{ color: errorColor, fontWeight: "bold" }}>API Request Failed</span>
|
||||
) : (
|
||||
<span style={{ color: normalColor, fontWeight: "bold" }}>Making API Request...</span>
|
||||
),
|
||||
]
|
||||
default:
|
||||
return [null, null]
|
||||
}
|
||||
}
|
||||
|
||||
const convertToMarkdown = (markdown: string = "") => {
|
||||
// react-markdown lets us customize elements, so here we're using their example of replacing code blocks with SyntaxHighlighter. However when there are no language matches (` or ``` without a language specifier) then we default to a normal code element for inline code. Code blocks without a language specifier shouldn't be a common occurrence as we prompt Claude to always use a language specifier.
|
||||
return (
|
||||
<Markdown
|
||||
children={markdown}
|
||||
components={{
|
||||
p(props) {
|
||||
const { style, ...rest } = props
|
||||
return <p style={{ ...style, margin: 0, marginTop: 0, marginBottom: 0 }} {...rest} />
|
||||
},
|
||||
//p: "span",
|
||||
// https://github.com/remarkjs/react-markdown?tab=readme-ov-file#use-custom-components-syntax-highlight
|
||||
code(props) {
|
||||
const { children, className, node, ...rest } = props
|
||||
const match = /language-(\w+)/.exec(className || "")
|
||||
return match ? (
|
||||
<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",
|
||||
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}>
|
||||
{children}
|
||||
</code>
|
||||
)
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const renderContent = () => {
|
||||
const [icon, title] = getIconAndTitle(message.type === "ask" ? message.ask : message.say)
|
||||
|
||||
@@ -89,7 +161,7 @@ const ChatRow: React.FC<ChatRowProps> = ({ message, syntaxHighlighterStyle }) =>
|
||||
marginBottom: "10px",
|
||||
}
|
||||
|
||||
const contentStyle: React.CSSProperties = {
|
||||
const pStyle: React.CSSProperties = {
|
||||
margin: 0,
|
||||
whiteSpace: "pre-line",
|
||||
}
|
||||
@@ -99,24 +171,37 @@ const ChatRow: React.FC<ChatRowProps> = ({ message, syntaxHighlighterStyle }) =>
|
||||
switch (message.say) {
|
||||
case "api_req_started":
|
||||
return (
|
||||
<div style={{ ...headerStyle, marginBottom: 0, justifyContent: "space-between" }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
|
||||
{icon}
|
||||
{title}
|
||||
{cost && <VSCodeBadge>${Number(cost).toFixed(4)}</VSCodeBadge>}
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
...headerStyle,
|
||||
marginBottom: cost == null && apiRequestFailedMessage ? 10 : 0,
|
||||
justifyContent: "space-between",
|
||||
}}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
|
||||
{icon}
|
||||
{title}
|
||||
{cost && <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>
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
aria-label="Toggle Details"
|
||||
onClick={() => setIsExpanded(!isExpanded)}>
|
||||
<span className={`codicon codicon-chevron-${isExpanded ? "up" : "down"}`}></span>
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
{cost == null && apiRequestFailedMessage && (
|
||||
<p style={{ ...pStyle, color: "var(--vscode-errorForeground)" }}>
|
||||
{apiRequestFailedMessage}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
case "api_req_finished":
|
||||
return null // we should never see this message type
|
||||
case "text":
|
||||
return <p style={contentStyle}>{message.text}</p>
|
||||
return <div>{convertToMarkdown(message.text)}</div>
|
||||
case "user_feedback":
|
||||
return (
|
||||
<div
|
||||
@@ -140,9 +225,7 @@ const ChatRow: React.FC<ChatRowProps> = ({ message, syntaxHighlighterStyle }) =>
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
<p style={{ ...contentStyle, color: "var(--vscode-errorForeground)" }}>
|
||||
{message.text}
|
||||
</p>
|
||||
<p style={{ ...pStyle, color: "var(--vscode-errorForeground)" }}>{message.text}</p>
|
||||
</>
|
||||
)
|
||||
case "completion_result":
|
||||
@@ -152,9 +235,9 @@ const ChatRow: React.FC<ChatRowProps> = ({ message, syntaxHighlighterStyle }) =>
|
||||
{icon}
|
||||
{title}
|
||||
</div>
|
||||
<p style={{ ...contentStyle, color: "var(--vscode-testing-iconPassed)" }}>
|
||||
{message.text}
|
||||
</p>
|
||||
<div style={{ color: "var(--vscode-testing-iconPassed)" }}>
|
||||
{convertToMarkdown(message.text)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
default:
|
||||
@@ -166,7 +249,7 @@ const ChatRow: React.FC<ChatRowProps> = ({ message, syntaxHighlighterStyle }) =>
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
<p style={contentStyle}>{message.text}</p>
|
||||
<div>{convertToMarkdown(message.text)}</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -192,6 +275,8 @@ const ChatRow: React.FC<ChatRowProps> = ({ message, syntaxHighlighterStyle }) =>
|
||||
diff={tool.diff!}
|
||||
path={tool.path!}
|
||||
syntaxHighlighterStyle={syntaxHighlighterStyle}
|
||||
isExpanded={isExpanded}
|
||||
onToggleExpand={onToggleExpand}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
@@ -208,6 +293,8 @@ const ChatRow: React.FC<ChatRowProps> = ({ message, syntaxHighlighterStyle }) =>
|
||||
code={tool.content!}
|
||||
path={tool.path!}
|
||||
syntaxHighlighterStyle={syntaxHighlighterStyle}
|
||||
isExpanded={isExpanded}
|
||||
onToggleExpand={onToggleExpand}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
@@ -222,6 +309,8 @@ const ChatRow: React.FC<ChatRowProps> = ({ message, syntaxHighlighterStyle }) =>
|
||||
code={tool.content!}
|
||||
path={tool.path!}
|
||||
syntaxHighlighterStyle={syntaxHighlighterStyle}
|
||||
isExpanded={isExpanded}
|
||||
onToggleExpand={onToggleExpand}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
@@ -239,6 +328,8 @@ const ChatRow: React.FC<ChatRowProps> = ({ message, syntaxHighlighterStyle }) =>
|
||||
path={tool.path!}
|
||||
language="shell-session"
|
||||
syntaxHighlighterStyle={syntaxHighlighterStyle}
|
||||
isExpanded={isExpanded}
|
||||
onToggleExpand={onToggleExpand}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
@@ -251,9 +342,7 @@ const ChatRow: React.FC<ChatRowProps> = ({ message, syntaxHighlighterStyle }) =>
|
||||
{icon}
|
||||
{title}
|
||||
</div>
|
||||
<p style={{ ...contentStyle, color: "var(--vscode-errorForeground)" }}>
|
||||
{message.text}
|
||||
</p>
|
||||
<p style={{ ...pStyle, color: "var(--vscode-errorForeground)" }}>{message.text}</p>
|
||||
</>
|
||||
)
|
||||
case "command":
|
||||
@@ -275,24 +364,28 @@ const ChatRow: React.FC<ChatRowProps> = ({ message, syntaxHighlighterStyle }) =>
|
||||
{icon}
|
||||
{title}
|
||||
</div>
|
||||
<div style={contentStyle}>
|
||||
<div>
|
||||
<div>
|
||||
<CodeBlock
|
||||
code={command}
|
||||
language="shell-session"
|
||||
syntaxHighlighterStyle={syntaxHighlighterStyle}
|
||||
isExpanded={isExpanded}
|
||||
onToggleExpand={onToggleExpand}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{output && (
|
||||
<>
|
||||
<p style={{ ...contentStyle, margin: "10px 0 10px 0" }}>
|
||||
<p style={{ ...pStyle, margin: "10px 0 10px 0" }}>
|
||||
{COMMAND_OUTPUT_STRING}
|
||||
</p>
|
||||
<CodeBlock
|
||||
code={output}
|
||||
language="shell-session"
|
||||
syntaxHighlighterStyle={syntaxHighlighterStyle}
|
||||
isExpanded={isExpanded}
|
||||
onToggleExpand={onToggleExpand}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -307,15 +400,15 @@ const ChatRow: React.FC<ChatRowProps> = ({ message, syntaxHighlighterStyle }) =>
|
||||
{icon}
|
||||
{title}
|
||||
</div>
|
||||
<p style={{ ...contentStyle, color: "var(--vscode-testing-iconPassed)" }}>
|
||||
{message.text}
|
||||
</p>
|
||||
<div style={{ color: "var(--vscode-testing-iconPassed)" }}>
|
||||
{convertToMarkdown(message.text)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return null // Don't render anything when we get a completion_result ask without text
|
||||
}
|
||||
default:
|
||||
case "followup":
|
||||
return (
|
||||
<>
|
||||
{title && (
|
||||
@@ -324,17 +417,14 @@ const ChatRow: React.FC<ChatRowProps> = ({ message, syntaxHighlighterStyle }) =>
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
<p style={contentStyle}>{message.text}</p>
|
||||
<div>{convertToMarkdown(message.text)}</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// we need to return null here instead of in getContent since that way would result in padding being applied
|
||||
if (!shouldShowChatRow(message)) {
|
||||
return null // Don't render anything for this message type
|
||||
}
|
||||
// NOTE: we cannot return null as virtuoso does not support it, so we must use a separate visibleMessages array to filter out messages that should not be rendered
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -348,6 +438,8 @@ const ChatRow: React.FC<ChatRowProps> = ({ message, syntaxHighlighterStyle }) =>
|
||||
code={JSON.stringify(JSON.parse(message.text || "{}").request, null, 2)}
|
||||
language="json"
|
||||
syntaxHighlighterStyle={syntaxHighlighterStyle}
|
||||
isExpanded={true}
|
||||
onToggleExpand={onToggleExpand}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -355,18 +447,4 @@ const ChatRow: React.FC<ChatRowProps> = ({ message, syntaxHighlighterStyle }) =>
|
||||
)
|
||||
}
|
||||
|
||||
export const shouldShowChatRow = (message: ClaudeMessage) => {
|
||||
// combineApiRequests removes this from modifiedMessages anyways
|
||||
if (message.say === "api_req_finished") {
|
||||
return false
|
||||
}
|
||||
|
||||
// don't show a chat row for a completion_result ask without text. This specific type of message only occurs if Claude wants to execute a command as part of its completion result, in which case we interject the completion_result tool with the execute_command tool.
|
||||
if (message.type === "ask" && message.ask === "completion_result" && message.text === "") {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export default ChatRow
|
||||
|
||||
@@ -9,7 +9,7 @@ import { combineCommandSequences } from "../utilities/combineCommandSequences"
|
||||
import { getApiMetrics } from "../utilities/getApiMetrics"
|
||||
import { getSyntaxHighlighterStyleFromTheme } from "../utilities/getSyntaxHighlighterStyleFromTheme"
|
||||
import { vscode } from "../utilities/vscode"
|
||||
import ChatRow, { shouldShowChatRow } from "./ChatRow"
|
||||
import ChatRow from "./ChatRow"
|
||||
import TaskHeader from "./TaskHeader"
|
||||
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"
|
||||
|
||||
@@ -42,6 +42,15 @@ const ChatView = ({ messages, isHidden, vscodeThemeName }: ChatViewProps) => {
|
||||
|
||||
const virtuosoRef = useRef<VirtuosoHandle>(null)
|
||||
|
||||
const [expandedRows, setExpandedRows] = useState<Record<number, boolean>>({})
|
||||
|
||||
const toggleRowExpansion = (ts: number) => {
|
||||
setExpandedRows((prev) => ({
|
||||
...prev,
|
||||
[ts]: !prev[ts],
|
||||
}))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!vscodeThemeName) return
|
||||
const theme = getSyntaxHighlighterStyleFromTheme(vscodeThemeName)
|
||||
@@ -68,6 +77,13 @@ const ChatView = ({ messages, isHidden, vscodeThemeName }: ChatViewProps) => {
|
||||
setPrimaryButtonText("Proceed")
|
||||
setSecondaryButtonText("Start New Task")
|
||||
break
|
||||
case "api_req_failed":
|
||||
setTextAreaDisabled(true)
|
||||
setClaudeAsk("api_req_failed")
|
||||
setEnableButtons(true)
|
||||
setPrimaryButtonText("Retry")
|
||||
setSecondaryButtonText("Start New Task")
|
||||
break
|
||||
case "followup":
|
||||
setTextAreaDisabled(false)
|
||||
setClaudeAsk("followup")
|
||||
@@ -170,6 +186,7 @@ const ChatView = ({ messages, isHidden, vscodeThemeName }: ChatViewProps) => {
|
||||
const handlePrimaryButtonClick = () => {
|
||||
switch (claudeAsk) {
|
||||
case "request_limit_reached":
|
||||
case "api_req_failed":
|
||||
case "command":
|
||||
case "tool":
|
||||
vscode.postMessage({ type: "askResponse", askResponse: "yesButtonTapped" })
|
||||
@@ -189,6 +206,7 @@ const ChatView = ({ messages, isHidden, vscodeThemeName }: ChatViewProps) => {
|
||||
const handleSecondaryButtonClick = () => {
|
||||
switch (claudeAsk) {
|
||||
case "request_limit_reached":
|
||||
case "api_req_failed":
|
||||
startNewTask()
|
||||
break
|
||||
case "command":
|
||||
@@ -262,6 +280,41 @@ const ChatView = ({ messages, isHidden, vscodeThemeName }: ChatViewProps) => {
|
||||
}
|
||||
}, [isHidden, textAreaDisabled, enableButtons])
|
||||
|
||||
const visibleMessages = useMemo(() => {
|
||||
return modifiedMessages.filter((message) => {
|
||||
switch (message.ask) {
|
||||
case "completion_result":
|
||||
// don't show a chat row for a completion_result ask without text. This specific type of message only occurs if Claude wants to execute a command as part of its completion result, in which case we interject the completion_result tool with the execute_command tool.
|
||||
if (message.text === "") {
|
||||
return false
|
||||
}
|
||||
break
|
||||
case "api_req_failed": // this message is used to update the latest api_req_started that the request failed
|
||||
return false
|
||||
}
|
||||
switch (message.say) {
|
||||
case "api_req_finished": // combineApiRequests removes this from modifiedMessages anyways
|
||||
case "api_req_retried": // this message is used to update the latest api_req_started that the request was retried
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}, [modifiedMessages])
|
||||
|
||||
useEffect(() => {
|
||||
// We use a setTimeout to ensure new content is rendered before scrolling to the bottom. virtuoso's followOutput would scroll to the bottom before the new content could render.
|
||||
const timer = setTimeout(() => {
|
||||
// TODO: we can use virtuoso's isAtBottom to prevent scrolling if user is scrolled up, and show a 'scroll to bottom' button for better UX
|
||||
// NOTE: scroll to bottom may not work if you use margin, see virtuoso's troubleshooting
|
||||
virtuosoRef.current?.scrollToIndex({
|
||||
index: "LAST",
|
||||
behavior: "smooth",
|
||||
})
|
||||
}, 50)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [visibleMessages])
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -306,18 +359,28 @@ const ChatView = ({ messages, isHidden, vscodeThemeName }: ChatViewProps) => {
|
||||
flexGrow: 1,
|
||||
overflowY: "scroll", // always show scrollbar
|
||||
}}
|
||||
followOutput={(isAtBottom) => {
|
||||
// TODO: we can use isAtBottom to prevent scrolling if user is scrolled up, and show a 'scroll to bottom' button for better UX
|
||||
const lastMessage = modifiedMessages.at(-1)
|
||||
if (lastMessage && shouldShowChatRow(lastMessage)) {
|
||||
return "smooth" // NOTE: scroll to bottom may not work if you use margin, see virtuoso's troubleshooting
|
||||
}
|
||||
return false
|
||||
}}
|
||||
// followOutput={(isAtBottom) => {
|
||||
// const lastMessage = modifiedMessages.at(-1)
|
||||
// if (lastMessage && shouldShowChatRow(lastMessage)) {
|
||||
// return "smooth"
|
||||
// }
|
||||
// return false
|
||||
// }}
|
||||
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={modifiedMessages}
|
||||
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) => (
|
||||
<ChatRow key={message.ts} message={message} syntaxHighlighterStyle={syntaxHighlighterStyle} />
|
||||
<ChatRow
|
||||
key={message.ts}
|
||||
message={message}
|
||||
syntaxHighlighterStyle={syntaxHighlighterStyle}
|
||||
isExpanded={expandedRows[message.ts] || false}
|
||||
onToggleExpand={() => toggleRowExpansion(message.ts)}
|
||||
apiRequestFailedMessage={
|
||||
index === visibleMessages.length - 1 && modifiedMessages.at(-1)?.ask === "api_req_failed" // if request is retried then the latest message is a api_req_retried
|
||||
? modifiedMessages.at(-1)?.text
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useMemo, useState } from "react"
|
||||
import { useMemo } from "react"
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
|
||||
import { getLanguageFromPath } from "../../utilities/getLanguageFromPath"
|
||||
import { SyntaxHighlighterStyle } from "../../utilities/getSyntaxHighlighterStyleFromTheme"
|
||||
@@ -64,11 +64,19 @@ interface CodeBlockProps {
|
||||
language?: string | undefined
|
||||
path?: string
|
||||
syntaxHighlighterStyle: SyntaxHighlighterStyle
|
||||
isExpanded: boolean
|
||||
onToggleExpand: () => void
|
||||
}
|
||||
|
||||
const CodeBlock = ({ code, diff, language, path, syntaxHighlighterStyle }: CodeBlockProps) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
|
||||
const CodeBlock = ({
|
||||
code,
|
||||
diff,
|
||||
language,
|
||||
path,
|
||||
syntaxHighlighterStyle,
|
||||
isExpanded,
|
||||
onToggleExpand,
|
||||
}: CodeBlockProps) => {
|
||||
/*
|
||||
We need to remove leading non-alphanumeric characters from the path in order for our leading ellipses trick to work.
|
||||
|
||||
@@ -100,7 +108,7 @@ const CodeBlock = ({ code, diff, language, path, syntaxHighlighterStyle }: CodeB
|
||||
padding: "6px 10px",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => setIsExpanded(!isExpanded)}>
|
||||
onClick={onToggleExpand}>
|
||||
<span
|
||||
style={{
|
||||
color: "#BABCC3",
|
||||
@@ -115,14 +123,14 @@ const CodeBlock = ({ code, diff, language, path, syntaxHighlighterStyle }: CodeB
|
||||
}}>
|
||||
{removeLeadingNonAlphanumeric(path) + "\u200E"}
|
||||
</span>
|
||||
<VSCodeButton appearance="icon" aria-label="Toggle Code" onClick={() => setIsExpanded(!isExpanded)}>
|
||||
<VSCodeButton appearance="icon" aria-label="Toggle Code">
|
||||
<span className={`codicon codicon-chevron-${isExpanded ? "up" : "down"}`}></span>
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
)}
|
||||
{(!path || isExpanded) && (
|
||||
<div
|
||||
className="code-block-scrollable"
|
||||
//className="code-block-scrollable" this doesn't seem to be necessary anymore, on silicon macs it shows the native mac scrollbar instead of the vscode styled one
|
||||
style={{
|
||||
overflowX: "auto",
|
||||
overflowY: "hidden",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { VSCodeButton, VSCodeDivider, VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
import React, { useState } from "react"
|
||||
import { useMount } from "react-use"
|
||||
import { useEffectOnce } from "react-use"
|
||||
import { vscode } from "../utilities/vscode"
|
||||
|
||||
type SettingsViewProps = {
|
||||
@@ -70,9 +70,13 @@ const SettingsView = ({ apiKey, setApiKey, maxRequestsPerTask, setMaxRequestsPer
|
||||
|
||||
If we only want to run code once on mount we can use react-use's useEffectOnce or useMount
|
||||
*/
|
||||
useMount(() => {
|
||||
validateApiKey(apiKey)
|
||||
validateMaxRequests(maxRequestsPerTask)
|
||||
useEffectOnce(() => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
validateApiKey(apiKey)
|
||||
validateMaxRequests(maxRequestsPerTask)
|
||||
}, 1000)
|
||||
|
||||
return () => clearTimeout(timeoutId)
|
||||
})
|
||||
|
||||
return (
|
||||
|
||||
@@ -34,9 +34,11 @@ body {
|
||||
}
|
||||
|
||||
body.scrollable,
|
||||
.scrollable, body.code-block-scrollable, .code-block-scrollable {
|
||||
border-color: transparent;
|
||||
transition: border-color 0.7s linear;
|
||||
.scrollable,
|
||||
body.code-block-scrollable,
|
||||
.code-block-scrollable {
|
||||
border-color: transparent;
|
||||
transition: border-color 0.7s linear;
|
||||
}
|
||||
|
||||
body:hover.scrollable,
|
||||
@@ -46,30 +48,29 @@ body:focus-within .scrollable,
|
||||
body:hover.code-block-scrollable,
|
||||
body:hover .code-block-scrollable,
|
||||
body:focus-within.code-block-scrollable,
|
||||
body:focus-within .code-block-scrollable
|
||||
{
|
||||
border-color: var(--vscode-scrollbarSlider-background);
|
||||
transition: none;
|
||||
body:focus-within .code-block-scrollable {
|
||||
border-color: var(--vscode-scrollbarSlider-background);
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.scrollable::-webkit-scrollbar-corner {
|
||||
background-color: transparent !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.scrollable::-webkit-scrollbar-thumb {
|
||||
background-color: transparent;
|
||||
border-color: inherit;
|
||||
border-right-style: inset;
|
||||
border-right-width: calc(100vw + 100vh);
|
||||
border-radius: unset !important;
|
||||
background-color: transparent;
|
||||
border-color: inherit;
|
||||
border-right-style: inset;
|
||||
border-right-width: calc(100vw + 100vh);
|
||||
border-radius: unset !important;
|
||||
}
|
||||
|
||||
.scrollable::-webkit-scrollbar-thumb:hover {
|
||||
border-color: var(--vscode-scrollbarSlider-hoverBackground);
|
||||
border-color: var(--vscode-scrollbarSlider-hoverBackground);
|
||||
}
|
||||
|
||||
.scrollable::-webkit-scrollbar-thumb:active {
|
||||
border-color: var(--vscode-scrollbarSlider-activeBackground);
|
||||
border-color: var(--vscode-scrollbarSlider-activeBackground);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -87,24 +88,24 @@ The above scrollbar styling uses some transparent background color magic to acco
|
||||
*/
|
||||
|
||||
.code-block-scrollable::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.code-block-scrollable::-webkit-scrollbar-thumb {
|
||||
background-color: var(--vscode-scrollbarSlider-background);
|
||||
border-radius: 5px;
|
||||
border: 2px solid transparent;
|
||||
background-clip: content-box;
|
||||
background-color: var(--vscode-scrollbarSlider-background);
|
||||
border-radius: 5px;
|
||||
border: 2px solid transparent;
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
.code-block-scrollable::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--vscode-scrollbarSlider-hoverBackground);
|
||||
background-color: var(--vscode-scrollbarSlider-hoverBackground);
|
||||
}
|
||||
|
||||
.code-block-scrollable::-webkit-scrollbar-thumb:active {
|
||||
background-color: var(--vscode-scrollbarSlider-activeBackground);
|
||||
background-color: var(--vscode-scrollbarSlider-activeBackground);
|
||||
}
|
||||
|
||||
.code-block-scrollable::-webkit-scrollbar-corner {
|
||||
background-color: transparent;
|
||||
background-color: transparent;
|
||||
}
|
||||
Reference in New Issue
Block a user