From c798df215e72caf330c04c1a738af20788f7d994 Mon Sep 17 00:00:00 2001 From: Hanzen Shou Date: Sat, 28 Dec 2024 21:03:19 -0800 Subject: [PATCH 01/33] fix: double underscored filenames now render correctly --- .../src/components/common/MarkdownBlock.tsx | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/webview-ui/src/components/common/MarkdownBlock.tsx b/webview-ui/src/components/common/MarkdownBlock.tsx index 37a0278991..4de1f568d1 100644 --- a/webview-ui/src/components/common/MarkdownBlock.tsx +++ b/webview-ui/src/components/common/MarkdownBlock.tsx @@ -50,6 +50,43 @@ const remarkUrlToLink = () => { } } +/** + * Custom remark plugin that prevents filenames with extensions from being parsed as bold text + * For example: __init__.py should not be rendered as bold "init" followed by ".py" + */ +const remarkPreventBoldFilenames = () => { + return (tree: any) => { + visit(tree, "strong", (node: any, index: number | undefined, parent: any) => { + // Only process if there's a next node (potential file extension) + if (!parent || typeof index === "undefined" || index === parent.children.length - 1) return + + const nextNode = parent.children[index + 1] + + // Check if next node is text and starts with . followed by extension + if (nextNode.type !== "text" || !nextNode.value.match(/^\.[a-zA-Z0-9]+/)) return + + // If the strong node has multiple children, something weird is happening + if (node.children?.length !== 1) return + + // Get the text content from inside the strong node + const strongContent = node.children?.[0]?.value + if (!strongContent || typeof strongContent !== "string") return + + // Validate that the strong content is a valid filename + if (!strongContent.match(/^[a-zA-Z0-9_-]+$/)) return + + // Combine into a single text node + const newNode = { + type: "text", + value: `__${strongContent}__${nextNode.value}`, + } + + // Replace both nodes with the combined text node + parent.children.splice(index, 2, newNode) + }) + } +} + const StyledMarkdown = styled.div` pre { background-color: ${CODE_BLOCK_BG_COLOR}; @@ -160,6 +197,7 @@ const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => { const { theme } = useExtensionState() const [reactContent, setMarkdown] = useRemark({ remarkPlugins: [ + remarkPreventBoldFilenames, remarkUrlToLink, () => { return (tree) => { From 1ce57f22031758c3a1fbb504ee59688b999c4f22 Mon Sep 17 00:00:00 2001 From: Hanzen Shou Date: Sat, 28 Dec 2024 21:04:25 -0800 Subject: [PATCH 02/33] doc: linked issue to markdown plugin --- webview-ui/src/components/common/MarkdownBlock.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/webview-ui/src/components/common/MarkdownBlock.tsx b/webview-ui/src/components/common/MarkdownBlock.tsx index 4de1f568d1..91e129ceb2 100644 --- a/webview-ui/src/components/common/MarkdownBlock.tsx +++ b/webview-ui/src/components/common/MarkdownBlock.tsx @@ -53,6 +53,7 @@ const remarkUrlToLink = () => { /** * Custom remark plugin that prevents filenames with extensions from being parsed as bold text * For example: __init__.py should not be rendered as bold "init" followed by ".py" + * Solves https://github.com/cline/cline/issues/1028 */ const remarkPreventBoldFilenames = () => { return (tree: any) => { From 6bd6c830fe8df112440a18f59cd513ea5b7f0c4e Mon Sep 17 00:00:00 2001 From: tszhong0411 Date: Sun, 26 Jan 2025 21:06:21 +0800 Subject: [PATCH 03/33] fix: typos in docs and code --- docs/mcp/mcp-quickstart.md | 4 ++-- src/core/Cline.ts | 6 +++--- src/core/sliding-window/index.ts | 2 +- src/core/webview/ClineProvider.ts | 8 ++++---- webview-ui/src/components/chat/AutoApproveMenu.tsx | 2 +- webview-ui/src/components/chat/ChatRow.tsx | 2 +- webview-ui/src/components/chat/ChatTextArea.tsx | 2 +- webview-ui/src/components/chat/ChatView.tsx | 2 +- webview-ui/src/components/common/CodeBlock.tsx | 2 +- 9 files changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/mcp/mcp-quickstart.md b/docs/mcp/mcp-quickstart.md index 13e194e47c..b1b5700694 100644 --- a/docs/mcp/mcp-quickstart.md +++ b/docs/mcp/mcp-quickstart.md @@ -38,7 +38,7 @@ STOP! Before proceeding, you MUST verify these requirements: MCP Server Panel 1. The MCP settings files should be display in a tab in VS Code. -1. Replce the file's contents with this code: +1. Replace the file's contents with this code: For Windows: @@ -96,7 +96,7 @@ You should witness Cline: 1. Update the mcp setting json file 1. Start the server and start the server -The mcp seetings file should now look like this: +The mcp settings file should now look like this: _For a Windows machine:_ diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 947a5fae6f..3b0877e05a 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -214,7 +214,7 @@ export class Cline { private async addToClineMessages(message: ClineMessage) { // these values allow us to reconstruct the conversation history at the time this cline message was created // it's important that apiConversationHistory is initialized before we add cline messages - message.conversationHistoryIndex = this.apiConversationHistory.length - 1 // NOTE: this is the index of the last added message which is the user message, and once the clinemessages have been presented we update the apiconversationhistory with the completed assistant message. This means when reseting to a message, we need to +1 this index to get the correct assistant message that this tool use corresponds to + message.conversationHistoryIndex = this.apiConversationHistory.length - 1 // NOTE: this is the index of the last added message which is the user message, and once the clinemessages have been presented we update the apiconversationhistory with the completed assistant message. This means when resetting to a message, we need to +1 this index to get the correct assistant message that this tool use corresponds to message.conversationHistoryDeletedRange = this.conversationHistoryDeletedRange this.clineMessages.push(message) await this.saveClineMessages() @@ -1386,7 +1386,7 @@ export class Cline { if (!block.partial) { // Some models add code block artifacts (around the tool calls) which show up at the end of text content - // matches ``` with atleast one char after the last backtick, at the end of the string + // matches ``` with at least one char after the last backtick, at the end of the string const match = content?.trimEnd().match(/```[a-zA-Z0-9_-]+$/) if (match) { const matchLength = match[0].length @@ -2773,7 +2773,7 @@ export class Cline { if (!block.partial || this.didRejectTool || this.didAlreadyUseTool) { // block is finished streaming and executing if (this.currentStreamingContentIndex === this.assistantMessageContent.length - 1) { - // its okay that we increment if !didCompleteReadingStream, it'll just return bc out of bounds and as streaming continues it will call presentAssitantMessage if a new block is ready. if streaming is finished then we set userMessageContentReady to true when out of bounds. This gracefully allows the stream to continue on and all potential content blocks be presented. + // its okay that we increment if !didCompleteReadingStream, it'll just return bc out of bounds and as streaming continues it will call presentAssistantMessage if a new block is ready. if streaming is finished then we set userMessageContentReady to true when out of bounds. This gracefully allows the stream to continue on and all potential content blocks be presented. // last block is complete and it is finished executing this.userMessageContentReady = true // will allow pwaitfor to continue } diff --git a/src/core/sliding-window/index.ts b/src/core/sliding-window/index.ts index 83b91eb381..a9ea7a0da9 100644 --- a/src/core/sliding-window/index.ts +++ b/src/core/sliding-window/index.ts @@ -65,7 +65,7 @@ export function getNextTruncationRange( let rangeEndIndex = startOfRest + messagesToRemove - 1 // Make sure the last message being removed is a user message, so that the next message after the initial task message is an assistant message. This preservers the user-assistant-user-assistant structure. - // NOTE: anthropic format messages are always user-assitant-user-assistant, while openai format messages can have multiple user messages in a row (we use anthropic format throughout cline) + // NOTE: anthropic format messages are always user-assistant-user-assistant, while openai format messages can have multiple user messages in a row (we use anthropic format throughout cline) if (messages[rangeEndIndex].role !== "user") { rangeEndIndex -= 1 } diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index a54dc97986..6ca8cadff5 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -172,7 +172,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { webviewView.webview.html = this.getHtmlContent(webviewView.webview) // Sets up an event listener to listen for messages passed from the webview view context - // and executes code based on the message that is recieved + // and executes code based on the message that is received this.setWebviewMessageListener(webviewView.webview) // Logs show up in bottom panel > Debug Console @@ -243,7 +243,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { } async initClineWithTask(task?: string, images?: string[]) { - await this.clearTask() // ensures that an exising task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one + await this.clearTask() // ensures that an existing task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one const { apiConfiguration, customInstructions, autoApprovalSettings, browserSettings, chatSettings } = await this.getState() this.cline = new Cline( @@ -357,7 +357,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { /** * Sets up an event listener to listen for messages passed from the webview context and - * executes code based on the message that is recieved. + * executes code based on the message that is received. * * @param webview A reference to the extension webview */ @@ -1184,7 +1184,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { Now that we use retainContextWhenHidden, we don't have to store a cache of cline messages in the user's state, but we could to reduce memory footprint in long conversations. - We have to be careful of what state is shared between ClineProvider instances since there could be multiple instances of the extension running at once. For example when we cached cline messages using the same key, two instances of the extension could end up using the same key and overwriting each other's messages. - - Some state does need to be shared between the instances, i.e. the API key--however there doesn't seem to be a good way to notfy the other instances that the API key has changed. + - Some state does need to be shared between the instances, i.e. the API key--however there doesn't seem to be a good way to notify the other instances that the API key has changed. We need to use a unique identifier for each ClineProvider instance's message cache since we could be running several instances of the extension outside of just the sidebar i.e. in editor panels. diff --git a/webview-ui/src/components/chat/AutoApproveMenu.tsx b/webview-ui/src/components/chat/AutoApproveMenu.tsx index aa3a8a44a7..68863a8fd2 100644 --- a/webview-ui/src/components/chat/AutoApproveMenu.tsx +++ b/webview-ui/src/components/chat/AutoApproveMenu.tsx @@ -169,7 +169,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => { // }} onClick={(e) => { /* - vscode web toolkit bug: when changing the value of a vscodecheckbox programatically, it will call its onChange with stale state. This led to updateEnabled being called with an old vesion of autoApprovalSettings, effectively undoing the state change that was triggered by the last action being unchecked. A simple workaround is to just not use onChange and intead use onClick. We are lucky this is a checkbox and the newvalue is simply opposite of current state. + vscode web toolkit bug: when changing the value of a vscodecheckbox programmatically, it will call its onChange with stale state. This led to updateEnabled being called with an old version of autoApprovalSettings, effectively undoing the state change that was triggered by the last action being unchecked. A simple workaround is to just not use onChange and instead use onClick. We are lucky this is a checkbox and the newvalue is simply opposite of current state. */ if (!hasEnabledActions) return e.stopPropagation() // stops click from bubbling up to the parent, in this case stopping the expanding/collapsing diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index fed1bb0cf4..766be253b9 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -751,7 +751,7 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi }}> {icon} {title} - {/* Need to render this everytime since it affects height of row by 2px */} + {/* Need to render this every time since it affects height of row by 2px */} 0 ? 1 : 0, diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index 0f11b0a677..0852335694 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -378,7 +378,7 @@ const ChatTextArea = forwardRef( charBeforeCursor === " " || charBeforeCursor === "\n" || charBeforeCursor === "\r\n" const charAfterIsWhitespace = charAfterCursor === " " || charAfterCursor === "\n" || charAfterCursor === "\r\n" - // checks if char before cusor is whitespace after a mention + // checks if char before cursor is whitespace after a mention if ( charBeforeIsWhitespace && inputValue.slice(0, cursorPosition - 1).match(new RegExp(mentionRegex.source + "$")) // "$" is added to ensure the match occurs at the end of the string diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index aec4e544a9..07ab484ff6 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -418,7 +418,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie break } } - // textAreaRef.current is not explicitly required here since react gaurantees that ref will be stable across re-renders, and we're not using its value but its reference. + // textAreaRef.current is not explicitly required here since react guarantees that ref will be stable across re-renders, and we're not using its value but its reference. }, [isHidden, textAreaDisabled, enableButtons, handleSendMessage, handlePrimaryButtonClick, handleSecondaryButtonClick], ) diff --git a/webview-ui/src/components/common/CodeBlock.tsx b/webview-ui/src/components/common/CodeBlock.tsx index b00f641d5d..bc5c1fcbcf 100644 --- a/webview-ui/src/components/common/CodeBlock.tsx +++ b/webview-ui/src/components/common/CodeBlock.tsx @@ -120,7 +120,7 @@ const CodeBlock = memo(({ source, forceWrap = false }: CodeBlockProps) => { if (!node.lang) { node.lang = "javascript" } else if (node.lang.includes(".")) { - // if the langauge is a file, get the extension + // if the language is a file, get the extension node.lang = node.lang.split(".").slice(-1)[0] } }) From 5eb8086b421d0882045931c7fd6224bee1dfbfcc Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Fri, 31 Jan 2025 15:21:42 -0800 Subject: [PATCH 04/33] Add o3-mini support to OpenAI --- CHANGELOG.md | 4 +++ package-lock.json | 55 ++++++++++++++---------------- package.json | 4 +-- src/api/providers/openai-native.ts | 25 ++++++++++++++ src/shared/api.ts | 8 +++++ 5 files changed, 64 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7041248a3f..2c9d59f27f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [3.2.11] + +- Add OpenAI o3-mini model + ## [3.2.10] - Improve support for DeepSeek-R1 (deepseek-reasoner) model for OpenRouter, OpenAI-compatible, and DeepSeek direct diff --git a/package-lock.json b/package-lock.json index 26881a7159..f859809461 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "claude-dev", - "version": "3.2.9", + "version": "3.2.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "claude-dev", - "version": "3.2.9", + "version": "3.2.10", "license": "Apache-2.0", "dependencies": { "@anthropic-ai/bedrock-sdk": "^0.10.2", @@ -36,7 +36,7 @@ "isbinaryfile": "^5.0.2", "mammoth": "^1.8.0", "monaco-vscode-textmate-theme-converter": "^0.1.7", - "openai": "^4.61.0", + "openai": "^4.82.0", "os-name": "^6.0.0", "p-wait-for": "^5.0.2", "pdf-parse": "^1.1.1", @@ -5583,12 +5583,6 @@ "integrity": "sha512-+gbBHbNCVGGYw1S9lAIIvrHW47UYOhMIFUsJcMkMrzy1Jf0vulBN3XQIjPgnoOXveMuHnF3b57fXROnY/Or7eg==", "license": "MIT" }, - "node_modules/@types/qs": { - "version": "6.9.16", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz", - "integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==", - "license": "MIT" - }, "node_modules/@types/should": { "version": "11.2.0", "resolved": "https://registry.npmjs.org/@types/should/-/should-11.2.0.tgz", @@ -6479,6 +6473,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", @@ -7029,6 +7024,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", @@ -7410,6 +7406,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, "license": "MIT", "dependencies": { "get-intrinsic": "^1.2.4" @@ -7422,6 +7419,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8306,6 +8304,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8453,6 +8452,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -8678,6 +8678,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, "license": "MIT", "dependencies": { "get-intrinsic": "^1.1.3" @@ -8735,6 +8736,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" @@ -8747,6 +8749,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8759,6 +8762,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8793,6 +8797,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -10548,6 +10553,7 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -10611,28 +10617,30 @@ } }, "node_modules/openai": { - "version": "4.61.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-4.61.0.tgz", - "integrity": "sha512-xkygRBRLIUumxzKGb1ug05pWmJROQsHkGuj/N6Jiw2dj0dI19JvbFpErSZKmJ/DA+0IvpcugZqCAyk8iLpyM6Q==", + "version": "4.82.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.82.0.tgz", + "integrity": "sha512-1bTxOVGZuVGsKKUWbh3BEwX1QxIXUftJv+9COhhGGVDTFwiaOd4gWsMynF2ewj1mg6by3/O+U8+EEHpWRdPaJg==", "license": "Apache-2.0", "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", - "@types/qs": "^6.9.15", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", - "node-fetch": "^2.6.7", - "qs": "^6.10.3" + "node-fetch": "^2.6.7" }, "bin": { "openai": "bin/cli" }, "peerDependencies": { + "ws": "^8.18.0", "zod": "^3.23.8" }, "peerDependenciesMeta": { + "ws": { + "optional": true + }, "zod": { "optional": true } @@ -11329,21 +11337,6 @@ "node": ">=18" } }, - "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -11793,6 +11786,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", @@ -11940,6 +11934,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.7", diff --git a/package.json b/package.json index 2f1167ee04..5618a24c6e 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "claude-dev", "displayName": "Cline", "description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.", - "version": "3.2.10", + "version": "3.2.11", "icon": "assets/icons/icon.png", "galleryBanner": { "color": "#617A91", @@ -238,7 +238,7 @@ "isbinaryfile": "^5.0.2", "mammoth": "^1.8.0", "monaco-vscode-textmate-theme-converter": "^0.1.7", - "openai": "^4.61.0", + "openai": "^4.82.0", "os-name": "^6.0.0", "p-wait-for": "^5.0.2", "pdf-parse": "^1.1.1", diff --git a/src/api/providers/openai-native.ts b/src/api/providers/openai-native.ts index f91a90dbc5..8a47ec4345 100644 --- a/src/api/providers/openai-native.ts +++ b/src/api/providers/openai-native.ts @@ -43,6 +43,31 @@ export class OpenAiNativeHandler implements ApiHandler { } break } + case "o3-mini": { + const stream = await this.client.chat.completions.create({ + model: this.getModel().id, + messages: [{ role: "developer", content: systemPrompt }, ...convertToOpenAiMessages(messages)], + stream: true, + stream_options: { include_usage: true }, + }) + for await (const chunk of stream) { + const delta = chunk.choices[0]?.delta + if (delta?.content) { + yield { + type: "text", + text: delta.content, + } + } + if (chunk.usage) { + yield { + type: "usage", + inputTokens: chunk.usage.prompt_tokens || 0, + outputTokens: chunk.usage.completion_tokens || 0, + } + } + } + break + } default: { const stream = await this.client.chat.completions.create({ model: this.getModel().id, diff --git a/src/shared/api.ts b/src/shared/api.ts index 81eb1d5897..de36d1fb46 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -322,6 +322,14 @@ export const geminiModels = { export type OpenAiNativeModelId = keyof typeof openAiNativeModels export const openAiNativeDefaultModelId: OpenAiNativeModelId = "gpt-4o" export const openAiNativeModels = { + "o3-mini": { + maxTokens: 100_000, + contextWindow: 200_000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 1.1, + outputPrice: 4.4, + }, // don't support tool use yet o1: { maxTokens: 100_000, From a087f5e583c5d8fd9d3e864a75df19492694f049 Mon Sep 17 00:00:00 2001 From: Daniel Trugman Date: Sun, 2 Feb 2025 00:15:57 +0000 Subject: [PATCH 05/33] Fix reasoning_content check for openai & deepseek streams (#1594) --- src/api/providers/deepseek.ts | 2 +- src/api/providers/openai.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/providers/deepseek.ts b/src/api/providers/deepseek.ts index 43aefe7117..763e1ae68f 100644 --- a/src/api/providers/deepseek.ts +++ b/src/api/providers/deepseek.ts @@ -51,7 +51,7 @@ export class DeepSeekHandler implements ApiHandler { } } - if ("reasoning_content" in delta && delta.reasoning_content) { + if (delta && "reasoning_content" in delta && delta.reasoning_content) { yield { type: "reasoning", reasoning: (delta.reasoning_content as string | undefined) || "", diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index e70273041f..fd73abb567 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -56,7 +56,7 @@ export class OpenAiHandler implements ApiHandler { } } - if ("reasoning_content" in delta && delta.reasoning_content) { + if (delta && "reasoning_content" in delta && delta.reasoning_content) { yield { type: "reasoning", reasoning: (delta.reasoning_content as string | undefined) || "", From 5fd60b7000940ad4c240e8fc0e967218b9a9057b Mon Sep 17 00:00:00 2001 From: canvrno <46584286+canvrno@users.noreply.github.com> Date: Sat, 1 Feb 2025 16:32:06 -0800 Subject: [PATCH 06/33] Refactor Shell Detection to Use VS Code Terminal Profiles and Fallback Hierarchy (#1543) * Provide explicit command chaining instructions * Added shell detection for powershell The default-shell library being used only returns cmd for windows users. This change will utilize VS Code API calls to determine the user's shell/terminal settings. MacOS & Linux will, for now, continue to use the existing method. Still working on tests. * Replaced default-shell, added tests Replaced default-shell with local code that replicates the old behavior on macOS & Linux Windows shell detection uses VS Code settings to get the user's default terminal profile Adjusted prompt change * One small change * Removed & attributed old package + typo * Added VSC load for other OSes, refactor, better tests * Fixed system.ts explicit git lines * Added back changes for terminal-command-chaining * One minor, but important change --- src/core/prompts/system.ts | 6 +- src/test/shell.test.ts | 235 +++++++++++++++++++++++++++++++++++++ src/utils/shell.ts | 227 +++++++++++++++++++++++++++++++++++ 3 files changed, 465 insertions(+), 3 deletions(-) create mode 100644 src/test/shell.test.ts create mode 100644 src/utils/shell.ts diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index 3c26f70d75..a043189438 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -1,4 +1,4 @@ -import defaultShell from "default-shell" +import { getShell } from "../../utils/shell" import os from "os" import osName from "os-name" import { McpHub } from "../../services/mcp/McpHub" @@ -38,7 +38,7 @@ Always adhere to this format for the tool use to ensure proper parsing and execu # Tools ## execute_command -Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: ${cwd.toPosix()} +Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: ${cwd.toPosix()} Parameters: - command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. - requires_approval: (required) A boolean indicating whether this command requires explicit user approval before execution in case the user has auto-approve mode enabled. Set to 'true' for potentially impactful operations like installing/uninstalling packages, deleting/overwriting files, system configuration changes, network operations, or any commands that could have unintended side effects. Set to 'false' for safe operations like reading files/directories, running development servers, building projects, and other non-destructive operations. @@ -941,7 +941,7 @@ ${ SYSTEM INFORMATION Operating System: ${osName()} -Default Shell: ${defaultShell} +Default Shell: ${getShell()} Home Directory: ${os.homedir().toPosix()} Current Working Directory: ${cwd.toPosix()} diff --git a/src/test/shell.test.ts b/src/test/shell.test.ts new file mode 100644 index 0000000000..51d0e85dc9 --- /dev/null +++ b/src/test/shell.test.ts @@ -0,0 +1,235 @@ +import { describe, it, beforeEach, afterEach } from "mocha" +import { expect } from "chai" +import { getShell } from "../utils/shell" +import * as vscode from "vscode" +import { userInfo } from "os" + +describe("Shell Detection Tests", () => { + let originalPlatform: string + let originalEnv: NodeJS.ProcessEnv + let originalGetConfig: any + let originalUserInfo: any + + // Helper to mock VS Code configuration + function mockVsCodeConfig(platformKey: string, defaultProfileName: string | null, profiles: Record) { + vscode.workspace.getConfiguration = () => + ({ + get: (key: string) => { + if (key === `defaultProfile.${platformKey}`) { + return defaultProfileName + } + if (key === `profiles.${platformKey}`) { + return profiles + } + return undefined + }, + }) as any + } + + beforeEach(() => { + // Store original references + originalPlatform = process.platform + originalEnv = { ...process.env } + originalGetConfig = vscode.workspace.getConfiguration + originalUserInfo = userInfo + + // Clear environment variables for a clean test + delete process.env.SHELL + delete process.env.COMSPEC + + // Default userInfo() mock + ;(userInfo as any) = () => ({ shell: null }) + }) + + afterEach(() => { + // Restore everything + Object.defineProperty(process, "platform", { value: originalPlatform }) + process.env = originalEnv + vscode.workspace.getConfiguration = originalGetConfig + ;(userInfo as any) = originalUserInfo + }) + + // -------------------------------------------------------------------------- + // Windows Shell Detection + // -------------------------------------------------------------------------- + describe("Windows Shell Detection", () => { + beforeEach(() => { + Object.defineProperty(process, "platform", { value: "win32" }) + }) + + it("uses explicit PowerShell 7 path from VS Code config (profile path)", () => { + mockVsCodeConfig("windows", "PowerShell", { + PowerShell: { path: "C:\\Program Files\\PowerShell\\7\\pwsh.exe" }, + }) + expect(getShell()).to.equal("C:\\Program Files\\PowerShell\\7\\pwsh.exe") + }) + + it("uses PowerShell 7 path if source is 'PowerShell' but no explicit path", () => { + mockVsCodeConfig("windows", "PowerShell", { + PowerShell: { source: "PowerShell" }, + }) + expect(getShell()).to.equal("C:\\Program Files\\PowerShell\\7\\pwsh.exe") + }) + + it("falls back to legacy PowerShell if profile includes 'powershell' but no path/source", () => { + mockVsCodeConfig("windows", "PowerShell", { + PowerShell: {}, + }) + expect(getShell()).to.equal("C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe") + }) + + it("uses WSL bash when profile indicates WSL source", () => { + mockVsCodeConfig("windows", "WSL", { + WSL: { source: "WSL" }, + }) + expect(getShell()).to.equal("/bin/bash") + }) + + it("uses WSL bash when profile name includes 'wsl'", () => { + mockVsCodeConfig("windows", "Ubuntu WSL", { + "Ubuntu WSL": {}, + }) + expect(getShell()).to.equal("/bin/bash") + }) + + it("defaults to cmd.exe if no special profile is matched", () => { + mockVsCodeConfig("windows", "CommandPrompt", { + CommandPrompt: {}, + }) + expect(getShell()).to.equal("C:\\Windows\\System32\\cmd.exe") + }) + + it("respects userInfo() if no VS Code config is available", () => { + vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any + ;(userInfo as any) = () => ({ shell: "C:\\Custom\\PowerShell.exe" }) + + expect(getShell()).to.equal("C:\\Custom\\PowerShell.exe") + }) + + it("respects an odd COMSPEC if no userInfo shell is available", () => { + vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any + process.env.COMSPEC = "D:\\CustomCmd\\cmd.exe" + + expect(getShell()).to.equal("D:\\CustomCmd\\cmd.exe") + }) + }) + + // -------------------------------------------------------------------------- + // macOS Shell Detection + // -------------------------------------------------------------------------- + describe("macOS Shell Detection", () => { + beforeEach(() => { + Object.defineProperty(process, "platform", { value: "darwin" }) + }) + + it("uses VS Code profile path if available", () => { + mockVsCodeConfig("osx", "MyCustomShell", { + MyCustomShell: { path: "/usr/local/bin/fish" }, + }) + expect(getShell()).to.equal("/usr/local/bin/fish") + }) + + it("falls back to userInfo().shell if no VS Code config is available", () => { + vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any + ;(userInfo as any) = () => ({ shell: "/opt/homebrew/bin/zsh" }) + + expect(getShell()).to.equal("/opt/homebrew/bin/zsh") + }) + + it("falls back to SHELL env var if no userInfo shell is found", () => { + vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any + process.env.SHELL = "/usr/local/bin/zsh" + + expect(getShell()).to.equal("/usr/local/bin/zsh") + }) + + it("falls back to /bin/zsh if no config, userInfo, or env variable is set", () => { + vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any + // userInfo => null, SHELL => undefined + expect(getShell()).to.equal("/bin/zsh") + }) + }) + + // -------------------------------------------------------------------------- + // Linux Shell Detection + // -------------------------------------------------------------------------- + describe("Linux Shell Detection", () => { + beforeEach(() => { + Object.defineProperty(process, "platform", { value: "linux" }) + }) + + it("uses VS Code profile path if available", () => { + mockVsCodeConfig("linux", "CustomProfile", { + CustomProfile: { path: "/usr/bin/fish" }, + }) + expect(getShell()).to.equal("/usr/bin/fish") + }) + + it("falls back to userInfo().shell if no VS Code config is available", () => { + vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any + ;(userInfo as any) = () => ({ shell: "/usr/bin/zsh" }) + + expect(getShell()).to.equal("/usr/bin/zsh") + }) + + it("falls back to SHELL env var if no userInfo shell is found", () => { + vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any + process.env.SHELL = "/usr/bin/fish" + + expect(getShell()).to.equal("/usr/bin/fish") + }) + + it("falls back to /bin/bash if nothing is set", () => { + vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any + // userInfo => null, SHELL => undefined + expect(getShell()).to.equal("/bin/bash") + }) + }) + + // -------------------------------------------------------------------------- + // Unknown Platform & Error Handling + // -------------------------------------------------------------------------- + describe("Unknown Platform / Error Handling", () => { + it("falls back to /bin/sh for unknown platforms", () => { + Object.defineProperty(process, "platform", { value: "sunos" }) + vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any + + expect(getShell()).to.equal("/bin/sh") + }) + + it("handles VS Code config errors gracefully, falling back to userInfo shell if present", () => { + Object.defineProperty(process, "platform", { value: "linux" }) + vscode.workspace.getConfiguration = () => { + throw new Error("Configuration error") + } + ;(userInfo as any) = () => ({ shell: "/bin/bash" }) + + expect(getShell()).to.equal("/bin/bash") + }) + + it("handles userInfo errors gracefully, falling back to environment variable if present", () => { + Object.defineProperty(process, "platform", { value: "darwin" }) + vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any + ;(userInfo as any) = () => { + throw new Error("userInfo error") + } + process.env.SHELL = "/bin/zsh" + + expect(getShell()).to.equal("/bin/zsh") + }) + + it("falls back fully to default shell paths if everything fails", () => { + Object.defineProperty(process, "platform", { value: "linux" }) + vscode.workspace.getConfiguration = () => { + throw new Error("Configuration error") + } + ;(userInfo as any) = () => { + throw new Error("userInfo error") + } + // No SHELL in env + delete process.env.SHELL + + expect(getShell()).to.equal("/bin/bash") + }) + }) +}) diff --git a/src/utils/shell.ts b/src/utils/shell.ts new file mode 100644 index 0000000000..8871550a0e --- /dev/null +++ b/src/utils/shell.ts @@ -0,0 +1,227 @@ +import * as vscode from "vscode" +import { userInfo } from "os" + +const SHELL_PATHS = { + // Windows paths + POWERSHELL_7: "C:\\Program Files\\PowerShell\\7\\pwsh.exe", + POWERSHELL_LEGACY: "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", + CMD: "C:\\Windows\\System32\\cmd.exe", + WSL_BASH: "/bin/bash", + // Unix paths + MAC_DEFAULT: "/bin/zsh", + LINUX_DEFAULT: "/bin/bash", + CSH: "/bin/csh", + BASH: "/bin/bash", + KSH: "/bin/ksh", + SH: "/bin/sh", + ZSH: "/bin/zsh", + DASH: "/bin/dash", + TCSH: "/bin/tcsh", + FALLBACK: "/bin/sh", +} as const + +interface MacTerminalProfile { + path?: string +} + +type MacTerminalProfiles = Record + +interface WindowsTerminalProfile { + path?: string + source?: "PowerShell" | "WSL" +} + +type WindowsTerminalProfiles = Record + +interface LinuxTerminalProfile { + path?: string +} + +type LinuxTerminalProfiles = Record + +// ----------------------------------------------------- +// 1) VS Code Terminal Configuration Helpers +// ----------------------------------------------------- + +function getWindowsTerminalConfig() { + try { + const config = vscode.workspace.getConfiguration("terminal.integrated") + const defaultProfileName = config.get("defaultProfile.windows") + const profiles = config.get("profiles.windows") || {} + return { defaultProfileName, profiles } + } catch { + return { defaultProfileName: null, profiles: {} as WindowsTerminalProfiles } + } +} + +function getMacTerminalConfig() { + try { + const config = vscode.workspace.getConfiguration("terminal.integrated") + const defaultProfileName = config.get("defaultProfile.osx") + const profiles = config.get("profiles.osx") || {} + return { defaultProfileName, profiles } + } catch { + return { defaultProfileName: null, profiles: {} as MacTerminalProfiles } + } +} + +function getLinuxTerminalConfig() { + try { + const config = vscode.workspace.getConfiguration("terminal.integrated") + const defaultProfileName = config.get("defaultProfile.linux") + const profiles = config.get("profiles.linux") || {} + return { defaultProfileName, profiles } + } catch { + return { defaultProfileName: null, profiles: {} as LinuxTerminalProfiles } + } +} + +// ----------------------------------------------------- +// 2) Platform-Specific VS Code Shell Retrieval +// ----------------------------------------------------- + +/** Attempts to retrieve a shell path from VS Code config on Windows. */ +function getWindowsShellFromVSCode(): string | null { + const { defaultProfileName, profiles } = getWindowsTerminalConfig() + if (!defaultProfileName) { + return null + } + + const profile = profiles[defaultProfileName] + + // If the profile name indicates PowerShell, do version-based detection. + // In testing it was found these typically do not have a path, and this + // implementation manages to deductively get the corect version of PowerShell + if (defaultProfileName.toLowerCase().includes("powershell")) { + if (profile?.path) { + // If there's an explicit PowerShell path, return that + return profile.path + } else if (profile?.source === "PowerShell") { + // If the profile is sourced from PowerShell, assume the newest + return SHELL_PATHS.POWERSHELL_7 + } + // Otherwise, assume legacy Windows PowerShell + return SHELL_PATHS.POWERSHELL_LEGACY + } + + // If there's a specific path, return that immediately + if (profile.path) { + return profile.path + } + + // If the profile indicates WSL + if (profile?.source === "WSL" || defaultProfileName.toLowerCase().includes("wsl")) { + return SHELL_PATHS.WSL_BASH + } + + // If nothing special detected, we assume cmd + return SHELL_PATHS.CMD +} + +/** Attempts to retrieve a shell path from VS Code config on macOS. */ +function getMacShellFromVSCode(): string | null { + const { defaultProfileName, profiles } = getMacTerminalConfig() + if (!defaultProfileName) { + return null + } + + const profile = profiles[defaultProfileName] + return profile?.path || null +} + +/** Attempts to retrieve a shell path from VS Code config on Linux. */ +function getLinuxShellFromVSCode(): string | null { + const { defaultProfileName, profiles } = getLinuxTerminalConfig() + if (!defaultProfileName) { + return null + } + + const profile = profiles[defaultProfileName] + return profile?.path || null +} + +// ----------------------------------------------------- +// 3) General Fallback Helpers +// ----------------------------------------------------- + +/** + * Tries to get a user’s shell from os.userInfo() (works on Unix if the + * underlying system call is supported). Returns null on error or if not found. + */ +function getShellFromUserInfo(): string | null { + try { + const { shell } = userInfo() + return shell || null + } catch { + return null + } +} + +/** Returns the environment-based shell variable, or null if not set. */ +function getShellFromEnv(): string | null { + const { env } = process + + if (process.platform === "win32") { + // On Windows, COMSPEC typically holds cmd.exe + return env.COMSPEC || "C:\\Windows\\System32\\cmd.exe" + } + + if (process.platform === "darwin") { + // On macOS/Linux, SHELL is commonly the environment variable + return env.SHELL || "/bin/zsh" + } + + if (process.platform === "linux") { + // On Linux, SHELL is commonly the environment variable + return env.SHELL || "/bin/bash" + } + return null +} + +// ----------------------------------------------------- +// 4) Publicly Exposed Shell Getter +// ----------------------------------------------------- + +export function getShell(): string { + // 1. Check VS Code config first. + if (process.platform === "win32") { + // Special logic for Windows + const windowsShell = getWindowsShellFromVSCode() + if (windowsShell) { + return windowsShell + } + } else if (process.platform === "darwin") { + // macOS from VS Code + const macShell = getMacShellFromVSCode() + if (macShell) { + return macShell + } + } else if (process.platform === "linux") { + // Linux from VS Code + const linuxShell = getLinuxShellFromVSCode() + if (linuxShell) { + return linuxShell + } + } + + // 2. If no shell from VS Code, try userInfo() + const userInfoShell = getShellFromUserInfo() + if (userInfoShell) { + return userInfoShell + } + + // 3. If still nothing, try environment variable + const envShell = getShellFromEnv() + if (envShell) { + return envShell + } + + // 4. Finally, fall back to a default + if (process.platform === "win32") { + // On Windows, if we got here, we have no config, no COMSPEC, and one very messed up operating system. + // Use CMD as a last resort + return SHELL_PATHS.CMD + } + // On macOS/Linux, fallback to a POSIX shell - This is the behavior of our old shell detection method. + return SHELL_PATHS.FALLBACK +} From 162cd3b9c552f29ccb3d8aa8ec2ff7fc722c52ba Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Sat, 1 Feb 2025 18:32:48 -0800 Subject: [PATCH 07/33] Prepare for release --- CHANGELOG.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c9d59f27f..2f6d5e5d65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [3.2.12] + +- Fix command chaining for Windows users +- Fix reasoning_content error for OpenAI providers + ## [3.2.11] - Add OpenAI o3-mini model diff --git a/package.json b/package.json index 5618a24c6e..03702f04fa 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "claude-dev", "displayName": "Cline", "description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.", - "version": "3.2.11", + "version": "3.2.12", "icon": "assets/icons/icon.png", "galleryBanner": { "color": "#617A91", From 466b1982f26f63e74a290b633433870b2cea4a63 Mon Sep 17 00:00:00 2001 From: Michael Overhorst Date: Mon, 3 Feb 2025 13:34:16 +0100 Subject: [PATCH 08/33] feat: Add support for all available Mistral API models - Add all available Mistral models with specific version numbers - Include Premier models (Mistral Large, Pixtral, Ministral, etc.) - Include Free models (Mistral Small, Pixtral 12B, etc.) - Set correct token limits and pricing for each model Fixes #1609 --- src/shared/api.ts | 86 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 83 insertions(+), 3 deletions(-) diff --git a/src/shared/api.ts b/src/shared/api.ts index de36d1fb46..8bda7877dd 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -408,10 +408,90 @@ export const deepSeekModels = { // Mistral // https://docs.mistral.ai/getting-started/models/models_overview/ export type MistralModelId = keyof typeof mistralModels -export const mistralDefaultModelId: MistralModelId = "codestral-latest" +export const mistralDefaultModelId: MistralModelId = "codestral-2501" export const mistralModels = { - "codestral-latest": { - maxTokens: 32_768, + "mistral-large-2411": { + maxTokens: 131_000, + contextWindow: 131_000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 2.0, + outputPrice: 6.0, + }, + "pixtral-large-2411": { + maxTokens: 131_000, + contextWindow: 131_000, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 2.0, + outputPrice: 6.0, + }, + "ministral-3b-2410": { + maxTokens: 131_000, + contextWindow: 131_000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.04, + outputPrice: 0.04, + }, + "ministral-8b-2410": { + maxTokens: 131_000, + contextWindow: 131_000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.1, + outputPrice: 0.1, + }, + "mistral-embed": { + maxTokens: 8_000, + contextWindow: 8_000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.1, + outputPrice: 0.1, + }, + "mistral-moderation-2411": { + maxTokens: 8_000, + contextWindow: 8_000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.1, + outputPrice: 0.1, + }, + "mistral-small-2501": { + maxTokens: 32_000, + contextWindow: 32_000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.1, + outputPrice: 0.3, + }, + "pixtral-12b-2409": { + maxTokens: 131_000, + contextWindow: 131_000, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0.15, + outputPrice: 0.15, + }, + "open-mistral-nemo-2407": { + maxTokens: 131_000, + contextWindow: 131_000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.15, + outputPrice: 0.15, + }, + "open-codestral-mamba": { + maxTokens: 256_000, + contextWindow: 256_000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.15, + outputPrice: 0.15, + }, + "codestral-2501": { + maxTokens: 256_000, contextWindow: 256_000, supportsImages: false, supportsPromptCache: false, From fa67db75e0ae2abc19de970d8ee1bf7854aab0bc Mon Sep 17 00:00:00 2001 From: Evan <58194240+celestial-vault@users.noreply.github.com> Date: Tue, 4 Feb 2025 08:53:49 +0800 Subject: [PATCH 09/33] Add clineignore class into cline file (#1623) --- src/core/Cline.ts | 8 +++ .../LLMFileAccessController.test.ts | 53 ++++++++--------- .../LLMFileAccessController.ts | 59 +++++++++++++++++-- 3 files changed, 89 insertions(+), 31 deletions(-) diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 9561d3b281..2a6aaa4aa8 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -46,6 +46,7 @@ import { HistoryItem } from "../shared/HistoryItem" import { ClineAskResponse, ClineCheckpointRestore } from "../shared/WebviewMessage" import { calculateApiCost } from "../utils/cost" import { fileExistsAtPath } from "../utils/fs" +import { LLMFileAccessController } from "../services/llm-access-control/LLMFileAccessController" import { arePathsEqual, getReadablePath } from "../utils/path" import { fixModelHtmlEscaping, removeInvalidChars } from "../utils/string" import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from "./assistant-message" @@ -80,6 +81,7 @@ export class Cline { private chatSettings: ChatSettings apiConversationHistory: Anthropic.MessageParam[] = [] clineMessages: ClineMessage[] = [] + private llmAccessController: LLMFileAccessController private askResponse?: ClineAskResponse private askResponseText?: string private askResponseImages?: string[] @@ -123,6 +125,10 @@ export class Cline { images?: string[], historyItem?: HistoryItem, ) { + this.llmAccessController = new LLMFileAccessController(cwd) + this.llmAccessController.initialize().catch((error) => { + console.error("Failed to initialize LLMFileAccessController:", error) + }) this.providerRef = new WeakRef(provider) this.api = buildApiHandler(apiConfiguration) this.terminalManager = new TerminalManager() @@ -748,6 +754,7 @@ export class Cline { // if the extension process were killed, then on restart the clineMessages might not be empty, so we need to set it to [] when we create a new Cline client (otherwise webview would show stale messages from previous session) this.clineMessages = [] this.apiConversationHistory = [] + await this.providerRef.deref()?.postStateToWebview() await this.say("text", task, images) @@ -1050,6 +1057,7 @@ export class Cline { this.terminalManager.disposeAll() this.urlContentFetcher.closeBrowser() this.browserSession.closeBrowser() + this.llmAccessController.dispose() await this.diffViewProvider.revertChanges() // need to await for when we want to make sure directories/files are reverted before re-starting the task from a checkpoint } diff --git a/src/services/llm-access-control/LLMFileAccessController.test.ts b/src/services/llm-access-control/LLMFileAccessController.test.ts index b8cee93e9a..8bf6353a48 100644 --- a/src/services/llm-access-control/LLMFileAccessController.test.ts +++ b/src/services/llm-access-control/LLMFileAccessController.test.ts @@ -33,44 +33,44 @@ describe("LLMFileAccessController", () => { describe("Default Patterns", () => { // it("should block access to common ignored files", async () => { - // const results = await Promise.all([ + // const results = [ // controller.validateAccess(".env"), // controller.validateAccess(".git/config"), // controller.validateAccess("node_modules/package.json"), - // ]) + // ] // results.forEach((result) => result.should.be.false()) // }) it("should allow access to regular files", async () => { - const results = await Promise.all([ + const results = [ controller.validateAccess("src/index.ts"), controller.validateAccess("README.md"), controller.validateAccess("package.json"), - ]) + ] results.forEach((result) => result.should.be.true()) }) }) describe("Custom Patterns", () => { it("should block access to custom ignored patterns", async () => { - const results = await Promise.all([ + const results = [ controller.validateAccess("config.secret"), controller.validateAccess("private/data.txt"), controller.validateAccess("temp.json"), controller.validateAccess("nested/deep/file.secret"), controller.validateAccess("private/nested/deep/file.txt"), - ]) + ] results.forEach((result) => result.should.be.false()) }) it("should allow access to non-ignored files", async () => { - const results = await Promise.all([ + const results = [ controller.validateAccess("public/data.txt"), controller.validateAccess("config.json"), controller.validateAccess("src/temp/file.ts"), controller.validateAccess("nested/deep/file.txt"), controller.validateAccess("not-private/data.txt"), - ]) + ] results.forEach((result) => result.should.be.true()) }) @@ -83,11 +83,11 @@ describe("LLMFileAccessController", () => { controller = new LLMFileAccessController(tempDir) await controller.initialize() - const results = await Promise.all([ + const results = [ controller.validateAccess("data-123.json"), // Should be false (wildcard) controller.validateAccess("data.json"), // Should be true (doesn't match pattern) controller.validateAccess("script.tmp"), // Should be false (extension match) - ]) + ] results[0].should.be.false() // data-123.json results[1].should.be.true() // data.json @@ -112,9 +112,8 @@ describe("LLMFileAccessController", () => { // ) // controller = new LLMFileAccessController(tempDir) - // await controller.initialize() - // const results = await Promise.all([ + // const results = [ // // Basic negation // controller.validateAccess("temp/file.txt"), // Should be false (in temp/) // controller.validateAccess("temp/allowed/file.txt"), // Should be true (negated) @@ -130,7 +129,7 @@ describe("LLMFileAccessController", () => { // controller.validateAccess("assets/logo.png"), // Should be false (in assets/) // controller.validateAccess("assets/public/logo.png"), // Should be true (negated and matches *.png) // controller.validateAccess("assets/public/data.json"), // Should be true (in negated public/) - // ]) + // ] // results[0].should.be.false() // temp/file.txt // results[1].should.be.true() // temp/allowed/file.txt @@ -154,7 +153,7 @@ describe("LLMFileAccessController", () => { controller = new LLMFileAccessController(tempDir) await controller.initialize() - const result = await controller.validateAccess("test.secret") + const result = controller.validateAccess("test.secret") result.should.be.false() }) }) @@ -163,55 +162,55 @@ describe("LLMFileAccessController", () => { it("should handle absolute paths and match ignore patterns", async () => { // Test absolute path that should be allowed const allowedPath = path.join(tempDir, "src/file.ts") - const allowedResult = await controller.validateAccess(allowedPath) + const allowedResult = controller.validateAccess(allowedPath) allowedResult.should.be.true() // Test absolute path that matches an ignore pattern (*.secret) const ignoredPath = path.join(tempDir, "config.secret") - const ignoredResult = await controller.validateAccess(ignoredPath) + const ignoredResult = controller.validateAccess(ignoredPath) ignoredResult.should.be.false() // Test absolute path in ignored directory (private/) const ignoredDirPath = path.join(tempDir, "private/data.txt") - const ignoredDirResult = await controller.validateAccess(ignoredDirPath) + const ignoredDirResult = controller.validateAccess(ignoredDirPath) ignoredDirResult.should.be.false() }) it("should handle relative paths and match ignore patterns", async () => { // Test relative path that should be allowed - const allowedResult = await controller.validateAccess("./src/file.ts") + const allowedResult = controller.validateAccess("./src/file.ts") allowedResult.should.be.true() // Test relative path that matches an ignore pattern (*.secret) - const ignoredResult = await controller.validateAccess("./config.secret") + const ignoredResult = controller.validateAccess("./config.secret") ignoredResult.should.be.false() // Test relative path in ignored directory (private/) - const ignoredDirResult = await controller.validateAccess("./private/data.txt") + const ignoredDirResult = controller.validateAccess("./private/data.txt") ignoredDirResult.should.be.false() }) it("should normalize paths with backslashes", async () => { - const result = await controller.validateAccess("src\\file.ts") + const result = controller.validateAccess("src\\file.ts") result.should.be.true() }) it("should handle paths outside cwd", async () => { // Create a path that points to parent directory of cwd const outsidePath = path.join(path.dirname(tempDir), "outside.txt") - const result = await controller.validateAccess(outsidePath) + const result = controller.validateAccess(outsidePath) // Should return false for security since path is outside cwd result.should.be.false() // Test with a deeply nested path outside cwd const deepOutsidePath = path.join(path.dirname(tempDir), "deep", "nested", "outside.secret") - const deepResult = await controller.validateAccess(deepOutsidePath) + const deepResult = controller.validateAccess(deepOutsidePath) deepResult.should.be.false() // Test with a path that tries to escape using ../ const escapeAttemptPath = path.join(tempDir, "..", "escape-attempt.txt") - const escapeResult = await controller.validateAccess(escapeAttemptPath) + const escapeResult = controller.validateAccess(escapeAttemptPath) escapeResult.should.be.false() }) }) @@ -228,7 +227,7 @@ describe("LLMFileAccessController", () => { describe("Error Handling", () => { it("should handle invalid paths", async () => { // Test with an invalid path containing null byte - const result = await controller.validateAccess("\0invalid") + const result = controller.validateAccess("\0invalid") result.should.be.true() }) @@ -240,7 +239,7 @@ describe("LLMFileAccessController", () => { try { const controller = new LLMFileAccessController(emptyDir) await controller.initialize() - const result = await controller.validateAccess("file.txt") + const result = controller.validateAccess("file.txt") result.should.be.true() } finally { await fs.rm(emptyDir, { recursive: true, force: true }) @@ -253,7 +252,7 @@ describe("LLMFileAccessController", () => { controller = new LLMFileAccessController(tempDir) await controller.initialize() - const result = await controller.validateAccess("regular-file.txt") + const result = controller.validateAccess("regular-file.txt") result.should.be.true() }) }) diff --git a/src/services/llm-access-control/LLMFileAccessController.ts b/src/services/llm-access-control/LLMFileAccessController.ts index b5139c43a8..40a2e46b55 100644 --- a/src/services/llm-access-control/LLMFileAccessController.ts +++ b/src/services/llm-access-control/LLMFileAccessController.ts @@ -2,6 +2,7 @@ import path from "path" import { fileExistsAtPath } from "../../utils/fs" import fs from "fs/promises" import ignore, { Ignore } from "ignore" +import * as vscode from "vscode" /** * Controls LLM access to files by enforcing ignore patterns. @@ -11,6 +12,8 @@ import ignore, { Ignore } from "ignore" export class LLMFileAccessController { private cwd: string private ignoreInstance: Ignore + private fileWatcher: vscode.FileSystemWatcher | null + private disposables: vscode.Disposable[] = [] /** * Default patterns that are always ignored for security @@ -20,19 +23,49 @@ export class LLMFileAccessController { constructor(cwd: string) { this.cwd = cwd this.ignoreInstance = ignore() - - // Add default patterns immediately this.ignoreInstance.add(LLMFileAccessController.DEFAULT_PATTERNS) + this.fileWatcher = null + + // Set up file watcher for .clineignore + this.setupFileWatcher() } /** * Initialize the controller by loading custom patterns - * This must be called and awaited before using the controller + * Must be called after construction and before using the controller */ async initialize(): Promise { await this.loadCustomPatterns() } + /** + * Set up the file watcher for .clineignore changes + */ + private setupFileWatcher(): void { + const clineignorePattern = new vscode.RelativePattern(this.cwd, ".clineignore") + this.fileWatcher = vscode.workspace.createFileSystemWatcher(clineignorePattern) + + // Watch for changes and updates + this.disposables.push( + this.fileWatcher.onDidChange(() => { + this.loadCustomPatterns().catch((error) => { + console.error("Failed to load updated .clineignore patterns:", error) + }) + }), + this.fileWatcher.onDidCreate(() => { + this.loadCustomPatterns().catch((error) => { + console.error("Failed to load new .clineignore patterns:", error) + }) + }), + this.fileWatcher.onDidDelete(() => { + this.resetToDefaultPatterns() + }), + ) + + // Add fileWatcher itself to disposables + this.disposables.push(this.fileWatcher) + } + /** * Load custom patterns from .clineignore if it exists */ @@ -40,6 +73,8 @@ export class LLMFileAccessController { try { const ignorePath = path.join(this.cwd, ".clineignore") if (await fileExistsAtPath(ignorePath)) { + // Reset ignore instance to prevent duplicate patterns + this.resetToDefaultPatterns() const content = await fs.readFile(ignorePath, "utf8") const customPatterns = content .split("\n") @@ -49,11 +84,18 @@ export class LLMFileAccessController { this.ignoreInstance.add(customPatterns) } } catch (error) { - console.error("Failed to load .clineignore:", error) // Continue with default patterns } } + /** + * Reset ignore patterns to defaults + */ + private resetToDefaultPatterns(): void { + this.ignoreInstance = ignore() + this.ignoreInstance.add(LLMFileAccessController.DEFAULT_PATTERNS) + } + /** * Check if a file should be accessible to the LLM * @param filePath - Path to check (relative to cwd) @@ -97,4 +139,13 @@ export class LLMFileAccessController { return [] // Fail closed for security } } + + /** + * Clean up resources when the controller is no longer needed + */ + dispose(): void { + this.disposables.forEach((d) => d.dispose()) + this.disposables = [] + this.fileWatcher = null + } } From 650854958421193955688b2a9f90a515a8bc5c33 Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Tue, 4 Feb 2025 12:22:45 -0500 Subject: [PATCH 10/33] Update copyright year (#1637) * Update copyright year * Update LICENSE --- LICENSE | 4 ++-- README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index b8f1f99fda..5fb83b31e2 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2024 Cline Bot Inc. + Copyright 2025 Cline Bot Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -198,4 +198,4 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file + limitations under the License. diff --git a/README.md b/README.md index ccd711b9c8..b3e8959beb 100644 --- a/README.md +++ b/README.md @@ -187,4 +187,4 @@ To contribute to the project, start with our [Contributing Guide](CONTRIBUTING.m ## License -[Apache 2.0 © 2024 Cline Bot Inc.](./LICENSE) +[Apache 2.0 © 2025 Cline Bot Inc.](./LICENSE) From e41ff7877024fb0a7c6949bbc6ee7ffd964a1dc6 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 4 Feb 2025 18:40:57 +0100 Subject: [PATCH 11/33] feat:show api error status code (#1635) * Show status code / message when API request error occurs. * Moved logic to a helper method. * Different error message format. * Removed old comment. --------- Co-authored-by: Michael Overhorst --- src/core/Cline.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 2a6aaa4aa8..6c1239cae3 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -1202,6 +1202,14 @@ export class Cline { return false } + private formatErrorWithStatusCode(error: any): string { + const statusCode = error.status || error.statusCode || (error.response && error.response.status) + const message = error.message ?? JSON.stringify(serializeError(error), null, 2) + + // Only prepend the statusCode if it's not already part of the message + return statusCode && !message.includes(statusCode.toString()) ? `${statusCode} - ${message}` : message + } + async *attemptApiRequest(previousApiReqIndex: number): ApiStream { // Wait for MCP servers to be connected before generating system prompt await pWaitFor(() => this.providerRef.deref()?.mcpHub?.isConnecting !== true, { timeout: 10_000 }).catch(() => { @@ -1309,10 +1317,9 @@ export class Cline { } else { // request failed after retrying automatically once, ask user if they want to retry again // note that this api_req_failed ask is unique in that we only present this option if the api hasn't streamed any content yet (ie it fails on the first chunk due), as it would allow them to hit a retry button. However if the api failed mid-stream, it could be in any arbitrary state where some tools may have executed, so that error is handled differently and requires cancelling the task entirely. - const { response } = await this.ask( - "api_req_failed", - error.message ?? JSON.stringify(serializeError(error), null, 2), - ) + const errorMessage = this.formatErrorWithStatusCode(error) + + const { response } = await this.ask("api_req_failed", errorMessage) if (response !== "yesButtonClicked") { // this will never happen since if noButtonClicked, we will clear current task, aborting this instance throw new Error("API request failed") @@ -3060,7 +3067,9 @@ export class Cline { // abandoned happens when extension is no longer waiting for the cline instance to finish aborting (error is thrown here when any function in the for loop throws due to this.abort) if (!this.abandoned) { this.abortTask() // if the stream failed, there's various states the task could be in (i.e. could have streamed some tools the user may have executed), so we just resort to replicating a cancel task - await abortStream("streaming_failed", error.message ?? JSON.stringify(serializeError(error), null, 2)) + const errorMessage = this.formatErrorWithStatusCode(error) + + await abortStream("streaming_failed", errorMessage) const history = await this.providerRef.deref()?.getTaskWithId(this.taskId) if (history) { await this.providerRef.deref()?.initClineWithHistoryItem(history.historyItem) From 42924c971f16bb421dd5aaa119d9edb4c88525fe Mon Sep 17 00:00:00 2001 From: watany <76135106+watany-dev@users.noreply.github.com> Date: Wed, 5 Feb 2025 02:41:39 +0900 Subject: [PATCH 12/33] chore: reduse cost openai (#1629) --- src/shared/api.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/shared/api.ts b/src/shared/api.ts index de36d1fb46..34020dec28 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -352,16 +352,16 @@ export const openAiNativeModels = { contextWindow: 128_000, supportsImages: true, supportsPromptCache: false, - inputPrice: 3, - outputPrice: 12, + inputPrice: 1.1, + outputPrice: 4.4, }, "gpt-4o": { maxTokens: 4_096, contextWindow: 128_000, supportsImages: true, supportsPromptCache: false, - inputPrice: 5, - outputPrice: 15, + inputPrice: 2.5, + outputPrice: 10, }, "gpt-4o-mini": { maxTokens: 16_384, From 180fbd5995948768c17f3659a46fec1e58cbda2c Mon Sep 17 00:00:00 2001 From: Hiroki Nakashima Date: Wed, 5 Feb 2025 07:46:13 +0900 Subject: [PATCH 13/33] feat: add LiteLLM API provider support (#1618) --- src/api/index.ts | 3 + src/api/providers/litellm.ts | 60 +++++++++++++++++++ src/core/webview/ClineProvider.ts | 18 ++++++ src/shared/api.ts | 16 +++++ .../src/components/settings/ApiOptions.tsx | 41 ++++++++++++- 5 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 src/api/providers/litellm.ts diff --git a/src/api/index.ts b/src/api/index.ts index 2ef82f8659..5ed9e6de8c 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -13,6 +13,7 @@ import { ApiStream } from "./transform/stream" import { DeepSeekHandler } from "./providers/deepseek" import { MistralHandler } from "./providers/mistral" import { VsCodeLmHandler } from "./providers/vscode-lm" +import { LiteLlmHandler } from "./providers/litellm" export interface ApiHandler { createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream @@ -50,6 +51,8 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler { return new MistralHandler(options) case "vscode-lm": return new VsCodeLmHandler(options) + case "litellm": + return new LiteLlmHandler(options) default: return new AnthropicHandler(options) } diff --git a/src/api/providers/litellm.ts b/src/api/providers/litellm.ts new file mode 100644 index 0000000000..80ad5e2c75 --- /dev/null +++ b/src/api/providers/litellm.ts @@ -0,0 +1,60 @@ +import { Anthropic } from "@anthropic-ai/sdk" +import OpenAI from "openai" +import { ApiHandlerOptions, liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults } from "../../shared/api" +import { ApiHandler } from ".." +import { ApiStream } from "../transform/stream" +import { convertToOpenAiMessages } from "../transform/openai-format" + +export class LiteLlmHandler implements ApiHandler { + private options: ApiHandlerOptions + private client: OpenAI + + constructor(options: ApiHandlerOptions) { + this.options = options + this.client = new OpenAI({ + baseURL: this.options.liteLlmBaseUrl || "http://localhost:4000", + apiKey: "not-needed", + }) + } + + async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { + const formattedMessages = convertToOpenAiMessages(messages) + const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = { + role: "system", + content: systemPrompt, + } + + const stream = await this.client.chat.completions.create({ + model: this.options.liteLlmModelId || liteLlmDefaultModelId, + messages: [systemMessage, ...formattedMessages], + temperature: 0, + stream: true, + stream_options: { include_usage: true }, + }) + + for await (const chunk of stream) { + const delta = chunk.choices[0]?.delta + if (delta?.content) { + yield { + type: "text", + text: delta.content, + } + } + + if (chunk.usage) { + yield { + type: "usage", + inputTokens: chunk.usage.prompt_tokens || 0, + outputTokens: chunk.usage.completion_tokens || 0, + } + } + } + } + + getModel() { + return { + id: this.options.liteLlmModelId || liteLlmDefaultModelId, + info: liteLlmModelInfoSaneDefaults, + } + } +} diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 12a36daddf..a15b99ed05 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -76,6 +76,8 @@ type GlobalStateKey = | "previousModeApiProvider" | "previousModeModelId" | "previousModeModelInfo" + | "liteLlmBaseUrl" + | "liteLlmModelId" export const GlobalFileNames = { apiConversationHistory: "api_conversation_history.json", @@ -443,6 +445,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { openRouterModelId, openRouterModelInfo, vsCodeLmModelSelector, + liteLlmBaseUrl, + liteLlmModelId, } = message.apiConfiguration await this.updateGlobalState("apiProvider", apiProvider) await this.updateGlobalState("apiModelId", apiModelId) @@ -471,6 +475,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.updateGlobalState("openRouterModelId", openRouterModelId) await this.updateGlobalState("openRouterModelInfo", openRouterModelInfo) await this.updateGlobalState("vsCodeLmModelSelector", vsCodeLmModelSelector) + await this.updateGlobalState("liteLlmBaseUrl", liteLlmBaseUrl) + await this.updateGlobalState("liteLlmModelId", liteLlmModelId) if (this.cline) { this.cline.api = buildApiHandler(message.apiConfiguration) } @@ -535,6 +541,9 @@ export class ClineProvider implements vscode.WebviewViewProvider { case "lmstudio": await this.updateGlobalState("previousModeModelId", apiConfiguration.lmStudioModelId) break + case "litellm": + await this.updateGlobalState("previousModeModelId", apiConfiguration.liteLlmModelId) + break } // Restore the model used in previous mode @@ -563,6 +572,9 @@ export class ClineProvider implements vscode.WebviewViewProvider { case "lmstudio": await this.updateGlobalState("lmStudioModelId", newModelId) break + case "litellm": + await this.updateGlobalState("liteLlmModelId", newModelId) + break } if (this.cline) { @@ -1364,6 +1376,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { browserSettings, chatSettings, vsCodeLmModelSelector, + liteLlmBaseUrl, + liteLlmModelId, userInfo, authToken, previousModeApiProvider, @@ -1403,6 +1417,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.getGlobalState("browserSettings") as Promise, this.getGlobalState("chatSettings") as Promise, this.getGlobalState("vsCodeLmModelSelector") as Promise, + this.getGlobalState("liteLlmBaseUrl") as Promise, + this.getGlobalState("liteLlmModelId") as Promise, this.getGlobalState("userInfo") as Promise, this.getSecret("authToken") as Promise, this.getGlobalState("previousModeApiProvider") as Promise, @@ -1453,6 +1469,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { openRouterModelId, openRouterModelInfo, vsCodeLmModelSelector, + liteLlmBaseUrl, + liteLlmModelId, }, lastShownAnnouncementId, customInstructions, diff --git a/src/shared/api.ts b/src/shared/api.ts index 34020dec28..5c4f1c9486 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -11,10 +11,13 @@ export type ApiProvider = | "deepseek" | "mistral" | "vscode-lm" + | "litellm" export interface ApiHandlerOptions { apiModelId?: string apiKey?: string // anthropic + liteLlmBaseUrl?: string + liteLlmModelId?: string anthropicBaseUrl?: string openRouterApiKey?: string openRouterModelId?: string @@ -419,3 +422,16 @@ export const mistralModels = { outputPrice: 0.9, }, } as const satisfies Record + +// LiteLLM +// https://docs.litellm.ai/docs/ +export type LiteLLMModelId = string +export const liteLlmDefaultModelId = "gpt-3.5-turbo" +export const liteLlmModelInfoSaneDefaults: ModelInfo = { + maxTokens: 4096, + contextWindow: 8192, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, +} diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index ceb75a0ee4..84018b9efd 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -133,7 +133,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is VSCodeDropdown has an open bug where dynamically rendered options don't auto select the provided value prop. You can see this for yourself by comparing it with normal select/option elements, which work as expected. https://github.com/microsoft/vscode-webview-ui-toolkit/issues/433 - In our case, when the user switches between providers, we recalculate the selectedModelId depending on the provider, the default model for that provider, and a modelId that the user may have selected. Unfortunately, the VSCodeDropdown component wouldn't select this calculated value, and would default to the first "Select a model..." option instead, which makes it seem like the model was cleared out when it wasn't. + In our case, when the user switches between providers, we recalculate the selectedModelId depending on the provider, the default model for that provider, and a modelId that the user may have selected. Unfortunately, the VSCodeDropdown component wouldn't select this calculated value, and would default to the first "Select a model..." option instead, which makes it seem like the model was cleared out when it wasn't. As a workaround, we create separate instances of the dropdown for each provider, and then conditionally render the one that matches the current provider. */ @@ -187,6 +187,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is VS Code LM API LM Studio Ollama + LiteLLM @@ -739,6 +740,38 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is )} + {selectedProvider === "litellm" && ( +
+ + Base URL (optional) + + + Model ID + +

+ LiteLLM provides a unified interface to access various LLM providers' models. See their{" "} + + quickstart guide + {" "} + for more information. +

+
+ )} + {selectedProvider === "ollama" && (
Date: Wed, 5 Feb 2025 07:56:04 +0700 Subject: [PATCH 14/33] Fix installing-dev-essentials.md (#1600) * Fix installing-dev-essentials.md Fix installing-dev-essentials.md * Fix link --- docs/getting-started-new-coders/installing-dev-essentials.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started-new-coders/installing-dev-essentials.md b/docs/getting-started-new-coders/installing-dev-essentials.md index 9b22353afb..68e1b10f5b 100644 --- a/docs/getting-started-new-coders/installing-dev-essentials.md +++ b/docs/getting-started-new-coders/installing-dev-essentials.md @@ -102,4 +102,4 @@ The **Problems** section in VS Code shows any errors or warnings in your code. Y ## Next Steps -After installing these tools, you'll be ready to start coding! Return to the [Getting Started with Cline for New Coders](getting-started-new-coders.md) guide to continue your journey. +After installing these tools, you'll be ready to start coding! Return to the [Getting Started with Cline for New Coders](../getting-started-new-coders/README.md) guide to continue your journey. From 36d40afb4eec31b9f7679e649e11e1455f46d160 Mon Sep 17 00:00:00 2001 From: Ocasta Date: Tue, 4 Feb 2025 18:58:51 -0800 Subject: [PATCH 15/33] changelogs --- .changeset/changelog-config.js | 20 +++ .changeset/config.json | 4 +- .changeset/twelve-deers-search.md | 5 + .github/pull_request_template.md | 1 + .../scripts/overwrite_changeset_changelog.py | 62 +++++++ .github/workflows/changeset-release.yml | 158 ++++++++++++++++++ .github/workflows/check-changeset.yml | 78 +++++++++ CONTRIBUTING.md | 16 +- package-lock.json | 4 +- package.json | 3 +- 10 files changed, 343 insertions(+), 8 deletions(-) create mode 100644 .changeset/changelog-config.js create mode 100644 .changeset/twelve-deers-search.md create mode 100644 .github/scripts/overwrite_changeset_changelog.py create mode 100644 .github/workflows/changeset-release.yml create mode 100644 .github/workflows/check-changeset.yml diff --git a/.changeset/changelog-config.js b/.changeset/changelog-config.js new file mode 100644 index 0000000000..1e64dbf093 --- /dev/null +++ b/.changeset/changelog-config.js @@ -0,0 +1,20 @@ +// Half-works to simplify the format but needs 'overwrite_changeset_changelog.py' in GHA to finish formatting + +const getReleaseLine = async (changeset) => { + const [firstLine] = changeset.summary + .split("\n") + .map((l) => l.trim()) + .filter(Boolean) + return `- ${firstLine}` +} + +const getDependencyReleaseLine = async () => { + return "" +} + +const changelogFunctions = { + getReleaseLine, + getDependencyReleaseLine, +} + +module.exports = changelogFunctions diff --git a/.changeset/config.json b/.changeset/config.json index 42efc1c834..bcd6eefa00 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -1,6 +1,6 @@ { - "$schema": "https://unpkg.com/@changesets/config@3.0.5/schema.json", - "changelog": "@changesets/cli/changelog", + "$schema": "https://unpkg.com/@changesets/config@3.0.4/schema.json", + "changelog": "./changelog-config.js", "commit": false, "fixed": [], "linked": [], diff --git a/.changeset/twelve-deers-search.md b/.changeset/twelve-deers-search.md new file mode 100644 index 0000000000..f87090b079 --- /dev/null +++ b/.changeset/twelve-deers-search.md @@ -0,0 +1,5 @@ +--- +"claude-dev": minor +--- + +Adding changesets for automating version bumping and release notes diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 22a8a9976e..989040aab2 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -21,6 +21,7 @@ - [ ] Changes are limited to a single feature, bugfix or chore (split larger changes into separate PRs) - [ ] Tests are passing (`npm test`) and code is formatted and linted (`npm run format && npm run lint`) +- [ ] I have created a changeset using `npm run changeset` (required for user-facing changes) - [ ] I have reviewed [contributor guidelines](https://github.com/cline/cline/blob/main/CONTRIBUTING.md) ### Screenshots diff --git a/.github/scripts/overwrite_changeset_changelog.py b/.github/scripts/overwrite_changeset_changelog.py new file mode 100644 index 0000000000..0be482c555 --- /dev/null +++ b/.github/scripts/overwrite_changeset_changelog.py @@ -0,0 +1,62 @@ +""" +This script updates a specific version's release notes section in CHANGELOG.md with new content +or reformats existing content. + +The script: +1. Takes a version number, changelog path, and optionally new content as input from environment variables +2. Finds the section in the changelog for the specified version +3. Either: + a) Replaces the content with new content if provided, or + b) Reformats existing content by: + - Removing the first two lines of the changeset format + - Ensuring version numbers are wrapped in square brackets +4. Writes the updated changelog back to the file + +Environment Variables: + CHANGELOG_PATH: Path to the changelog file (defaults to 'CHANGELOG.md') + VERSION: The version number to update/format + PREV_VERSION: The previous version number (used to locate section boundaries) + NEW_CONTENT: Optional new content to insert for this version +""" + +#!/usr/bin/env python3 + +import os + +CHANGELOG_PATH = os.environ.get("CHANGELOG_PATH", "CHANGELOG.md") +VERSION = os.environ['VERSION'] +PREV_VERSION = os.environ.get("PREV_VERSION", "") +NEW_CONTENT = os.environ.get("NEW_CONTENT", "") + +def overwrite_changelog_section(changelog_text: str, new_content: str): + # Find the section for the specified version + version_pattern = f"## {VERSION}\n" + prev_version_pattern = f"## [{PREV_VERSION}]\n" + print(f"latest version: {VERSION}") + print(f"prev_version: {PREV_VERSION}") + + notes_start_index = changelog_text.find(version_pattern) + len(version_pattern) + notes_end_index = changelog_text.find(prev_version_pattern, notes_start_index) if PREV_VERSION and prev_version_pattern in changelog_text else len(changelog_text) + + if new_content: + return changelog_text[:notes_start_index] + f"{new_content}\n" + changelog_text[notes_end_index:] + else: + changeset_lines = changelog_text[notes_start_index:notes_end_index].split("\n") + # Remove the first two lines from the regular changeset format, ex: \n### Patch Changes + parsed_lines = "\n".join(changeset_lines[2:]) + updated_changelog = changelog_text[:notes_start_index] + parsed_lines + changelog_text[notes_end_index:] + updated_changelog = updated_changelog.replace(f"## {VERSION}", f"## [{VERSION}]") + return updated_changelog + +with open(CHANGELOG_PATH, 'r') as f: + changelog_content = f.read() + +new_changelog = overwrite_changelog_section(changelog_content, NEW_CONTENT) +print("----------------------------------------------------------------------------------") +print(new_changelog) +print("----------------------------------------------------------------------------------") +# Write back to CHANGELOG.md +with open(CHANGELOG_PATH, 'w') as f: + f.write(new_changelog) + +print(f"{CHANGELOG_PATH} updated successfully!") diff --git a/.github/workflows/changeset-release.yml b/.github/workflows/changeset-release.yml new file mode 100644 index 0000000000..0e97893d1d --- /dev/null +++ b/.github/workflows/changeset-release.yml @@ -0,0 +1,158 @@ +name: Changeset Release +run-name: Changeset Release ${{ github.actor != 'cline-bot' && '- Create PR' || '- Update Changelog' }} + +on: + workflow_dispatch: + pull_request: + types: [closed, opened, labeled] + +env: + REPO_PATH: ${{ github.repository }} + GIT_REF: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || 'main' }} + +jobs: + # Job 1: Create version bump PR when changesets are merged to main + changeset-pr-version-bump: + if: > + ( github.event_name == 'pull_request' && + github.event.pull_request.merged == true && + github.event.pull_request.base.ref == 'main' && + github.actor != 'cline-bot' ) || + github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Git Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ env.GIT_REF }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: "npm" + + - name: Install Dependencies + run: npm run install:all + + # Check if there are any new changesets to process + - name: Check for changesets + id: check-changesets + run: | + NEW_CHANGESETS=$(find .changeset -name "*.md" ! -name "README.md" | wc -l | tr -d ' ') + echo "Changesets diff with previous version: $NEW_CHANGESETS" + echo "new_changesets=$NEW_CHANGESETS" >> $GITHUB_OUTPUT + + # Create version bump PR using changesets/action if there are new changesets + - name: Changeset Pull Request + if: steps.check-changesets.outputs.new_changesets != '0' + id: changesets + uses: changesets/action@v1 + with: + commit: "changeset version bump" + title: "Changeset version bump" + version: npm run version-packages # This performs the changeset version bump + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Job 2: Process version bump PR created by cline-bot + changeset-pr-edit-approve: + name: Auto approve and merge Bump version PRs + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + if: > + github.event_name == 'pull_request' && + github.event.pull_request.base.ref == 'main' && + github.actor == 'cline-bot' && + contains(github.event.pull_request.title, 'Changeset version bump') + steps: + - name: Determine checkout ref + id: checkout-ref + run: | + echo "Event action: ${{ github.event.action }}" + echo "Actor: ${{ github.actor }}" + echo "Head ref: ${{ github.head_ref }}" + echo "PR SHA: ${{ github.event.pull_request.head.sha }}" + + if [[ "${{ github.event.action }}" == "opened" && "${{ github.actor }}" == "cline-bot" ]]; then + echo "Using branch ref: ${{ github.head_ref }}" + echo "git_ref=${{ github.head_ref }}" >> $GITHUB_OUTPUT + else + echo "Using SHA ref: ${{ github.event.pull_request.head.sha }}" + echo "git_ref=${{ github.event.pull_request.head.sha }}" >> $GITHUB_OUTPUT + fi + + - name: Checkout Repo + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 + ref: ${{ steps.checkout-ref.outputs.git_ref }} + + # Get current and previous versions to edit changelog entry + - name: Get version + id: get_version + run: | + VERSION=$(git show HEAD:package.json | jq -r '.version') + echo "version=$VERSION" >> $GITHUB_OUTPUT + PREV_VERSION=$(git show origin/main:package.json | jq -r '.version') + echo "prev_version=$PREV_VERSION" >> $GITHUB_OUTPUT + echo "version=$VERSION" + echo "prev_version=$PREV_VERSION" + + # Update CHANGELOG.md with proper format + - name: Update Changelog Format + if: ${{ !contains(github.event.pull_request.labels.*.name, 'changelog-ready') }} + env: + VERSION: ${{ steps.get_version.outputs.version }} + PREV_VERSION: ${{ steps.get_version.outputs.prev_version }} + run: python .github/scripts/overwrite_changeset_changelog.py + + # Commit and push changelog updates + - name: Push Changelog updates + if: ${{ !contains(github.event.pull_request.labels.*.name, 'changelog-ready') }} + run: | + git config user.name "cline-bot" + git config user.email github-actions@github.com + echo "Running git add and commit..." + git add CHANGELOG.md + git commit -m "Updating CHANGELOG.md format" + git status + echo "--------------------------------------------------------------------------------" + echo "Pushing to remote..." + echo "--------------------------------------------------------------------------------" + git push + + # Add label to indicate changelog has been formatted + - name: Add changelog-ready label + if: ${{ !contains(github.event.pull_request.labels.*.name, 'changelog-ready') }} + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + labels: ['changelog-ready'] + }); + + # Auto-approve PR only after it has been labeled + - name: Auto approve PR + if: contains(github.event.pull_request.labels.*.name, 'changelog-ready') + uses: hmarr/auto-approve-action@v4 + with: + review-message: "I'm approving since it's a bump version PR" + + # Auto-merge PR + - name: Automerge on PR + if: false # Needs enablePullRequestAutoMerge in repo settings to work contains(github.event.pull_request.labels.*.name, 'changelog-ready') + run: gh pr merge --auto --merge ${{ github.event.pull_request.number }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/check-changeset.yml b/.github/workflows/check-changeset.yml new file mode 100644 index 0000000000..29c7b20801 --- /dev/null +++ b/.github/workflows/check-changeset.yml @@ -0,0 +1,78 @@ +name: Check Changeset +run-name: Check for Changeset in PR + +on: + pull_request: + branches: + - main + types: [opened, synchronize, reopened, ready_for_review] + +jobs: + check-changeset: + # Skip draft PRs and dependabot PRs + if: github.event.pull_request.draft == false && github.actor != 'dependabot[bot]' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check for changeset + id: check-changeset + run: | + # Get list of changed files + CHANGED_FILES=$(git diff --name-only origin/main...HEAD) + echo "Changed files:" + echo "$CHANGED_FILES" + + # Check if any of the changed files are in docs/ or .github/ + DOCS_ONLY=true + while IFS= read -r file; do + if [[ ! "$file" =~ ^(docs/|.github/) ]]; then + DOCS_ONLY=false + break + fi + done <<< "$CHANGED_FILES" + + # If changes are docs-only, skip changeset check + if [ "$DOCS_ONLY" = true ]; then + echo "Only documentation files were changed, skipping changeset check" + exit 0 + fi + + # Count number of changeset files (excluding README.md) + CHANGESETS=$(find .changeset -name "*.md" ! -name "README.md" | wc -l | tr -d ' ') + echo "Number of changesets: $CHANGESETS" + + if [ "$CHANGESETS" -eq 0 ]; then + echo "::error::No changeset file found. Please run 'npm run changeset' to create one." + exit 1 + fi + + - name: Find Comment + uses: peter-evans/find-comment@v3 + if: failure() + id: find-comment + with: + issue-number: ${{ github.event.pull_request.number }} + comment-author: "github-actions[bot]" + body-includes: This PR requires a changeset + + - name: Create Comment + uses: peter-evans/create-or-update-comment@v4 + if: failure() && steps.find-comment.outputs.comment-id == '' + with: + issue-number: ${{ github.event.pull_request.number }} + body: | + This PR requires a changeset since it includes user-facing changes. Please: + + 1. Run `npm run changeset` locally + 2. Choose the appropriate version bump: + - `major` for breaking changes (1.0.0 → 2.0.0) + - `minor` for new features (1.0.0 → 1.1.0) + - `patch` for bug fixes (1.0.0 → 1.0.1) + 3. Write a clear description of your changes + 4. Commit the generated changeset file + + Note: Documentation-only changes do not require a changeset. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 75edd9ed43..ce24b906bf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -56,20 +56,30 @@ Anyone can contribute code to Cline, but we ask that you follow these guidelines - Update existing tests if your changes affect them - Include both unit tests and integration tests where appropriate -4. **Commit Guidelines** +4. **Version Management with Changesets** + + - Create a changeset for any user-facing changes using `npm run changeset` + - Choose the appropriate version bump: + - `major` for breaking changes (1.0.0 → 2.0.0) + - `minor` for new features (1.0.0 → 1.1.0) + - `patch` for bug fixes (1.0.0 → 1.0.1) + - Write clear, descriptive changeset messages that explain the impact + - Documentation-only changes don't require changesets + +5. **Commit Guidelines** - Write clear, descriptive commit messages - Use conventional commit format (e.g., "feat:", "fix:", "docs:") - Reference relevant issues in commits using #issue-number -5. **Before Submitting** +6. **Before Submitting** - Rebase your branch on the latest main - Ensure your branch builds successfully - Double-check all tests are passing - Review your changes for any debugging code or console logs -6. **Pull Request Description** +7. **Pull Request Description** - Clearly describe what your changes do - Include steps to test the changes - List any breaking changes diff --git a/package-lock.json b/package-lock.json index f859809461..25b4a9e508 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "claude-dev", - "version": "3.2.10", + "version": "3.2.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "claude-dev", - "version": "3.2.10", + "version": "3.2.12", "license": "Apache-2.0", "dependencies": { "@anthropic-ai/bedrock-sdk": "^0.10.2", diff --git a/package.json b/package.json index 03702f04fa..f33033f379 100644 --- a/package.json +++ b/package.json @@ -187,7 +187,8 @@ "publish:marketplace": "vsce publish && ovsx publish", "publish:marketplace:prerelease": "vsce publish --pre-release && ovsx publish --pre-release", "prepare": "husky", - "changeset": "changeset" + "changeset": "changeset", + "version-packages": "changeset version" }, "devDependencies": { "@changesets/cli": "^2.27.12", From 5db9ad2585ab5c7441037e525233c9bb57969bc8 Mon Sep 17 00:00:00 2001 From: Ocasta Date: Tue, 4 Feb 2025 19:06:34 -0800 Subject: [PATCH 16/33] revert to older config --- .changeset/changelog-config.js | 20 -------------------- .changeset/config.json | 4 ++-- 2 files changed, 2 insertions(+), 22 deletions(-) delete mode 100644 .changeset/changelog-config.js diff --git a/.changeset/changelog-config.js b/.changeset/changelog-config.js deleted file mode 100644 index 1e64dbf093..0000000000 --- a/.changeset/changelog-config.js +++ /dev/null @@ -1,20 +0,0 @@ -// Half-works to simplify the format but needs 'overwrite_changeset_changelog.py' in GHA to finish formatting - -const getReleaseLine = async (changeset) => { - const [firstLine] = changeset.summary - .split("\n") - .map((l) => l.trim()) - .filter(Boolean) - return `- ${firstLine}` -} - -const getDependencyReleaseLine = async () => { - return "" -} - -const changelogFunctions = { - getReleaseLine, - getDependencyReleaseLine, -} - -module.exports = changelogFunctions diff --git a/.changeset/config.json b/.changeset/config.json index bcd6eefa00..42efc1c834 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -1,6 +1,6 @@ { - "$schema": "https://unpkg.com/@changesets/config@3.0.4/schema.json", - "changelog": "./changelog-config.js", + "$schema": "https://unpkg.com/@changesets/config@3.0.5/schema.json", + "changelog": "@changesets/cli/changelog", "commit": false, "fixed": [], "linked": [], From 42c6dc7e94ba21d8854997253e10cf797ff1d20e Mon Sep 17 00:00:00 2001 From: Ocasta Date: Tue, 4 Feb 2025 19:20:18 -0800 Subject: [PATCH 17/33] update based on automated feedback --- .github/scripts/overwrite_changeset_changelog.py | 15 ++++++++++++--- .github/workflows/changeset-release.yml | 16 ++++++++++------ .github/workflows/check-changeset.yml | 8 ++++++-- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/.github/scripts/overwrite_changeset_changelog.py b/.github/scripts/overwrite_changeset_changelog.py index 0be482c555..eb3361bf2f 100644 --- a/.github/scripts/overwrite_changeset_changelog.py +++ b/.github/scripts/overwrite_changeset_changelog.py @@ -35,15 +35,24 @@ def overwrite_changelog_section(changelog_text: str, new_content: str): print(f"latest version: {VERSION}") print(f"prev_version: {PREV_VERSION}") - notes_start_index = changelog_text.find(version_pattern) + len(version_pattern) + version_index = changelog_text.find(version_pattern) + if version_index == -1: + raise ValueError(f"Could not find version {VERSION} in changelog") + + notes_start_index = version_index + len(version_pattern) notes_end_index = changelog_text.find(prev_version_pattern, notes_start_index) if PREV_VERSION and prev_version_pattern in changelog_text else len(changelog_text) if new_content: return changelog_text[:notes_start_index] + f"{new_content}\n" + changelog_text[notes_end_index:] else: changeset_lines = changelog_text[notes_start_index:notes_end_index].split("\n") - # Remove the first two lines from the regular changeset format, ex: \n### Patch Changes - parsed_lines = "\n".join(changeset_lines[2:]) + # Ensure we have at least 2 lines before removing them + if len(changeset_lines) < 2: + print("Warning: Changeset content has fewer than 2 lines") + parsed_lines = "\n".join(changeset_lines) + else: + # Remove the first two lines from the regular changeset format, ex: \n### Patch Changes + parsed_lines = "\n".join(changeset_lines[2:]) updated_changelog = changelog_text[:notes_start_index] + parsed_lines + changelog_text[notes_end_index:] updated_changelog = updated_changelog.replace(f"## {VERSION}", f"## [{VERSION}]") return updated_changelog diff --git a/.github/workflows/changeset-release.yml b/.github/workflows/changeset-release.yml index 0e97893d1d..332b98a66d 100644 --- a/.github/workflows/changeset-release.yml +++ b/.github/workflows/changeset-release.yml @@ -1,6 +1,10 @@ name: Changeset Release run-name: Changeset Release ${{ github.actor != 'cline-bot' && '- Create PR' || '- Update Changelog' }} +permissions: + contents: write + pull-requests: write + on: workflow_dispatch: pull_request: @@ -25,13 +29,13 @@ jobs: pull-requests: write steps: - name: Git Checkout - uses: actions/checkout@v4 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 with: fetch-depth: 0 ref: ${{ env.GIT_REF }} - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4 with: node-version: 20 cache: "npm" @@ -51,7 +55,7 @@ jobs: - name: Changeset Pull Request if: steps.check-changesets.outputs.new_changesets != '0' id: changesets - uses: changesets/action@v1 + uses: changesets/action@e9cc34b540dd3ad1b030c57fd97269e8f6ad905a # v1 with: commit: "changeset version bump" title: "Changeset version bump" @@ -89,7 +93,7 @@ jobs: fi - name: Checkout Repo - uses: actions/checkout@v4 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 with: token: ${{ secrets.GITHUB_TOKEN }} fetch-depth: 0 @@ -132,7 +136,7 @@ jobs: # Add label to indicate changelog has been formatted - name: Add changelog-ready label if: ${{ !contains(github.event.pull_request.labels.*.name, 'changelog-ready') }} - uses: actions/github-script@v7 + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | @@ -146,7 +150,7 @@ jobs: # Auto-approve PR only after it has been labeled - name: Auto approve PR if: contains(github.event.pull_request.labels.*.name, 'changelog-ready') - uses: hmarr/auto-approve-action@v4 + uses: hmarr/auto-approve-action@de8bf34d0402c38aa2c8346973342b2cb02c4435 # v4 with: review-message: "I'm approving since it's a bump version PR" diff --git a/.github/workflows/check-changeset.yml b/.github/workflows/check-changeset.yml index 29c7b20801..9d8f7a3469 100644 --- a/.github/workflows/check-changeset.yml +++ b/.github/workflows/check-changeset.yml @@ -1,6 +1,10 @@ name: Check Changeset run-name: Check for Changeset in PR +permissions: + contents: read + pull-requests: write + on: pull_request: branches: @@ -51,7 +55,7 @@ jobs: fi - name: Find Comment - uses: peter-evans/find-comment@v3 + uses: peter-evans/find-comment@45803def666fc704971eff4c7d57d650f81ae24a # v3 if: failure() id: find-comment with: @@ -60,7 +64,7 @@ jobs: body-includes: This PR requires a changeset - name: Create Comment - uses: peter-evans/create-or-update-comment@v4 + uses: peter-evans/create-or-update-comment@23ff15e22924c50649c1d63cc73f02f16fc0a8e8 # v4 if: failure() && steps.find-comment.outputs.comment-id == '' with: issue-number: ${{ github.event.pull_request.number }} From 2cc0fa906c64329a0721295ee27e1596f13d7a10 Mon Sep 17 00:00:00 2001 From: Michael Overhorst Date: Wed, 5 Feb 2025 06:41:00 +0100 Subject: [PATCH 18/33] Updated the mistral url. --- src/api/providers/mistral.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/providers/mistral.ts b/src/api/providers/mistral.ts index c4377f0003..408863ed6f 100644 --- a/src/api/providers/mistral.ts +++ b/src/api/providers/mistral.ts @@ -21,7 +21,7 @@ export class MistralHandler implements ApiHandler { constructor(options: ApiHandlerOptions) { this.options = options this.client = new Mistral({ - serverURL: "https://codestral.mistral.ai", + serverURL: "https://api.mistral.ai/v1", apiKey: this.options.mistralApiKey, }) } From 15c147cf93c2e0d1cb9940756d8328150d68d289 Mon Sep 17 00:00:00 2001 From: Ocasta Date: Tue, 4 Feb 2025 21:45:15 -0800 Subject: [PATCH 19/33] fix check changeset git action --- .github/workflows/check-changeset.yml | 91 +++++++++++++++++++-------- 1 file changed, 64 insertions(+), 27 deletions(-) diff --git a/.github/workflows/check-changeset.yml b/.github/workflows/check-changeset.yml index 9d8f7a3469..48ffc1ca41 100644 --- a/.github/workflows/check-changeset.yml +++ b/.github/workflows/check-changeset.yml @@ -18,18 +18,31 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 with: fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} - name: Check for changeset id: check-changeset run: | + # Debug info + echo "Current directory: $(pwd)" + echo "PR Base Ref: ${{ github.event.pull_request.base.ref }}" + echo "PR Head Ref: ${{ github.event.pull_request.head.ref }}" + echo "PR Head SHA: ${{ github.event.pull_request.head.sha }}" + echo "Git status:" + git status + # Get list of changed files - CHANGED_FILES=$(git diff --name-only origin/main...HEAD) + git fetch origin ${{ github.event.pull_request.base.ref }} + CHANGED_FILES=$(git diff --name-only origin/${{ github.event.pull_request.base.ref }} HEAD) echo "Changed files:" echo "$CHANGED_FILES" + echo "Listing .changeset directory:" + ls -la .changeset/ + # Check if any of the changed files are in docs/ or .github/ DOCS_ONLY=true while IFS= read -r file; do @@ -45,38 +58,62 @@ jobs: exit 0 fi - # Count number of changeset files (excluding README.md) - CHANGESETS=$(find .changeset -name "*.md" ! -name "README.md" | wc -l | tr -d ' ') - echo "Number of changesets: $CHANGESETS" + # Check if any changeset files are in the changed files + CHANGESET_IN_PR=false + while IFS= read -r file; do + if [[ "$file" =~ ^\.changeset/.*\.md$ && "$file" != ".changeset/README.md" ]]; then + echo "Found changeset file in PR: $file" + CHANGESET_IN_PR=true + break + fi + done <<< "$CHANGED_FILES" - if [ "$CHANGESETS" -eq 0 ]; then - echo "::error::No changeset file found. Please run 'npm run changeset' to create one." - exit 1 + if [ "$CHANGESET_IN_PR" = false ]; then + # Double check local changeset files as backup + CHANGESETS=$(find .changeset -name "*.md" ! -name "README.md" | wc -l | tr -d ' ') + echo "Number of local changesets: $CHANGESETS" + + if [ "$CHANGESETS" -eq 0 ]; then + echo "::error::No changeset file found in PR changes or local directory. Please run 'npm run changeset' to create one." + exit 1 + fi fi - - name: Find Comment - uses: peter-evans/find-comment@45803def666fc704971eff4c7d57d650f81ae24a # v3 + - name: Comment on PR if: failure() - id: find-comment + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 with: - issue-number: ${{ github.event.pull_request.number }} - comment-author: "github-actions[bot]" - body-includes: This PR requires a changeset + script: | + const message = `This PR requires a changeset since it includes user-facing changes. Please: - - name: Create Comment - uses: peter-evans/create-or-update-comment@23ff15e22924c50649c1d63cc73f02f16fc0a8e8 # v4 - if: failure() && steps.find-comment.outputs.comment-id == '' - with: - issue-number: ${{ github.event.pull_request.number }} - body: | - This PR requires a changeset since it includes user-facing changes. Please: - - 1. Run `npm run changeset` locally + 1. Run \`npm run changeset\` locally 2. Choose the appropriate version bump: - - `major` for breaking changes (1.0.0 → 2.0.0) - - `minor` for new features (1.0.0 → 1.1.0) - - `patch` for bug fixes (1.0.0 → 1.0.1) + - \`major\` for breaking changes (1.0.0 → 2.0.0) + - \`minor\` for new features (1.0.0 → 1.1.0) + - \`patch\` for bug fixes (1.0.0 → 1.0.1) 3. Write a clear description of your changes 4. Commit the generated changeset file - Note: Documentation-only changes do not require a changeset. + Note: Documentation-only changes do not require a changeset.`; + + // Get existing comments + const comments = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number + }); + + // Check if we already commented + const botComment = comments.data.find(comment => + comment.user.login === 'github-actions[bot]' && + comment.body.includes('This PR requires a changeset') + ); + + if (!botComment) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: message + }); + } From 9fbd8318f0a161ad022d0af8dfd9906d001010b2 Mon Sep 17 00:00:00 2001 From: Ocasta Date: Tue, 4 Feb 2025 21:49:41 -0800 Subject: [PATCH 20/33] error handling when notes_start_index is invalid --- .../scripts/overwrite_changeset_changelog.py | 53 +++++++++++++++---- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/.github/scripts/overwrite_changeset_changelog.py b/.github/scripts/overwrite_changeset_changelog.py index eb3361bf2f..36fde943b4 100644 --- a/.github/scripts/overwrite_changeset_changelog.py +++ b/.github/scripts/overwrite_changeset_changelog.py @@ -31,13 +31,25 @@ NEW_CONTENT = os.environ.get("NEW_CONTENT", "") def overwrite_changelog_section(changelog_text: str, new_content: str): # Find the section for the specified version version_pattern = f"## {VERSION}\n" + bracketed_version_pattern = f"## [{VERSION}]\n" prev_version_pattern = f"## [{PREV_VERSION}]\n" print(f"latest version: {VERSION}") print(f"prev_version: {PREV_VERSION}") + # Try both unbracketed and bracketed version patterns version_index = changelog_text.find(version_pattern) if version_index == -1: - raise ValueError(f"Could not find version {VERSION} in changelog") + version_index = changelog_text.find(bracketed_version_pattern) + if version_index == -1: + # If version not found, add it at the top (after the first line) + first_newline = changelog_text.find('\n') + if first_newline == -1: + # If no newline found, just prepend + return f"## [{VERSION}]\n\n{changelog_text}" + return f"{changelog_text[:first_newline + 1]}## [{VERSION}]\n\n{changelog_text[first_newline + 1:]}" + else: + # Using bracketed version + version_pattern = bracketed_version_pattern notes_start_index = version_index + len(version_pattern) notes_end_index = changelog_text.find(prev_version_pattern, notes_start_index) if PREV_VERSION and prev_version_pattern in changelog_text else len(changelog_text) @@ -54,18 +66,37 @@ def overwrite_changelog_section(changelog_text: str, new_content: str): # Remove the first two lines from the regular changeset format, ex: \n### Patch Changes parsed_lines = "\n".join(changeset_lines[2:]) updated_changelog = changelog_text[:notes_start_index] + parsed_lines + changelog_text[notes_end_index:] + # Ensure version number is bracketed updated_changelog = updated_changelog.replace(f"## {VERSION}", f"## [{VERSION}]") return updated_changelog -with open(CHANGELOG_PATH, 'r') as f: - changelog_content = f.read() +try: + print(f"Reading changelog from: {CHANGELOG_PATH}") + with open(CHANGELOG_PATH, 'r') as f: + changelog_content = f.read() -new_changelog = overwrite_changelog_section(changelog_content, NEW_CONTENT) -print("----------------------------------------------------------------------------------") -print(new_changelog) -print("----------------------------------------------------------------------------------") -# Write back to CHANGELOG.md -with open(CHANGELOG_PATH, 'w') as f: - f.write(new_changelog) + print(f"Changelog content length: {len(changelog_content)} characters") + print("First 200 characters of changelog:") + print(changelog_content[:200]) + print("----------------------------------------------------------------------------------") -print(f"{CHANGELOG_PATH} updated successfully!") + new_changelog = overwrite_changelog_section(changelog_content, NEW_CONTENT) + + print("New changelog content:") + print("----------------------------------------------------------------------------------") + print(new_changelog) + print("----------------------------------------------------------------------------------") + + print(f"Writing updated changelog back to: {CHANGELOG_PATH}") + with open(CHANGELOG_PATH, 'w') as f: + f.write(new_changelog) + + print(f"{CHANGELOG_PATH} updated successfully!") + +except FileNotFoundError: + print(f"Error: Changelog file not found at {CHANGELOG_PATH}") + exit(1) +except Exception as e: + print(f"Error updating changelog: {str(e)}") + print(f"Current working directory: {os.getcwd()}") + exit(1) From fd676ef04740d5e500663c5b9b92675b10a81b79 Mon Sep 17 00:00:00 2001 From: Michael Overhorst Date: Wed, 5 Feb 2025 06:51:02 +0100 Subject: [PATCH 21/33] Removed /v1 from the url and tested several models. --- src/api/providers/mistral.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/providers/mistral.ts b/src/api/providers/mistral.ts index 408863ed6f..5be89b18f3 100644 --- a/src/api/providers/mistral.ts +++ b/src/api/providers/mistral.ts @@ -21,7 +21,7 @@ export class MistralHandler implements ApiHandler { constructor(options: ApiHandlerOptions) { this.options = options this.client = new Mistral({ - serverURL: "https://api.mistral.ai/v1", + serverURL: "https://api.mistral.ai", apiKey: this.options.mistralApiKey, }) } From 1415c8ce7b55368eb6353a95a6b5955dbce9686a Mon Sep 17 00:00:00 2001 From: Ocasta Date: Tue, 4 Feb 2025 21:56:53 -0800 Subject: [PATCH 22/33] use sys.exit --- .github/scripts/overwrite_changeset_changelog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/scripts/overwrite_changeset_changelog.py b/.github/scripts/overwrite_changeset_changelog.py index 36fde943b4..56fea2ad37 100644 --- a/.github/scripts/overwrite_changeset_changelog.py +++ b/.github/scripts/overwrite_changeset_changelog.py @@ -22,6 +22,7 @@ Environment Variables: #!/usr/bin/env python3 import os +import sys CHANGELOG_PATH = os.environ.get("CHANGELOG_PATH", "CHANGELOG.md") VERSION = os.environ['VERSION'] @@ -95,7 +96,7 @@ try: except FileNotFoundError: print(f"Error: Changelog file not found at {CHANGELOG_PATH}") - exit(1) + sys.exit(1) except Exception as e: print(f"Error updating changelog: {str(e)}") print(f"Current working directory: {os.getcwd()}") From 6937823b690e0399ad4f8ae15164fc255dcf8172 Mon Sep 17 00:00:00 2001 From: Ocasta Date: Tue, 4 Feb 2025 22:05:03 -0800 Subject: [PATCH 23/33] use sys.exit, again --- .github/scripts/overwrite_changeset_changelog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/overwrite_changeset_changelog.py b/.github/scripts/overwrite_changeset_changelog.py index 56fea2ad37..67fdb6a647 100644 --- a/.github/scripts/overwrite_changeset_changelog.py +++ b/.github/scripts/overwrite_changeset_changelog.py @@ -100,4 +100,4 @@ except FileNotFoundError: except Exception as e: print(f"Error updating changelog: {str(e)}") print(f"Current working directory: {os.getcwd()}") - exit(1) + sys.exit(1) From 4d311ffb43517c7ab8c8d3d2f2c218461fd4cb8d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Feb 2025 06:18:51 +0000 Subject: [PATCH 24/33] Bump vitest from 2.1.8 to 2.1.9 in /webview-ui in the npm_and_yarn group Bumps the npm_and_yarn group in /webview-ui with 1 update: [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest). Updates `vitest` from 2.1.8 to 2.1.9 - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Commits](https://github.com/vitest-dev/vitest/commits/v2.1.9/packages/vitest) --- updated-dependencies: - dependency-name: vitest dependency-type: direct:development dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] --- webview-ui/package-lock.json | 246 +++++++++++++++++------------------ webview-ui/package.json | 2 +- 2 files changed, 124 insertions(+), 124 deletions(-) diff --git a/webview-ui/package-lock.json b/webview-ui/package-lock.json index faec4c48fa..5ac343c886 100644 --- a/webview-ui/package-lock.json +++ b/webview-ui/package-lock.json @@ -36,7 +36,7 @@ "@types/react-dom": "^18.3.0", "@types/vscode-webview": "^1.57.5", "jsdom": "^25.0.1", - "vitest": "^2.1.8" + "vitest": "^2.1.9" } }, "node_modules/@adobe/css-tools": { @@ -3860,9 +3860,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.32.1.tgz", - "integrity": "sha512-/pqA4DmqyCm8u5YIDzIdlLcEmuvxb0v8fZdFhVMszSpDTgbQKdw3/mB3eMUHIbubtJ6F9j+LtmyCnHTEqIHyzA==", + "version": "4.34.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.2.tgz", + "integrity": "sha512-6Fyg9yQbwJR+ykVdT9sid1oc2ewejS6h4wzQltmJfSW53N60G/ah9pngXGANdy9/aaE/TcUFpWosdm7JXS1WTQ==", "cpu": [ "arm" ], @@ -3874,9 +3874,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.32.1.tgz", - "integrity": "sha512-If3PDskT77q7zgqVqYuj7WG3WC08G1kwXGVFi9Jr8nY6eHucREHkfpX79c0ACAjLj3QIWKPJR7w4i+f5EdLH5Q==", + "version": "4.34.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.2.tgz", + "integrity": "sha512-K5GfWe+vtQ3kyEbihrimM38UgX57UqHp+oME7X/EX9Im6suwZfa7Hsr8AtzbJvukTpwMGs+4s29YMSO3rwWtsw==", "cpu": [ "arm64" ], @@ -3888,9 +3888,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.32.1.tgz", - "integrity": "sha512-zCpKHioQ9KgZToFp5Wvz6zaWbMzYQ2LJHQ+QixDKq52KKrF65ueu6Af4hLlLWHjX1Wf/0G5kSJM9PySW9IrvHA==", + "version": "4.34.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.2.tgz", + "integrity": "sha512-PSN58XG/V/tzqDb9kDGutUruycgylMlUE59f40ny6QIRNsTEIZsrNQTJKUN2keMMSmlzgunMFqyaGLmly39sug==", "cpu": [ "arm64" ], @@ -3902,9 +3902,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.32.1.tgz", - "integrity": "sha512-sFvF+t2+TyUo/ZQqUcifrJIgznx58oFZbdHS9TvHq3xhPVL9nOp+yZ6LKrO9GWTP+6DbFtoyLDbjTpR62Mbr3Q==", + "version": "4.34.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.2.tgz", + "integrity": "sha512-gQhK788rQJm9pzmXyfBB84VHViDERhAhzGafw+E5mUpnGKuxZGkMVDa3wgDFKT6ukLC5V7QTifzsUKdNVxp5qQ==", "cpu": [ "x64" ], @@ -3916,9 +3916,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.32.1.tgz", - "integrity": "sha512-NbOa+7InvMWRcY9RG+B6kKIMD/FsnQPH0MWUvDlQB1iXnF/UcKSudCXZtv4lW+C276g3w5AxPbfry5rSYvyeYA==", + "version": "4.34.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.2.tgz", + "integrity": "sha512-eiaHgQwGPpxLC3+zTAcdKl4VsBl3r0AiJOd1Um/ArEzAjN/dbPK1nROHrVkdnoE6p7Svvn04w3f/jEZSTVHunA==", "cpu": [ "arm64" ], @@ -3930,9 +3930,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.32.1.tgz", - "integrity": "sha512-JRBRmwvHPXR881j2xjry8HZ86wIPK2CcDw0EXchE1UgU0ubWp9nvlT7cZYKc6bkypBt745b4bglf3+xJ7hXWWw==", + "version": "4.34.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.2.tgz", + "integrity": "sha512-lhdiwQ+jf8pewYOTG4bag0Qd68Jn1v2gO1i0mTuiD+Qkt5vNfHVK/jrT7uVvycV8ZchlzXp5HDVmhpzjC6mh0g==", "cpu": [ "x64" ], @@ -3944,9 +3944,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.32.1.tgz", - "integrity": "sha512-PKvszb+9o/vVdUzCCjL0sKHukEQV39tD3fepXxYrHE3sTKrRdCydI7uldRLbjLmDA3TFDmh418XH19NOsDRH8g==", + "version": "4.34.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.2.tgz", + "integrity": "sha512-lfqTpWjSvbgQP1vqGTXdv+/kxIznKXZlI109WkIFPbud41bjigjNmOAAKoazmRGx+k9e3rtIdbq2pQZPV1pMig==", "cpu": [ "arm" ], @@ -3958,9 +3958,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.32.1.tgz", - "integrity": "sha512-9WHEMV6Y89eL606ReYowXuGF1Yb2vwfKWKdD1A5h+OYnPZSJvxbEjxTRKPgi7tkP2DSnW0YLab1ooy+i/FQp/Q==", + "version": "4.34.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.2.tgz", + "integrity": "sha512-RGjqULqIurqqv+NJTyuPgdZhka8ImMLB32YwUle2BPTDqDoXNgwFjdjQC59FbSk08z0IqlRJjrJ0AvDQ5W5lpw==", "cpu": [ "arm" ], @@ -3972,9 +3972,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.32.1.tgz", - "integrity": "sha512-tZWc9iEt5fGJ1CL2LRPw8OttkCBDs+D8D3oEM8mH8S1ICZCtFJhD7DZ3XMGM8kpqHvhGUTvNUYVDnmkj4BDXnw==", + "version": "4.34.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.2.tgz", + "integrity": "sha512-ZvkPiheyXtXlFqHpsdgscx+tZ7hoR59vOettvArinEspq5fxSDSgfF+L5wqqJ9R4t+n53nyn0sKxeXlik7AY9Q==", "cpu": [ "arm64" ], @@ -3986,9 +3986,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.32.1.tgz", - "integrity": "sha512-FTYc2YoTWUsBz5GTTgGkRYYJ5NGJIi/rCY4oK/I8aKowx1ToXeoVVbIE4LGAjsauvlhjfl0MYacxClLld1VrOw==", + "version": "4.34.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.2.tgz", + "integrity": "sha512-UlFk+E46TZEoxD9ufLKDBzfSG7Ki03fo6hsNRRRHF+KuvNZ5vd1RRVQm8YZlGsjcJG8R252XFK0xNPay+4WV7w==", "cpu": [ "arm64" ], @@ -4000,9 +4000,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.32.1.tgz", - "integrity": "sha512-F51qLdOtpS6P1zJVRzYM0v6MrBNypyPEN1GfMiz0gPu9jN8ScGaEFIZQwteSsGKg799oR5EaP7+B2jHgL+d+Kw==", + "version": "4.34.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.2.tgz", + "integrity": "sha512-hJhfsD9ykx59jZuuoQgYT1GEcNNi3RCoEmbo5OGfG8RlHOiVS7iVNev9rhLKh7UBYq409f4uEw0cclTXx8nh8Q==", "cpu": [ "loong64" ], @@ -4014,9 +4014,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.32.1.tgz", - "integrity": "sha512-wO0WkfSppfX4YFm5KhdCCpnpGbtgQNj/tgvYzrVYFKDpven8w2N6Gg5nB6w+wAMO3AIfSTWeTjfVe+uZ23zAlg==", + "version": "4.34.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.2.tgz", + "integrity": "sha512-g/O5IpgtrQqPegvqopvmdCF9vneLE7eqYfdPWW8yjPS8f63DNam3U4ARL1PNNB64XHZDHKpvO2Giftf43puB8Q==", "cpu": [ "ppc64" ], @@ -4028,9 +4028,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.32.1.tgz", - "integrity": "sha512-iWswS9cIXfJO1MFYtI/4jjlrGb/V58oMu4dYJIKnR5UIwbkzR0PJ09O0PDZT0oJ3LYWXBSWahNf/Mjo6i1E5/g==", + "version": "4.34.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.2.tgz", + "integrity": "sha512-bSQijDC96M6PuooOuXHpvXUYiIwsnDmqGU8+br2U7iPoykNi9JtMUpN7K6xml29e0evK0/g0D1qbAUzWZFHY5Q==", "cpu": [ "riscv64" ], @@ -4042,9 +4042,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.32.1.tgz", - "integrity": "sha512-RKt8NI9tebzmEthMnfVgG3i/XeECkMPS+ibVZjZ6mNekpbbUmkNWuIN2yHsb/mBPyZke4nlI4YqIdFPgKuoyQQ==", + "version": "4.34.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.2.tgz", + "integrity": "sha512-49TtdeVAsdRuiUHXPrFVucaP4SivazetGUVH8CIxVsNsaPHV4PFkpLmH9LeqU/R4Nbgky9lzX5Xe1NrzLyraVA==", "cpu": [ "s390x" ], @@ -4056,9 +4056,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.32.1.tgz", - "integrity": "sha512-WQFLZ9c42ECqEjwg/GHHsouij3pzLXkFdz0UxHa/0OM12LzvX7DzedlY0SIEly2v18YZLRhCRoHZDxbBSWoGYg==", + "version": "4.34.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.2.tgz", + "integrity": "sha512-j+jFdfOycLIQ7FWKka9Zd3qvsIyugg5LeZuHF6kFlXo6MSOc6R1w37YUVy8VpAKd81LMWGi5g9J25P09M0SSIw==", "cpu": [ "x64" ], @@ -4070,9 +4070,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.32.1.tgz", - "integrity": "sha512-BLoiyHDOWoS3uccNSADMza6V6vCNiphi94tQlVIL5de+r6r/CCQuNnerf+1g2mnk2b6edp5dk0nhdZ7aEjOBsA==", + "version": "4.34.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.2.tgz", + "integrity": "sha512-aDPHyM/D2SpXfSNCVWCxyHmOqN9qb7SWkY1+vaXqMNMXslZYnwh9V/UCudl6psyG0v6Ukj7pXanIpfZwCOEMUg==", "cpu": [ "x64" ], @@ -4084,9 +4084,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.32.1.tgz", - "integrity": "sha512-w2l3UnlgYTNNU+Z6wOR8YdaioqfEnwPjIsJ66KxKAf0p+AuL2FHeTX6qvM+p/Ue3XPBVNyVSfCrfZiQh7vZHLQ==", + "version": "4.34.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.2.tgz", + "integrity": "sha512-LQRkCyUBnAo7r8dbEdtNU08EKLCJMgAk2oP5H3R7BnUlKLqgR3dUjrLBVirmc1RK6U6qhtDw29Dimeer8d5hzQ==", "cpu": [ "arm64" ], @@ -4098,9 +4098,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.32.1.tgz", - "integrity": "sha512-Am9H+TGLomPGkBnaPWie4F3x+yQ2rr4Bk2jpwy+iV+Gel9jLAu/KqT8k3X4jxFPW6Zf8OMnehyutsd+eHoq1WQ==", + "version": "4.34.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.2.tgz", + "integrity": "sha512-wt8OhpQUi6JuPFkm1wbVi1BByeag87LDFzeKSXzIdGcX4bMLqORTtKxLoCbV57BHYNSUSOKlSL4BYYUghainYA==", "cpu": [ "ia32" ], @@ -4112,9 +4112,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.32.1.tgz", - "integrity": "sha512-ar80GhdZb4DgmW3myIS9nRFYcpJRSME8iqWgzH2i44u+IdrzmiXVxeFnExQ5v4JYUSpg94bWjevMG8JHf1Da5Q==", + "version": "4.34.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.2.tgz", + "integrity": "sha512-rUrqINax0TvrPBXrFKg0YbQx18NpPN3NNrgmaao9xRNbTwek7lOXObhx8tQy8gelmQ/gLaGy1WptpU2eKJZImg==", "cpu": [ "x64" ], @@ -5209,14 +5209,14 @@ "license": "ISC" }, "node_modules/@vitest/expect": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.8.tgz", - "integrity": "sha512-8ytZ/fFHq2g4PJVAtDX57mayemKgDR6X3Oa2Foro+EygiOJHUXhCqBAAKQYYajZpFoIfvBCF1j6R6IYRSIUFuw==", + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.8", - "@vitest/utils": "2.1.8", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", "chai": "^5.1.2", "tinyrainbow": "^1.2.0" }, @@ -5225,13 +5225,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.8.tgz", - "integrity": "sha512-7guJ/47I6uqfttp33mgo6ga5Gr1VnL58rcqYKyShoRK9ebu8T5Rs6HN3s1NABiBeVTdWNrwUMcHH54uXZBN4zA==", + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.8", + "@vitest/spy": "2.1.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.12" }, @@ -5272,9 +5272,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.8.tgz", - "integrity": "sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==", + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5285,13 +5285,13 @@ } }, "node_modules/@vitest/runner": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.8.tgz", - "integrity": "sha512-17ub8vQstRnRlIU5k50bG+QOMLHRhYPAna5tw8tYbj+jzjcspnwnwtPtiOlkuKC4+ixDPTuLZiqiWWQ2PSXHVg==", + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "2.1.8", + "@vitest/utils": "2.1.9", "pathe": "^1.1.2" }, "funding": { @@ -5299,13 +5299,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.8.tgz", - "integrity": "sha512-20T7xRFbmnkfcmgVEz+z3AU/3b0cEzZOt/zmnvZEctg64/QZbSDJEVm9fLnnlSi74KibmRsO9/Qabi+t0vCRPg==", + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.8", + "@vitest/pretty-format": "2.1.9", "magic-string": "^0.30.12", "pathe": "^1.1.2" }, @@ -5324,9 +5324,9 @@ } }, "node_modules/@vitest/spy": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.8.tgz", - "integrity": "sha512-5swjf2q95gXeYPevtW0BLk6H8+bPlMb4Vw/9Em4hFxDcaOxS+e0LOX4yqNxoHzMR2akEB2xfpnWUzkZokmgWDg==", + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5337,13 +5337,13 @@ } }, "node_modules/@vitest/utils": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.8.tgz", - "integrity": "sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==", + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.8", + "@vitest/pretty-format": "2.1.9", "loupe": "^3.1.2", "tinyrainbow": "^1.2.0" }, @@ -19405,9 +19405,9 @@ } }, "node_modules/vite-node": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.8.tgz", - "integrity": "sha512-uPAwSr57kYjAUux+8E2j0q0Fxpn8M9VoyfGiRI8Kfktz9NcYMCenwY5RnZxnF1WTu3TGiYipirIzacLL3VVGFg==", + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", "dev": true, "license": "MIT", "dependencies": { @@ -19457,9 +19457,9 @@ } }, "node_modules/vite/node_modules/rollup": { - "version": "4.32.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.32.1.tgz", - "integrity": "sha512-z+aeEsOeEa3mEbS1Tjl6sAZ8NE3+AalQz1RJGj81M+fizusbdDMoEJwdJNHfaB40Scr4qNu+welOfes7maKonA==", + "version": "4.34.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.2.tgz", + "integrity": "sha512-sBDUoxZEaqLu9QeNalL8v3jw6WjPku4wfZGyTU7l7m1oC+rpRihXc/n/H+4148ZkGz5Xli8CHMns//fFGKvpIQ==", "dev": true, "license": "MIT", "dependencies": { @@ -19473,42 +19473,42 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.32.1", - "@rollup/rollup-android-arm64": "4.32.1", - "@rollup/rollup-darwin-arm64": "4.32.1", - "@rollup/rollup-darwin-x64": "4.32.1", - "@rollup/rollup-freebsd-arm64": "4.32.1", - "@rollup/rollup-freebsd-x64": "4.32.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.32.1", - "@rollup/rollup-linux-arm-musleabihf": "4.32.1", - "@rollup/rollup-linux-arm64-gnu": "4.32.1", - "@rollup/rollup-linux-arm64-musl": "4.32.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.32.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.32.1", - "@rollup/rollup-linux-riscv64-gnu": "4.32.1", - "@rollup/rollup-linux-s390x-gnu": "4.32.1", - "@rollup/rollup-linux-x64-gnu": "4.32.1", - "@rollup/rollup-linux-x64-musl": "4.32.1", - "@rollup/rollup-win32-arm64-msvc": "4.32.1", - "@rollup/rollup-win32-ia32-msvc": "4.32.1", - "@rollup/rollup-win32-x64-msvc": "4.32.1", + "@rollup/rollup-android-arm-eabi": "4.34.2", + "@rollup/rollup-android-arm64": "4.34.2", + "@rollup/rollup-darwin-arm64": "4.34.2", + "@rollup/rollup-darwin-x64": "4.34.2", + "@rollup/rollup-freebsd-arm64": "4.34.2", + "@rollup/rollup-freebsd-x64": "4.34.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.34.2", + "@rollup/rollup-linux-arm-musleabihf": "4.34.2", + "@rollup/rollup-linux-arm64-gnu": "4.34.2", + "@rollup/rollup-linux-arm64-musl": "4.34.2", + "@rollup/rollup-linux-loongarch64-gnu": "4.34.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.34.2", + "@rollup/rollup-linux-riscv64-gnu": "4.34.2", + "@rollup/rollup-linux-s390x-gnu": "4.34.2", + "@rollup/rollup-linux-x64-gnu": "4.34.2", + "@rollup/rollup-linux-x64-musl": "4.34.2", + "@rollup/rollup-win32-arm64-msvc": "4.34.2", + "@rollup/rollup-win32-ia32-msvc": "4.34.2", + "@rollup/rollup-win32-x64-msvc": "4.34.2", "fsevents": "~2.3.2" } }, "node_modules/vitest": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.8.tgz", - "integrity": "sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ==", + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "2.1.8", - "@vitest/mocker": "2.1.8", - "@vitest/pretty-format": "^2.1.8", - "@vitest/runner": "2.1.8", - "@vitest/snapshot": "2.1.8", - "@vitest/spy": "2.1.8", - "@vitest/utils": "2.1.8", + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", "chai": "^5.1.2", "debug": "^4.3.7", "expect-type": "^1.1.0", @@ -19520,7 +19520,7 @@ "tinypool": "^1.0.1", "tinyrainbow": "^1.2.0", "vite": "^5.0.0", - "vite-node": "2.1.8", + "vite-node": "2.1.9", "why-is-node-running": "^2.3.0" }, "bin": { @@ -19535,8 +19535,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.1.8", - "@vitest/ui": "2.1.8", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", "happy-dom": "*", "jsdom": "*" }, diff --git a/webview-ui/package.json b/webview-ui/package.json index d955fba53c..8d89ea490f 100644 --- a/webview-ui/package.json +++ b/webview-ui/package.json @@ -59,6 +59,6 @@ "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "jsdom": "^25.0.1", - "vitest": "^2.1.8" + "vitest": "^2.1.9" } } From f108f20466104e43557079289bdb89eed934e2dd Mon Sep 17 00:00:00 2001 From: Daniel Steigman <35793213+NightTrek@users.noreply.github.com> Date: Wed, 5 Feb 2025 01:11:08 -0800 Subject: [PATCH 25/33] updated the model list to remove the embedding model (#1646) --- src/shared/api.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/shared/api.ts b/src/shared/api.ts index 432e81fa15..a9eacd619f 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -445,22 +445,6 @@ export const mistralModels = { inputPrice: 0.1, outputPrice: 0.1, }, - "mistral-embed": { - maxTokens: 8_000, - contextWindow: 8_000, - supportsImages: false, - supportsPromptCache: false, - inputPrice: 0.1, - outputPrice: 0.1, - }, - "mistral-moderation-2411": { - maxTokens: 8_000, - contextWindow: 8_000, - supportsImages: false, - supportsPromptCache: false, - inputPrice: 0.1, - outputPrice: 0.1, - }, "mistral-small-2501": { maxTokens: 32_000, contextWindow: 32_000, From 0b7fac0c9d67c714ec4bea066c37ab69b45be0a4 Mon Sep 17 00:00:00 2001 From: omercelik Date: Wed, 5 Feb 2025 18:43:55 +0000 Subject: [PATCH 26/33] Add Gemini 2.0 Pro (#1655) --- src/shared/api.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/shared/api.ts b/src/shared/api.ts index a9eacd619f..6c03d8b9f1 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -246,6 +246,14 @@ export const openAiModelInfoSaneDefaults: ModelInfo = { export type GeminiModelId = keyof typeof geminiModels export const geminiDefaultModelId: GeminiModelId = "gemini-2.0-flash-thinking-exp-1219" export const geminiModels = { + "gemini-2.0-pro-exp-02-05": { + maxTokens: 8192, + contextWindow: 2_097_152, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + }, "gemini-2.0-flash-thinking-exp-01-21": { maxTokens: 65536, contextWindow: 1_048_576, From b2e4623559253e3f17915aaa45632a5b1d31536d Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Wed, 5 Feb 2025 10:49:55 -0800 Subject: [PATCH 27/33] Add new gemini models (#1656) * Update gemini models * Update changeset --- .changeset/funny-peas-call.md | 5 +++++ src/shared/api.ts | 38 +++++++++++++++++++++++++---------- 2 files changed, 32 insertions(+), 11 deletions(-) create mode 100644 .changeset/funny-peas-call.md diff --git a/.changeset/funny-peas-call.md b/.changeset/funny-peas-call.md new file mode 100644 index 0000000000..e60dc5ca5b --- /dev/null +++ b/.changeset/funny-peas-call.md @@ -0,0 +1,5 @@ +--- +"claude-dev": patch +--- + +Add new gemini models diff --git a/src/shared/api.ts b/src/shared/api.ts index 6c03d8b9f1..18b287d20f 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -244,8 +244,24 @@ export const openAiModelInfoSaneDefaults: ModelInfo = { // Gemini // https://ai.google.dev/gemini-api/docs/models/gemini export type GeminiModelId = keyof typeof geminiModels -export const geminiDefaultModelId: GeminiModelId = "gemini-2.0-flash-thinking-exp-1219" +export const geminiDefaultModelId: GeminiModelId = "gemini-2.0-flash-001" export const geminiModels = { + "gemini-2.0-flash-001": { + maxTokens: 8192, + contextWindow: 1_048_576, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + }, + "gemini-2.0-flash-lite-preview-02-05": { + maxTokens: 8192, + contextWindow: 1_048_576, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + }, "gemini-2.0-pro-exp-02-05": { maxTokens: 8192, contextWindow: 2_097_152, @@ -255,7 +271,7 @@ export const geminiModels = { outputPrice: 0, }, "gemini-2.0-flash-thinking-exp-01-21": { - maxTokens: 65536, + maxTokens: 65_536, contextWindow: 1_048_576, supportsImages: true, supportsPromptCache: false, @@ -278,14 +294,6 @@ export const geminiModels = { inputPrice: 0, outputPrice: 0, }, - "gemini-exp-1206": { - maxTokens: 8192, - contextWindow: 2_097_152, - supportsImages: true, - supportsPromptCache: false, - inputPrice: 0, - outputPrice: 0, - }, "gemini-1.5-flash-002": { maxTokens: 8192, contextWindow: 1_048_576, @@ -326,6 +334,14 @@ export const geminiModels = { inputPrice: 0, outputPrice: 0, }, + "gemini-exp-1206": { + maxTokens: 8192, + contextWindow: 2_097_152, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + }, } as const satisfies Record // OpenAI Native @@ -506,4 +522,4 @@ export const liteLlmModelInfoSaneDefaults: ModelInfo = { supportsPromptCache: false, inputPrice: 0, outputPrice: 0, -} +} \ No newline at end of file From 0795b046e1401f0a67c6af8b53ecccbb54d974a1 Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Wed, 5 Feb 2025 12:49:58 -0800 Subject: [PATCH 28/33] Prepare for release --- .changeset/funny-peas-call.md | 5 ----- .changeset/twelve-deers-search.md | 5 ----- CHANGELOG.md | 6 ++++++ package.json | 2 +- src/shared/api.ts | 2 +- 5 files changed, 8 insertions(+), 12 deletions(-) delete mode 100644 .changeset/funny-peas-call.md delete mode 100644 .changeset/twelve-deers-search.md diff --git a/.changeset/funny-peas-call.md b/.changeset/funny-peas-call.md deleted file mode 100644 index e60dc5ca5b..0000000000 --- a/.changeset/funny-peas-call.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"claude-dev": patch ---- - -Add new gemini models diff --git a/.changeset/twelve-deers-search.md b/.changeset/twelve-deers-search.md deleted file mode 100644 index f87090b079..0000000000 --- a/.changeset/twelve-deers-search.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"claude-dev": minor ---- - -Adding changesets for automating version bumping and release notes diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f6d5e5d65..b0c6a59c8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [3.2.13] + +- Add new gemini models gemini-2.0-flash-lite-preview-02-05 and gemini-2.0-flash-001 +- Add all available Mistral API models (thanks @ViezeVingertjes!) +- Add LiteLLM API provider support (thanks @him0!) + ## [3.2.12] - Fix command chaining for Windows users diff --git a/package.json b/package.json index f33033f379..c2d4bfc4e4 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "claude-dev", "displayName": "Cline", "description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.", - "version": "3.2.12", + "version": "3.2.13", "icon": "assets/icons/icon.png", "galleryBanner": { "color": "#617A91", diff --git a/src/shared/api.ts b/src/shared/api.ts index 18b287d20f..d0651b0419 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -522,4 +522,4 @@ export const liteLlmModelInfoSaneDefaults: ModelInfo = { supportsPromptCache: false, inputPrice: 0, outputPrice: 0, -} \ No newline at end of file +} From bd5eb8fcaef6e03ee6244ede331a892ce398952b Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 6 Feb 2025 08:11:01 +0100 Subject: [PATCH 29/33] feat: add retry decorator with rate limit handling (#1605) * fix: improve retry decorator with smart rate limit handling - Add handling of rate limit (429) errors - Implement retry timing based on response headers - Add exponential backoff when no headers present - Add a few unit tests Fixes #713 * Create modern-knives-tan.md * Improve readability in retry.ts --------- Co-authored-by: Michael Overhorst Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> --- .changeset/modern-knives-tan.md | 5 + src/api/providers/anthropic.ts | 2 + src/api/providers/deepseek.ts | 2 + src/api/providers/gemini.ts | 2 + src/api/providers/mistral.ts | 2 + src/api/providers/openai-native.ts | 2 + src/api/providers/openai.ts | 2 + src/api/providers/openrouter.ts | 2 + src/api/providers/vertex.ts | 2 + src/api/retry.test.ts | 216 +++++++++++++++++++++++++++++ src/api/retry.ts | 62 +++++++++ 11 files changed, 299 insertions(+) create mode 100644 .changeset/modern-knives-tan.md create mode 100644 src/api/retry.test.ts create mode 100644 src/api/retry.ts diff --git a/.changeset/modern-knives-tan.md b/.changeset/modern-knives-tan.md new file mode 100644 index 0000000000..d2aaa250b3 --- /dev/null +++ b/.changeset/modern-knives-tan.md @@ -0,0 +1,5 @@ +--- +"claude-dev": patch +--- + +Add automatic retry for rate limited requests diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts index 8c3fd1b87d..3aff867b7f 100644 --- a/src/api/providers/anthropic.ts +++ b/src/api/providers/anthropic.ts @@ -1,5 +1,6 @@ import { Anthropic } from "@anthropic-ai/sdk" import { Stream as AnthropicStream } from "@anthropic-ai/sdk/streaming" +import { withRetry } from "../retry" import { anthropicDefaultModelId, AnthropicModelId, anthropicModels, ApiHandlerOptions, ModelInfo } from "../../shared/api" import { ApiHandler } from "../index" import { ApiStream } from "../transform/stream" @@ -16,6 +17,7 @@ export class AnthropicHandler implements ApiHandler { }) } + @withRetry() async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { const model = this.getModel() let stream: AnthropicStream diff --git a/src/api/providers/deepseek.ts b/src/api/providers/deepseek.ts index 763e1ae68f..9049e646db 100644 --- a/src/api/providers/deepseek.ts +++ b/src/api/providers/deepseek.ts @@ -1,5 +1,6 @@ import { Anthropic } from "@anthropic-ai/sdk" import OpenAI from "openai" +import { withRetry } from "../retry" import { ApiHandler } from "../" import { ApiHandlerOptions, DeepSeekModelId, ModelInfo, deepSeekDefaultModelId, deepSeekModels } from "../../shared/api" import { convertToOpenAiMessages } from "../transform/openai-format" @@ -18,6 +19,7 @@ export class DeepSeekHandler implements ApiHandler { }) } + @withRetry() async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { const model = this.getModel() diff --git a/src/api/providers/gemini.ts b/src/api/providers/gemini.ts index 39c55548d1..b452af6dbb 100644 --- a/src/api/providers/gemini.ts +++ b/src/api/providers/gemini.ts @@ -1,5 +1,6 @@ import { Anthropic } from "@anthropic-ai/sdk" import { GoogleGenerativeAI } from "@google/generative-ai" +import { withRetry } from "../retry" import { ApiHandler } from "../" import { ApiHandlerOptions, geminiDefaultModelId, GeminiModelId, geminiModels, ModelInfo } from "../../shared/api" import { convertAnthropicMessageToGemini } from "../transform/gemini-format" @@ -17,6 +18,7 @@ export class GeminiHandler implements ApiHandler { this.client = new GoogleGenerativeAI(options.geminiApiKey) } + @withRetry() async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { const model = this.client.getGenerativeModel({ model: this.getModel().id, diff --git a/src/api/providers/mistral.ts b/src/api/providers/mistral.ts index 5be89b18f3..28c2331c60 100644 --- a/src/api/providers/mistral.ts +++ b/src/api/providers/mistral.ts @@ -1,5 +1,6 @@ import { Anthropic } from "@anthropic-ai/sdk" import { Mistral } from "@mistralai/mistralai" +import { withRetry } from "../retry" import { ApiHandler } from "../" import { ApiHandlerOptions, @@ -26,6 +27,7 @@ export class MistralHandler implements ApiHandler { }) } + @withRetry() async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { const stream = await this.client.chat.stream({ model: this.getModel().id, diff --git a/src/api/providers/openai-native.ts b/src/api/providers/openai-native.ts index 8a47ec4345..63f30bd61c 100644 --- a/src/api/providers/openai-native.ts +++ b/src/api/providers/openai-native.ts @@ -1,5 +1,6 @@ import { Anthropic } from "@anthropic-ai/sdk" import OpenAI from "openai" +import { withRetry } from "../retry" import { ApiHandler } from "../" import { ApiHandlerOptions, @@ -22,6 +23,7 @@ export class OpenAiNativeHandler implements ApiHandler { }) } + @withRetry() async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { switch (this.getModel().id) { case "o1": diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index fd73abb567..c03b1d13ec 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -1,5 +1,6 @@ import { Anthropic } from "@anthropic-ai/sdk" import OpenAI, { AzureOpenAI } from "openai" +import { withRetry } from "../retry" import { ApiHandlerOptions, azureOpenAiDefaultApiVersion, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api" import { ApiHandler } from "../index" import { convertToOpenAiMessages } from "../transform/openai-format" @@ -27,6 +28,7 @@ export class OpenAiHandler implements ApiHandler { } } + @withRetry() async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { const modelId = this.options.openAiModelId ?? "" const isDeepseekReasoner = modelId.includes("deepseek-reasoner") diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index 6b8f40c5e7..45cbd2c7ac 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -2,6 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk" import axios from "axios" import delay from "delay" import OpenAI from "openai" +import { withRetry } from "../retry" import { ApiHandler } from "../" import { ApiHandlerOptions, ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "../../shared/api" import { convertToOpenAiMessages } from "../transform/openai-format" @@ -24,6 +25,7 @@ export class OpenRouterHandler implements ApiHandler { }) } + @withRetry() async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { const model = this.getModel() diff --git a/src/api/providers/vertex.ts b/src/api/providers/vertex.ts index c8f1efd873..286562ed45 100644 --- a/src/api/providers/vertex.ts +++ b/src/api/providers/vertex.ts @@ -1,5 +1,6 @@ import { Anthropic } from "@anthropic-ai/sdk" import { AnthropicVertex } from "@anthropic-ai/vertex-sdk" +import { withRetry } from "../retry" import { ApiHandler } from "../" import { ApiHandlerOptions, ModelInfo, vertexDefaultModelId, VertexModelId, vertexModels } from "../../shared/api" import { ApiStream } from "../transform/stream" @@ -18,6 +19,7 @@ export class VertexHandler implements ApiHandler { }) } + @withRetry() async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { const stream = await this.client.messages.create({ model: this.getModel().id, diff --git a/src/api/retry.test.ts b/src/api/retry.test.ts new file mode 100644 index 0000000000..43b8eaf3e9 --- /dev/null +++ b/src/api/retry.test.ts @@ -0,0 +1,216 @@ +import { describe, it } from "mocha" +import "should" +import { withRetry } from "./retry" + +describe("Retry Decorator", () => { + describe("withRetry", () => { + it("should not retry on success", async () => { + let callCount = 0 + class TestClass { + @withRetry() + async *successMethod() { + callCount++ + yield "success" + } + } + + const test = new TestClass() + const result = [] + for await (const value of test.successMethod()) { + result.push(value) + } + + callCount.should.equal(1) + result.should.deepEqual(["success"]) + }) + + it("should retry on rate limit (429) error", async () => { + let callCount = 0 + class TestClass { + @withRetry({ maxRetries: 2, baseDelay: 10, maxDelay: 100 }) + async *failMethod() { + callCount++ + if (callCount === 1) { + const error: any = new Error("Rate limit exceeded") + error.status = 429 + throw error + } + yield "success after retry" + } + } + + const test = new TestClass() + const result = [] + for await (const value of test.failMethod()) { + result.push(value) + } + + callCount.should.equal(2) + result.should.deepEqual(["success after retry"]) + }) + + it("should not retry on non-rate-limit errors", async () => { + let callCount = 0 + class TestClass { + @withRetry() + async *failMethod() { + callCount++ + throw new Error("Regular error") + } + } + + const test = new TestClass() + try { + for await (const _ of test.failMethod()) { + // Should not reach here + } + throw new Error("Should have thrown") + } catch (error: any) { + error.message.should.equal("Regular error") + callCount.should.equal(1) + } + }) + + it("should respect retry-after header with delta seconds", async () => { + let callCount = 0 + const startTime = Date.now() + class TestClass { + @withRetry({ maxRetries: 2, baseDelay: 1000 }) // Use large baseDelay to ensure header takes precedence + async *failMethod() { + callCount++ + if (callCount === 1) { + const error: any = new Error("Rate limit exceeded") + error.status = 429 + error.headers = { "retry-after": "0.01" } // 10ms delay + throw error + } + yield "success after retry" + } + } + + const test = new TestClass() + const result = [] + for await (const value of test.failMethod()) { + result.push(value) + } + + const duration = Date.now() - startTime + duration.should.be.approximately(10, 10) // Allow 10ms variance + callCount.should.equal(2) + result.should.deepEqual(["success after retry"]) + }) + + it("should respect retry-after header with Unix timestamp", async () => { + let callCount = 0 + const startTime = Date.now() + const retryTimestamp = Math.floor(Date.now() / 1000) + 0.01 // 10ms in the future + + class TestClass { + @withRetry({ maxRetries: 2, baseDelay: 1000 }) // Use large baseDelay to ensure header takes precedence + async *failMethod() { + callCount++ + if (callCount === 1) { + const error: any = new Error("Rate limit exceeded") + error.status = 429 + error.headers = { "retry-after": retryTimestamp.toString() } + throw error + } + yield "success after retry" + } + } + + const test = new TestClass() + const result = [] + for await (const value of test.failMethod()) { + result.push(value) + } + + const duration = Date.now() - startTime + duration.should.be.approximately(10, 10) // Allow 10ms variance + callCount.should.equal(2) + result.should.deepEqual(["success after retry"]) + }) + + it("should use exponential backoff when no retry-after header", async () => { + let callCount = 0 + const startTime = Date.now() + class TestClass { + @withRetry({ maxRetries: 2, baseDelay: 10, maxDelay: 100 }) + async *failMethod() { + callCount++ + if (callCount === 1) { + const error: any = new Error("Rate limit exceeded") + error.status = 429 + throw error + } + yield "success after retry" + } + } + + const test = new TestClass() + const result = [] + for await (const value of test.failMethod()) { + result.push(value) + } + + const duration = Date.now() - startTime + // First retry should be after baseDelay (10ms) + duration.should.be.approximately(10, 10) + callCount.should.equal(2) + result.should.deepEqual(["success after retry"]) + }) + + it("should respect maxDelay", async () => { + let callCount = 0 + const startTime = Date.now() + class TestClass { + @withRetry({ maxRetries: 3, baseDelay: 50, maxDelay: 10 }) + async *failMethod() { + callCount++ + if (callCount < 3) { + const error: any = new Error("Rate limit exceeded") + error.status = 429 + throw error + } + yield "success after retries" + } + } + + const test = new TestClass() + const result = [] + for await (const value of test.failMethod()) { + result.push(value) + } + + const duration = Date.now() - startTime + // Both retries should be capped at maxDelay (10ms each) + duration.should.be.approximately(20, 20) + callCount.should.equal(3) + result.should.deepEqual(["success after retries"]) + }) + + it("should throw after maxRetries attempts", async () => { + let callCount = 0 + class TestClass { + @withRetry({ maxRetries: 2, baseDelay: 10 }) + async *failMethod() { + callCount++ + const error: any = new Error("Rate limit exceeded") + error.status = 429 + throw error + } + } + + const test = new TestClass() + try { + for await (const _ of test.failMethod()) { + // Should not reach here + } + throw new Error("Should have thrown") + } catch (error: any) { + error.message.should.equal("Rate limit exceeded") + callCount.should.equal(2) // Initial attempt + 1 retry + } + }) + }) +}) diff --git a/src/api/retry.ts b/src/api/retry.ts new file mode 100644 index 0000000000..deeabfb365 --- /dev/null +++ b/src/api/retry.ts @@ -0,0 +1,62 @@ +interface RetryOptions { + maxRetries?: number + baseDelay?: number + maxDelay?: number +} + +const DEFAULT_OPTIONS: Required = { + maxRetries: 3, + baseDelay: 1_000, + maxDelay: 10_000, +} + +export function withRetry(options: RetryOptions = {}) { + const { maxRetries, baseDelay, maxDelay } = { ...DEFAULT_OPTIONS, ...options } + + return function (_target: any, _propertyKey: string, descriptor: PropertyDescriptor) { + const originalMethod = descriptor.value + + descriptor.value = async function* (...args: any[]) { + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + yield* originalMethod.apply(this, args) + return + } catch (error: any) { + const isRateLimit = error?.status === 429 + const isLastAttempt = attempt === maxRetries - 1 + + if (!isRateLimit || isLastAttempt) { + throw error + } + + // Get retry delay from header or calculate exponential backoff + // Check various rate limit headers + const retryAfter = + error.headers?.["retry-after"] || + error.headers?.["x-ratelimit-reset"] || + error.headers?.["ratelimit-reset"] + + let delay: number + if (retryAfter) { + // Handle both delta-seconds and Unix timestamp formats + const retryValue = parseInt(retryAfter, 10) + if (retryValue > Date.now() / 1000) { + // Unix timestamp + delay = retryValue * 1000 - Date.now() + } else { + // Delta seconds + delay = retryValue * 1000 + } + } else { + // Use exponential backoff if no header + delay = Math.min(maxDelay, baseDelay * Math.pow(2, attempt)) + } + + await new Promise((resolve) => setTimeout(resolve, delay)) + } + } + } + + return descriptor + } +} From 1935aab2d554694631833d220f37f37a21aa8b8b Mon Sep 17 00:00:00 2001 From: nickbaumann98 <163209607+nickbaumann98@users.noreply.github.com> Date: Wed, 5 Feb 2025 23:14:00 -0800 Subject: [PATCH 30/33] Update README.md to include Getting Started (#1660) * Update README.md * Create neat-apricots-search.md --------- Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> --- .changeset/neat-apricots-search.md | 5 +++++ README.md | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/neat-apricots-search.md diff --git a/.changeset/neat-apricots-search.md b/.changeset/neat-apricots-search.md new file mode 100644 index 0000000000..46ce78fc0f --- /dev/null +++ b/.changeset/neat-apricots-search.md @@ -0,0 +1,5 @@ +--- +"claude-dev": patch +--- + +Update README.md to include Getting Started diff --git a/README.md b/README.md index b3e8959beb..4f48c5ad41 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ English | Feature Requests -We're Hiring! +Getting Started From c88b5c67d5a1789761175920fa19dda967145dc1 Mon Sep 17 00:00:00 2001 From: akfoster Date: Wed, 5 Feb 2025 23:15:52 -0800 Subject: [PATCH 31/33] Fix Changeset Test (#1657) * should only pass if the PR has a new changeset * improvements to docs-only check * add changeset --- .changeset/purple-panthers-arrive.md | 5 +++++ .github/workflows/check-changeset.yml | 24 +++++++++++------------- 2 files changed, 16 insertions(+), 13 deletions(-) create mode 100644 .changeset/purple-panthers-arrive.md diff --git a/.changeset/purple-panthers-arrive.md b/.changeset/purple-panthers-arrive.md new file mode 100644 index 0000000000..c4be31f613 --- /dev/null +++ b/.changeset/purple-panthers-arrive.md @@ -0,0 +1,5 @@ +--- +"claude-dev": patch +--- + +Fix a bug where we were not properly checking for changesets in check-changeset git action diff --git a/.github/workflows/check-changeset.yml b/.github/workflows/check-changeset.yml index 48ffc1ca41..f220257c89 100644 --- a/.github/workflows/check-changeset.yml +++ b/.github/workflows/check-changeset.yml @@ -40,13 +40,12 @@ jobs: echo "Changed files:" echo "$CHANGED_FILES" - echo "Listing .changeset directory:" - ls -la .changeset/ - # Check if any of the changed files are in docs/ or .github/ + echo "Checking if changes are docs-only..." DOCS_ONLY=true while IFS= read -r file; do if [[ ! "$file" =~ ^(docs/|.github/) ]]; then + echo "Found non-docs change: $file" DOCS_ONLY=false break fi @@ -54,14 +53,17 @@ jobs: # If changes are docs-only, skip changeset check if [ "$DOCS_ONLY" = true ]; then - echo "Only documentation files were changed, skipping changeset check" + echo "All changes are in docs/ or .github/, skipping changeset check" exit 0 + else + echo "Changes include non-docs files, checking for changeset..." fi # Check if any changeset files are in the changed files + echo "Checking for changeset files in changed files..." CHANGESET_IN_PR=false while IFS= read -r file; do - if [[ "$file" =~ ^\.changeset/.*\.md$ && "$file" != ".changeset/README.md" ]]; then + if [[ "$file" =~ ^\.changeset/.*\.md$ && "$file" != ".changeset/README.md" && "$file" != ".changeset/config.json" ]]; then echo "Found changeset file in PR: $file" CHANGESET_IN_PR=true break @@ -69,14 +71,10 @@ jobs: done <<< "$CHANGED_FILES" if [ "$CHANGESET_IN_PR" = false ]; then - # Double check local changeset files as backup - CHANGESETS=$(find .changeset -name "*.md" ! -name "README.md" | wc -l | tr -d ' ') - echo "Number of local changesets: $CHANGESETS" - - if [ "$CHANGESETS" -eq 0 ]; then - echo "::error::No changeset file found in PR changes or local directory. Please run 'npm run changeset' to create one." - exit 1 - fi + echo "No changeset files found in changed files. Changed files in .changeset/:" + echo "$CHANGED_FILES" | grep "^\.changeset/" || true + echo "::error::No changeset file found in PR changes. Please run 'npm run changeset' to create one." + exit 1 fi - name: Comment on PR From 548338d39e10f6c8431b25c3f2937191be5eb3c6 Mon Sep 17 00:00:00 2001 From: aicc Date: Thu, 6 Feb 2025 15:23:33 +0800 Subject: [PATCH 32/33] Add alibaba qwen models plus/max/coder-plus/turbo both stable and latest to use. (#1648) * add alibaba qwen-max qwen-plus qwen-turbo qwen-coder-plus stable/latest models * add alibaba qwen-max qwen-plus qwen-turbo qwen-coder-plus stable/latest models * Provide the api line choice for international user * Remove redundant code * Copy fixes * Create dry-socks-talk.md --------- Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> --- .changeset/dry-socks-talk.md | 5 ++ src/api/index.ts | 3 + src/api/providers/qwen.ts | 76 ++++++++++++++++ src/core/webview/ClineProvider.ts | 13 +++ src/shared/api.ts | 90 +++++++++++++++++++ .../src/components/settings/ApiOptions.tsx | 64 +++++++++++++ .../src/context/ExtensionStateContext.tsx | 1 + webview-ui/src/utils/validate.ts | 5 ++ 8 files changed, 257 insertions(+) create mode 100644 .changeset/dry-socks-talk.md create mode 100644 src/api/providers/qwen.ts diff --git a/.changeset/dry-socks-talk.md b/.changeset/dry-socks-talk.md new file mode 100644 index 0000000000..df4995488e --- /dev/null +++ b/.changeset/dry-socks-talk.md @@ -0,0 +1,5 @@ +--- +"claude-dev": patch +--- + +Add Alibaba qwen models plus/max/coder-plus/turbo diff --git a/src/api/index.ts b/src/api/index.ts index 5ed9e6de8c..680eb53232 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -11,6 +11,7 @@ import { GeminiHandler } from "./providers/gemini" import { OpenAiNativeHandler } from "./providers/openai-native" import { ApiStream } from "./transform/stream" import { DeepSeekHandler } from "./providers/deepseek" +import { QwenHandler } from "./providers/qwen" import { MistralHandler } from "./providers/mistral" import { VsCodeLmHandler } from "./providers/vscode-lm" import { LiteLlmHandler } from "./providers/litellm" @@ -47,6 +48,8 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler { return new OpenAiNativeHandler(options) case "deepseek": return new DeepSeekHandler(options) + case "qwen": + return new QwenHandler(options) case "mistral": return new MistralHandler(options) case "vscode-lm": diff --git a/src/api/providers/qwen.ts b/src/api/providers/qwen.ts new file mode 100644 index 0000000000..9744fc1338 --- /dev/null +++ b/src/api/providers/qwen.ts @@ -0,0 +1,76 @@ +import { Anthropic } from "@anthropic-ai/sdk" +import OpenAI from "openai" +import { ApiHandler } from "../" +import { ApiHandlerOptions, QwenModelId, ModelInfo, qwenDefaultModelId, qwenModels } from "../../shared/api" +import { convertToOpenAiMessages } from "../transform/openai-format" +import { ApiStream } from "../transform/stream" + +export class QwenHandler implements ApiHandler { + private options: ApiHandlerOptions + private client: OpenAI + + constructor(options: ApiHandlerOptions) { + this.options = options + this.client = new OpenAI({ + baseURL: this.options.qwenApiLine || "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", + apiKey: this.options.qwenApiKey, + }) + } + + getModel(): { id: QwenModelId; info: ModelInfo } { + const modelId = this.options.apiModelId + if (modelId && modelId in qwenModels) { + const id = modelId as QwenModelId + return { id, info: qwenModels[id] } + } + return { + id: qwenDefaultModelId, + info: qwenModels[qwenDefaultModelId], + } + } + + async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { + const model = this.getModel() + let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [ + { role: "system", content: systemPrompt }, + ...convertToOpenAiMessages(messages), + ] + + const stream = await this.client.chat.completions.create({ + model: model.id, + max_completion_tokens: model.info.maxTokens, + messages: openAiMessages, + stream: true, + stream_options: { include_usage: true }, + }) + + for await (const chunk of stream) { + const delta = chunk.choices[0]?.delta + if (delta?.content) { + yield { + type: "text", + text: delta.content, + } + } + + if (delta && "reasoning_content" in delta && delta.reasoning_content) { + yield { + type: "reasoning", + reasoning: (delta.reasoning_content as string | undefined) || "", + } + } + + if (chunk.usage) { + yield { + type: "usage", + inputTokens: chunk.usage.prompt_tokens || 0, + outputTokens: chunk.usage.completion_tokens || 0, + // @ts-ignore-next-line + cacheReadTokens: chunk.usage.prompt_cache_hit_tokens || 0, + // @ts-ignore-next-line + cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0, + } + } + } + } +} diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index a15b99ed05..415656ffe6 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -45,6 +45,7 @@ type SecretKey = | "geminiApiKey" | "openAiNativeApiKey" | "deepSeekApiKey" + | "qwenApiKey" | "mistralApiKey" | "authToken" | "authNonce" @@ -78,6 +79,7 @@ type GlobalStateKey = | "previousModeModelInfo" | "liteLlmBaseUrl" | "liteLlmModelId" + | "qwenApiLine" export const GlobalFileNames = { apiConversationHistory: "api_conversation_history.json", @@ -440,6 +442,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { geminiApiKey, openAiNativeApiKey, deepSeekApiKey, + qwenApiKey, mistralApiKey, azureApiVersion, openRouterModelId, @@ -447,6 +450,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { vsCodeLmModelSelector, liteLlmBaseUrl, liteLlmModelId, + qwenApiLine, } = message.apiConfiguration await this.updateGlobalState("apiProvider", apiProvider) await this.updateGlobalState("apiModelId", apiModelId) @@ -470,6 +474,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.storeSecret("geminiApiKey", geminiApiKey) await this.storeSecret("openAiNativeApiKey", openAiNativeApiKey) await this.storeSecret("deepSeekApiKey", deepSeekApiKey) + await this.storeSecret("qwenApiKey", qwenApiKey) await this.storeSecret("mistralApiKey", mistralApiKey) await this.updateGlobalState("azureApiVersion", azureApiVersion) await this.updateGlobalState("openRouterModelId", openRouterModelId) @@ -477,6 +482,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.updateGlobalState("vsCodeLmModelSelector", vsCodeLmModelSelector) await this.updateGlobalState("liteLlmBaseUrl", liteLlmBaseUrl) await this.updateGlobalState("liteLlmModelId", liteLlmModelId) + await this.updateGlobalState("qwenApiLine", qwenApiLine) if (this.cline) { this.cline.api = buildApiHandler(message.apiConfiguration) } @@ -1365,6 +1371,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { geminiApiKey, openAiNativeApiKey, deepSeekApiKey, + qwenApiKey, mistralApiKey, azureApiVersion, openRouterModelId, @@ -1383,6 +1390,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { previousModeApiProvider, previousModeModelId, previousModeModelInfo, + qwenApiLine, ] = await Promise.all([ this.getGlobalState("apiProvider") as Promise, this.getGlobalState("apiModelId") as Promise, @@ -1406,6 +1414,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.getSecret("geminiApiKey") as Promise, this.getSecret("openAiNativeApiKey") as Promise, this.getSecret("deepSeekApiKey") as Promise, + this.getSecret("qwenApiKey") as Promise, this.getSecret("mistralApiKey") as Promise, this.getGlobalState("azureApiVersion") as Promise, this.getGlobalState("openRouterModelId") as Promise, @@ -1424,6 +1433,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.getGlobalState("previousModeApiProvider") as Promise, this.getGlobalState("previousModeModelId") as Promise, this.getGlobalState("previousModeModelInfo") as Promise, + this.getGlobalState("qwenApiLine") as Promise, ]) let apiProvider: ApiProvider @@ -1464,6 +1474,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { geminiApiKey, openAiNativeApiKey, deepSeekApiKey, + qwenApiKey, + qwenApiLine, mistralApiKey, azureApiVersion, openRouterModelId, @@ -1559,6 +1571,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { "geminiApiKey", "openAiNativeApiKey", "deepSeekApiKey", + "qwenApiKey", "mistralApiKey", "authToken", ] diff --git a/src/shared/api.ts b/src/shared/api.ts index d0651b0419..24f93e96cf 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -9,6 +9,7 @@ export type ApiProvider = | "gemini" | "openai-native" | "deepseek" + | "qwen" | "mistral" | "vscode-lm" | "litellm" @@ -39,9 +40,11 @@ export interface ApiHandlerOptions { geminiApiKey?: string openAiNativeApiKey?: string deepSeekApiKey?: string + qwenApiKey?: string mistralApiKey?: string azureApiVersion?: string vsCodeLmModelSelector?: any + qwenApiLine?: string } export type ApiConfiguration = ApiHandlerOptions & { @@ -432,6 +435,93 @@ export const deepSeekModels = { }, } as const satisfies Record +// Qwen +// https://bailian.console.aliyun.com/ +export type QwenModelId = keyof typeof qwenModels +export const qwenDefaultModelId: QwenModelId = "qwen-coder-plus-latest" +export const qwenModels = { + "qwen-coder-plus-latest": { + maxTokens: 129_024, + contextWindow: 131_072, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.0035, + outputPrice: 0.007, + cacheWritesPrice: 0.0035, + cacheReadsPrice: 0.007, + }, + "qwen-plus-latest": { + maxTokens: 129_024, + contextWindow: 131_072, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.0008, + outputPrice: 0.002, + cacheWritesPrice: 0.0004, + cacheReadsPrice: 0.001, + }, + "qwen-turbo-latest": { + maxTokens: 1_000_000, + contextWindow: 1_000_000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.0003, + outputPrice: 0.0006, + cacheWritesPrice: 0.00015, + cacheReadsPrice: 0.0003, + }, + "qwen-max-latest": { + maxTokens: 30_720, + contextWindow: 32_768, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.0112, + outputPrice: 0.0448, + cacheWritesPrice: 0.0056, + cacheReadsPrice: 0.0224, + }, + "qwen-coder-plus": { + maxTokens: 129_024, + contextWindow: 131_072, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.0035, + outputPrice: 0.007, + cacheWritesPrice: 0.0035, + cacheReadsPrice: 0.007, + }, + "qwen-plus": { + maxTokens: 129_024, + contextWindow: 131_072, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.0008, + outputPrice: 0.002, + cacheWritesPrice: 0.0004, + cacheReadsPrice: 0.001, + }, + "qwen-turbo": { + maxTokens: 1_000_000, + contextWindow: 1_000_000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.0003, + outputPrice: 0.0006, + cacheWritesPrice: 0.00015, + cacheReadsPrice: 0.0003, + }, + "qwen-max": { + maxTokens: 30_720, + contextWindow: 32_768, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.0112, + outputPrice: 0.0448, + cacheWritesPrice: 0.0056, + cacheReadsPrice: 0.0224, + }, +} as const satisfies Record + // Mistral // https://docs.mistral.ai/getting-started/models/models_overview/ export type MistralModelId = keyof typeof mistralModels diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 84018b9efd..cbbf18198d 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -20,6 +20,8 @@ import { bedrockModels, deepSeekDefaultModelId, deepSeekModels, + qwenDefaultModelId, + qwenModels, geminiDefaultModelId, geminiModels, mistralDefaultModelId, @@ -179,6 +181,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is Anthropic Google Gemini DeepSeek + Qwen Mistral GCP Vertex AI AWS Bedrock @@ -310,6 +313,64 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
)} + {selectedProvider === "qwen" && ( +
+ + + + China API + + International API + + + +

+ Please select the appropriate API interface based on your location. If you are in China, choose the China + API interface. Otherwise, choose the International API interface. +

+ + Qwen API Key + +

+ This key is stored locally and only used to make API requests from this extension. + {!apiConfiguration?.qwenApiKey && ( + + You can get a Qwen API key by signing up here. + + )} +

+
+ )} + {selectedProvider === "mistral" && (
@@ -1068,6 +1130,8 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration): return getProviderData(openAiNativeModels, openAiNativeDefaultModelId) case "deepseek": return getProviderData(deepSeekModels, deepSeekDefaultModelId) + case "qwen": + return getProviderData(qwenModels, qwenDefaultModelId) case "mistral": return getProviderData(mistralModels, mistralDefaultModelId) case "openrouter": diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 626e9e6606..99a8cca3f9 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -67,6 +67,7 @@ export const ExtensionStateContextProvider: React.FC<{ config.geminiApiKey, config.openAiNativeApiKey, config.deepSeekApiKey, + config.qwenApiKey, config.mistralApiKey, config.vsCodeLmModelSelector, ].some((key) => key !== undefined) diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index beafc65572..4617fb2c70 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -38,6 +38,11 @@ export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): s return "You must provide a valid API key or choose a different provider." } break + case "qwen": + if (!apiConfiguration.qwenApiKey) { + return "You must provide a valid API key or choose a different provider." + } + break case "mistral": if (!apiConfiguration.mistralApiKey) { return "You must provide a valid API key or choose a different provider." From 10b8ee4f54fdb077724381d32530c5c67c4b54be Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Thu, 6 Feb 2025 15:12:25 -0500 Subject: [PATCH 33/33] Fix bug when shell profile is not found (#1671) --- src/test/shell.test.ts | 5 +++++ src/utils/shell.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/test/shell.test.ts b/src/test/shell.test.ts index 51d0e85dc9..7e919f8a3b 100644 --- a/src/test/shell.test.ts +++ b/src/test/shell.test.ts @@ -78,6 +78,11 @@ describe("Shell Detection Tests", () => { expect(getShell()).to.equal("C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe") }) + it("handles undefined shell profile gracefully", () => { + mockVsCodeConfig("windows", "NonExistentProfile", {}) + expect(getShell()).to.equal("C:\\Windows\\System32\\cmd.exe") + }) + it("uses WSL bash when profile indicates WSL source", () => { mockVsCodeConfig("windows", "WSL", { WSL: { source: "WSL" }, diff --git a/src/utils/shell.ts b/src/utils/shell.ts index 8871550a0e..2f7ffb3a88 100644 --- a/src/utils/shell.ts +++ b/src/utils/shell.ts @@ -105,7 +105,7 @@ function getWindowsShellFromVSCode(): string | null { } // If there's a specific path, return that immediately - if (profile.path) { + if (profile?.path) { return profile.path }