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:
Saoud Rizwan
2024-07-25 08:54:29 -04:00
parent 8058d261ba
commit 44a4140486
9 changed files with 1536 additions and 134 deletions

View File

@@ -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,7 +570,6 @@ export class ClaudeDev {
}
}
try {
// what the user sees in the webview
await this.say(
"api_req_started",
@@ -552,22 +584,8 @@ export class ClaudeDev {
},
})
)
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" },
}
)
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 }
}
}

View File

@@ -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"

File diff suppressed because it is too large Load Diff

View File

@@ -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",

View File

@@ -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,7 +171,13 @@ 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={{
...headerStyle,
marginBottom: cost == null && apiRequestFailedMessage ? 10 : 0,
justifyContent: "space-between",
}}>
<div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
{icon}
{title}
@@ -108,15 +186,22 @@ const ChatRow: React.FC<ChatRowProps> = ({ message, syntaxHighlighterStyle }) =>
<VSCodeButton
appearance="icon"
aria-label="Toggle Details"
onClick={() => setIsExpanded(!isExpanded)}>
<span className={`codicon codicon-chevron-${isExpanded ? "up" : "down"}`}></span>
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>
)}
</>
)
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

View File

@@ -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

View File

@@ -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",

View File

@@ -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(() => {
useEffectOnce(() => {
const timeoutId = setTimeout(() => {
validateApiKey(apiKey)
validateMaxRequests(maxRequestsPerTask)
}, 1000)
return () => clearTimeout(timeoutId)
})
return (

View File

@@ -34,7 +34,9 @@ body {
}
body.scrollable,
.scrollable, body.code-block-scrollable, .code-block-scrollable {
.scrollable,
body.code-block-scrollable,
.code-block-scrollable {
border-color: transparent;
transition: border-color 0.7s linear;
}
@@ -46,8 +48,7 @@ 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
{
body:focus-within .code-block-scrollable {
border-color: var(--vscode-scrollbarSlider-background);
transition: none;
}