From 8e12bdb00d5b913dbb390db7c549aacc283e7645 Mon Sep 17 00:00:00 2001 From: wen-jy <36290410+WEN-JY@users.noreply.github.com> Date: Fri, 14 Feb 2025 10:03:20 +0800 Subject: [PATCH 01/12] Add support for qwen vl models (#1776) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add support for qwen vl models * feat: add support for qwen vl models * feat: updated the price of the Qianwen model following the BaiLian platform documentation --------- Co-authored-by: 执无 --- .changeset/five-flies-breathe.md | 5 ++ src/shared/api.ts | 104 +++++++++++++++++++++---------- 2 files changed, 77 insertions(+), 32 deletions(-) create mode 100644 .changeset/five-flies-breathe.md diff --git a/.changeset/five-flies-breathe.md b/.changeset/five-flies-breathe.md new file mode 100644 index 0000000000..aa13a9b71f --- /dev/null +++ b/.changeset/five-flies-breathe.md @@ -0,0 +1,5 @@ +--- +"claude-dev": patch +--- + +Add support for qwen vl models diff --git a/src/shared/api.ts b/src/shared/api.ts index 4794327437..0494f2df1f 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -456,80 +456,80 @@ export const qwenModels = { contextWindow: 131_072, supportsImages: false, supportsPromptCache: false, - inputPrice: 0.0035, - outputPrice: 0.007, - cacheWritesPrice: 0.0035, - cacheReadsPrice: 0.007, + inputPrice: 3.5, + outputPrice: 7, + cacheWritesPrice: 3.5, + cacheReadsPrice: 7, }, "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, + inputPrice: 0.8, + outputPrice: 2, + cacheWritesPrice: 0.8, + cacheReadsPrice: 0.2, }, "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, + inputPrice: 0.8, + outputPrice: 2, + cacheWritesPrice: 0.8, + cacheReadsPrice: 2, }, "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, + inputPrice: 2.4, + outputPrice: 9.6, + cacheWritesPrice: 2.4, + cacheReadsPrice: 9.6, }, "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, + inputPrice: 3.5, + outputPrice: 7, + cacheWritesPrice: 3.5, + cacheReadsPrice: 7, }, "qwen-plus": { maxTokens: 129_024, contextWindow: 131_072, supportsImages: false, supportsPromptCache: false, - inputPrice: 0.0008, - outputPrice: 0.002, - cacheWritesPrice: 0.0004, - cacheReadsPrice: 0.001, + inputPrice: 0.8, + outputPrice: 2, + cacheWritesPrice: 0.8, + cacheReadsPrice: 0.2, }, "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, + inputPrice: 0.3, + outputPrice: 0.6, + cacheWritesPrice: 0.3, + cacheReadsPrice: 0.6, }, "qwen-max": { maxTokens: 30_720, contextWindow: 32_768, supportsImages: false, supportsPromptCache: false, - inputPrice: 0.0112, - outputPrice: 0.0448, - cacheWritesPrice: 0.0056, - cacheReadsPrice: 0.0224, + inputPrice: 2.4, + outputPrice: 9.6, + cacheWritesPrice: 2.4, + cacheReadsPrice: 9.6, }, "deepseek-v3": { maxTokens: 8_000, @@ -551,6 +551,46 @@ export const qwenModels = { cacheWritesPrice: 0.55, cacheReadsPrice: 0.14, }, + "qwen-vl-max": { + maxTokens: 30_720, + contextWindow: 32_768, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 3, + outputPrice: 9, + cacheWritesPrice: 3, + cacheReadsPrice: 9, + }, + "qwen-vl-max-latest": { + maxTokens: 129_024, + contextWindow: 131_072, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 3, + outputPrice: 9, + cacheWritesPrice: 3, + cacheReadsPrice: 9, + }, + "qwen-vl-plus": { + maxTokens: 6_000, + contextWindow: 8_000, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 1.5, + outputPrice: 4.5, + cacheWritesPrice: 1.5, + cacheReadsPrice: 4.5, + }, + "qwen-vl-plus-latest": { + maxTokens: 129_024, + contextWindow: 131_072, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 1.5, + outputPrice: 4.5, + cacheWritesPrice: 1.5, + cacheReadsPrice: 4.5, + }, } as const satisfies Record // Mistral From ee06c0811c6f72bdba0f48fe13a56cd995d72218 Mon Sep 17 00:00:00 2001 From: stephen8339 <55537353@qq.com> Date: Sat, 15 Feb 2025 03:10:37 +0800 Subject: [PATCH 02/12] add all qwen2.5 coder models (#1797) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * fix problem what is when you use Qwen api provider and then you want to change the api provider ,the apiline dropdown will obscure your api provider drop-down options * feat: add qwen2.5-coder models Description Add new models as list: qwen2.5-coder-32b-instruct qwen2.5-coder-14b-instruct qwen2.5-coder-7b-instruct qwen2.5-coder-3b-instruct Doc: https://help.aliyun.com/zh/model-studio/getting-started/models#9f8890ce29g5u * add changeset * feat: add all alibaba qwen2.5 coder models --------- Co-authored-by: yaojunWang Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Co-authored-by: 刘耸 --- .changeset/gentle-glasses-flow.md | 5 +++ .changeset/slimy-roses-dance.md | 5 +++ src/shared/api.ts | 60 +++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 .changeset/gentle-glasses-flow.md create mode 100644 .changeset/slimy-roses-dance.md diff --git a/.changeset/gentle-glasses-flow.md b/.changeset/gentle-glasses-flow.md new file mode 100644 index 0000000000..c3d9ed997c --- /dev/null +++ b/.changeset/gentle-glasses-flow.md @@ -0,0 +1,5 @@ +--- +"claude-dev": minor +--- + +add alibaba qwen2.5 coder models diff --git a/.changeset/slimy-roses-dance.md b/.changeset/slimy-roses-dance.md new file mode 100644 index 0000000000..e88ef3ca57 --- /dev/null +++ b/.changeset/slimy-roses-dance.md @@ -0,0 +1,5 @@ +--- +"claude-dev": minor +--- + +add alibaba qwen2.5-coder models diff --git a/src/shared/api.ts b/src/shared/api.ts index 0494f2df1f..444242433d 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -451,6 +451,66 @@ export const deepSeekModels = { export type QwenModelId = keyof typeof qwenModels export const qwenDefaultModelId: QwenModelId = "qwen-coder-plus-latest" export const qwenModels = { + "qwen2.5-coder-32b-instruct": { + maxTokens: 8_192, + contextWindow: 131_072, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.002, + outputPrice: 0.006, + cacheWritesPrice: 0.002, + cacheReadsPrice: 0.006, + }, + "qwen2.5-coder-14b-instruct": { + maxTokens: 8_192, + contextWindow: 131_072, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.002, + outputPrice: 0.006, + cacheWritesPrice: 0.002, + cacheReadsPrice: 0.006, + }, + "qwen2.5-coder-7b-instruct": { + maxTokens: 8_192, + contextWindow: 131_072, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.001, + outputPrice: 0.002, + cacheWritesPrice: 0.001, + cacheReadsPrice: 0.002, + }, + "qwen2.5-coder-3b-instruct": { + maxTokens: 8_192, + contextWindow: 32_768, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.0, + outputPrice: 0.0, + cacheWritesPrice: 0.0, + cacheReadsPrice: 0.0, + }, + "qwen2.5-coder-1.5b-instruct": { + maxTokens: 8_192, + contextWindow: 32_768, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.0, + outputPrice: 0.0, + cacheWritesPrice: 0.0, + cacheReadsPrice: 0.0, + }, + "qwen2.5-coder-0.5b-instruct": { + maxTokens: 8_192, + contextWindow: 32_768, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.0, + outputPrice: 0.0, + cacheWritesPrice: 0.0, + cacheReadsPrice: 0.0, + }, "qwen-coder-plus-latest": { maxTokens: 129_024, contextWindow: 131_072, From 584af643344b28ac878d2315797df1feb9dc1da7 Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Fri, 14 Feb 2025 18:48:47 -0800 Subject: [PATCH 03/12] Add terminal context mention (#1805) * Add terminal context mention * Create fuzzy-moose-punch.md --- .changeset/fuzzy-moose-punch.md | 5 +++ src/core/mentions/index.ts | 12 +++++ .../terminal/get-latest-output.ts | 45 +++++++++++++++++++ src/shared/context-mentions.ts | 6 ++- .../src/components/chat/ChatTextArea.tsx | 3 ++ .../src/components/chat/ContextMenu.tsx | 5 +++ webview-ui/src/utils/context-mentions.ts | 6 ++- 7 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 .changeset/fuzzy-moose-punch.md create mode 100644 src/integrations/terminal/get-latest-output.ts diff --git a/.changeset/fuzzy-moose-punch.md b/.changeset/fuzzy-moose-punch.md new file mode 100644 index 0000000000..7034341375 --- /dev/null +++ b/.changeset/fuzzy-moose-punch.md @@ -0,0 +1,5 @@ +--- +"claude-dev": patch +--- + +Add terminal context mention diff --git a/src/core/mentions/index.ts b/src/core/mentions/index.ts index 1c9c122d1d..ea9e2afd79 100644 --- a/src/core/mentions/index.ts +++ b/src/core/mentions/index.ts @@ -7,6 +7,7 @@ import fs from "fs/promises" import { extractTextFromFile } from "../../integrations/misc/extract-text" import { isBinaryFile } from "isbinaryfile" import { diagnosticsToProblemsString } from "../../integrations/diagnostics" +import { getLatestTerminalOutput } from "../../integrations/terminal/get-latest-output" export function openMention(mention?: string): void { if (!mention) { @@ -28,6 +29,8 @@ export function openMention(mention?: string): void { } } else if (mention === "problems") { vscode.commands.executeCommand("workbench.actions.view.problems") + } else if (mention === "terminal") { + vscode.commands.executeCommand("workbench.action.terminal.focus") } else if (mention.startsWith("http")) { vscode.env.openExternal(vscode.Uri.parse(mention)) } @@ -46,6 +49,8 @@ export async function parseMentions(text: string, cwd: string, urlContentFetcher : `'${mentionPath}' (see below for file content)` } else if (mention === "problems") { return `Workspace Problems (see below for diagnostics)` + } else if (mention === "terminal") { + return `Terminal Output (see below for output)` } return match }) @@ -99,6 +104,13 @@ export async function parseMentions(text: string, cwd: string, urlContentFetcher } catch (error) { parsedText += `\n\n\nError fetching diagnostics: ${error.message}\n` } + } else if (mention === "terminal") { + try { + const terminalOutput = await getLatestTerminalOutput() + parsedText += `\n\n\n${terminalOutput}\n` + } catch (error) { + parsedText += `\n\n\nError fetching terminal output: ${error.message}\n` + } } } diff --git a/src/integrations/terminal/get-latest-output.ts b/src/integrations/terminal/get-latest-output.ts new file mode 100644 index 0000000000..0c869e7fad --- /dev/null +++ b/src/integrations/terminal/get-latest-output.ts @@ -0,0 +1,45 @@ +import * as vscode from "vscode" + +/** + * Gets the contents of the active terminal + * @returns The terminal contents as a string + */ +export async function getLatestTerminalOutput(): Promise { + // Store original clipboard content to restore later + const originalClipboard = await vscode.env.clipboard.readText() + + try { + // Select terminal content + await vscode.commands.executeCommand("workbench.action.terminal.selectAll") + + // Copy selection to clipboard + await vscode.commands.executeCommand("workbench.action.terminal.copySelection") + + // Clear the selection + await vscode.commands.executeCommand("workbench.action.terminal.clearSelection") + + // Get terminal contents from clipboard + let terminalContents = (await vscode.env.clipboard.readText()).trim() + + // Check if there's actually a terminal open + if (terminalContents === originalClipboard) { + return "" + } + + // Clean up command separation + const lines = terminalContents.split("\n") + const lastLine = lines.pop()?.trim() + if (lastLine) { + let i = lines.length - 1 + while (i >= 0 && !lines[i].trim().startsWith(lastLine)) { + i-- + } + terminalContents = lines.slice(Math.max(i, 0)).join("\n") + } + + return terminalContents + } finally { + // Restore original clipboard content + await vscode.env.clipboard.writeText(originalClipboard) + } +} diff --git a/src/shared/context-mentions.ts b/src/shared/context-mentions.ts index 3912868b10..5444b903ef 100644 --- a/src/shared/context-mentions.ts +++ b/src/shared/context-mentions.ts @@ -25,6 +25,9 @@ Mention regex: - `problems\b`: - **Exact Word ('problems')**: Matches the exact word 'problems'. - **Word Boundary (`\b`)**: Ensures that 'problems' is matched as a whole word and not as part of another word (e.g., 'problematic'). + - `terminal\b`: + - **Exact Word ('terminal')**: Matches the exact word 'terminal'. + - **Word Boundary (`\b`)**: Ensures that 'terminal' is matched as a whole word and not as part of another word (e.g., 'terminals'). - `(?=[.,;:!?]?(?=[\s\r\n]|$))`: - **Positive Lookahead (`(?=...)`)**: Ensures that the match is followed by specific patterns without including them in the match. @@ -38,11 +41,12 @@ Mention regex: - Mentions that are file or folder paths starting with '/' and containing any non-whitespace characters (including periods within the path). - URLs that start with a protocol (like 'http://') followed by any non-whitespace characters (including query parameters). - The exact word 'problems'. + - The exact word 'terminal'. - It ensures that any trailing punctuation marks (such as ',', '.', '!', etc.) are not included in the matched mention, allowing the punctuation to follow the mention naturally in the text. - **Global Regex**: - `mentionRegexGlobal`: Creates a global version of the `mentionRegex` to find all matches within a given string. */ -export const mentionRegex = /@((?:\/|\w+:\/\/)[^\s]+?|problems\b)(?=[.,;:!?]?(?=[\s\r\n]|$))/ +export const mentionRegex = /@((?:\/|\w+:\/\/)[^\s]+?|problems\b|terminal\b)(?=[.,;:!?]?(?=[\s\r\n]|$))/ export const mentionRegexGlobal = new RegExp(mentionRegex.source, "g") diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index e86697405f..180332028b 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -243,6 +243,7 @@ const ChatTextArea = forwardRef( const queryItems = useMemo(() => { return [ { type: ContextMenuOptionType.Problems, value: "problems" }, + { type: ContextMenuOptionType.Terminal, value: "terminal" }, ...filePaths .map((file) => "/" + file) .map((path) => ({ @@ -293,6 +294,8 @@ const ChatTextArea = forwardRef( insertValue = value || "" } else if (type === ContextMenuOptionType.Problems) { insertValue = "problems" + } else if (type === ContextMenuOptionType.Terminal) { + insertValue = "terminal" } const { newValue, mentionIndex } = insertMention(textAreaRef.current.value, cursorPosition, insertValue) diff --git a/webview-ui/src/components/chat/ContextMenu.tsx b/webview-ui/src/components/chat/ContextMenu.tsx index 25a7c0902f..8c0b28c513 100644 --- a/webview-ui/src/components/chat/ContextMenu.tsx +++ b/webview-ui/src/components/chat/ContextMenu.tsx @@ -48,6 +48,8 @@ const ContextMenu: React.FC = ({ switch (option.type) { case ContextMenuOptionType.Problems: return Problems + case ContextMenuOptionType.Terminal: + return Terminal case ContextMenuOptionType.URL: return Paste URL to fetch contents case ContextMenuOptionType.NoResults: @@ -85,6 +87,8 @@ const ContextMenu: React.FC = ({ return "folder" case ContextMenuOptionType.Problems: return "warning" + case ContextMenuOptionType.Terminal: + return "terminal" case ContextMenuOptionType.URL: return "link" case ContextMenuOptionType.NoResults: @@ -173,6 +177,7 @@ const ContextMenu: React.FC = ({ /> )} {(option.type === ContextMenuOptionType.Problems || + option.type === ContextMenuOptionType.Terminal || ((option.type === ContextMenuOptionType.File || option.type === ContextMenuOptionType.Folder) && option.value)) && ( Date: Fri, 14 Feb 2025 19:03:11 -0800 Subject: [PATCH 04/12] Add git context mention (#1806) * Add git context mention * Fix context mention highlight * Create long-guests-occur.md --- .changeset/long-guests-occur.md | 5 + src/core/mentions/index.ts | 30 ++- src/core/webview/ClineProvider.ts | 16 ++ src/shared/ExtensionMessage.ts | 3 + src/shared/WebviewMessage.ts | 1 + src/shared/context-mentions.ts | 50 ++--- src/utils/git.ts | 177 ++++++++++++++++++ webview-ui/package-lock.json | 7 + webview-ui/package.json | 1 + .../src/components/chat/ChatTextArea.tsx | 52 ++++- .../src/components/chat/ContextMenu.tsx | 31 ++- webview-ui/src/utils/context-mentions.ts | 100 +++++++++- 12 files changed, 426 insertions(+), 47 deletions(-) create mode 100644 .changeset/long-guests-occur.md create mode 100644 src/utils/git.ts diff --git a/.changeset/long-guests-occur.md b/.changeset/long-guests-occur.md new file mode 100644 index 0000000000..34f6598335 --- /dev/null +++ b/.changeset/long-guests-occur.md @@ -0,0 +1,5 @@ +--- +"claude-dev": patch +--- + +Add git context mention diff --git a/src/core/mentions/index.ts b/src/core/mentions/index.ts index ea9e2afd79..146e823f34 100644 --- a/src/core/mentions/index.ts +++ b/src/core/mentions/index.ts @@ -8,22 +8,24 @@ import { extractTextFromFile } from "../../integrations/misc/extract-text" import { isBinaryFile } from "isbinaryfile" import { diagnosticsToProblemsString } from "../../integrations/diagnostics" import { getLatestTerminalOutput } from "../../integrations/terminal/get-latest-output" +import { getCommitInfo } from "../../utils/git" +import { getWorkingState } from "../../utils/git" export function openMention(mention?: string): void { if (!mention) { return } + const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) + if (!cwd) { + return + } + if (mention.startsWith("/")) { const relPath = mention.slice(1) - const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) - if (!cwd) { - return - } const absPath = path.resolve(cwd, relPath) if (mention.endsWith("/")) { vscode.commands.executeCommand("revealInExplorer", vscode.Uri.file(absPath)) - // vscode.commands.executeCommand("vscode.openFolder", , { forceNewWindow: false }) opens in new window } else { openFile(absPath) } @@ -51,6 +53,10 @@ export async function parseMentions(text: string, cwd: string, urlContentFetcher return `Workspace Problems (see below for diagnostics)` } else if (mention === "terminal") { return `Terminal Output (see below for output)` + } else if (mention === "git-changes") { + return `Working directory changes (see below for details)` + } else if (/^[a-f0-9]{7,40}$/.test(mention)) { + return `Git commit '${mention}' (see below for commit info)` } return match }) @@ -111,6 +117,20 @@ export async function parseMentions(text: string, cwd: string, urlContentFetcher } catch (error) { parsedText += `\n\n\nError fetching terminal output: ${error.message}\n` } + } else if (mention === "git-changes") { + try { + const workingState = await getWorkingState(cwd) + parsedText += `\n\n\n${workingState}\n` + } catch (error) { + parsedText += `\n\n\nError fetching working state: ${error.message}\n` + } + } else if (/^[a-f0-9]{7,40}$/.test(mention)) { + try { + const commitInfo = await getCommitInfo(mention, cwd) + parsedText += `\n\n\n${commitInfo}\n` + } catch (error) { + parsedText += `\n\n\nError fetching commit info: ${error.message}\n` + } } } diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 3e59dfad2a..09a735ae1c 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -28,6 +28,7 @@ import { getUri } from "./getUri" import { AutoApprovalSettings, DEFAULT_AUTO_APPROVAL_SETTINGS } from "../../shared/AutoApprovalSettings" import { BrowserSettings, DEFAULT_BROWSER_SETTINGS } from "../../shared/BrowserSettings" import { ChatSettings, DEFAULT_CHAT_SETTINGS } from "../../shared/ChatSettings" +import { searchCommits } from "../../utils/git" /* https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts @@ -803,6 +804,21 @@ export class ClineProvider implements vscode.WebviewViewProvider { } break } + case "searchCommits": { + const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) + if (cwd) { + try { + const commits = await searchCommits(message.text || "", cwd) + await this.postMessageToWebview({ + type: "commitSearchResults", + commits, + }) + } catch (error) { + console.error(`Error searching commits: ${JSON.stringify(error)}`) + } + } + break + } case "openExtensionSettings": { const settingsFilter = message.text || "" await vscode.commands.executeCommand( diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 70a58fbfd8..6350ed466d 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -1,5 +1,6 @@ // type that represents json data that is sent from extension to webview, called ExtensionMessage and has 'type' enum which can be 'plusButtonClicked' or 'settingsButtonClicked' or 'hello' +import { GitCommit } from "../utils/git" import { ApiConfiguration, ModelInfo } from "./api" import { AutoApprovalSettings } from "./AutoApprovalSettings" import { BrowserSettings } from "./BrowserSettings" @@ -26,6 +27,7 @@ export interface ExtensionMessage { | "vsCodeLmModels" | "requestVsCodeLmModels" | "emailSubscribed" + | "commitSearchResults" text?: string action?: | "chatButtonClicked" @@ -46,6 +48,7 @@ export interface ExtensionMessage { openRouterModels?: Record openAiModels?: string[] mcpServers?: McpServer[] + commits?: GitCommit[] } export type Platform = "aix" | "darwin" | "freebsd" | "linux" | "openbsd" | "sunos" | "win32" | "unknown" diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 447193dd18..9b0f43ee9b 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -43,6 +43,7 @@ export interface WebviewMessage { | "accountLoginClicked" | "accountLogoutClicked" | "subscribeEmail" + | "searchCommits" // | "relaunchChromeDebugMode" text?: string disabled?: boolean diff --git a/src/shared/context-mentions.ts b/src/shared/context-mentions.ts index 5444b903ef..eff8f0a5c0 100644 --- a/src/shared/context-mentions.ts +++ b/src/shared/context-mentions.ts @@ -7,22 +7,22 @@ Mention regex: - **Regex Breakdown**: - `/@`: - - **@**: The mention must start with the '@' symbol. + - **@**: The mention must start with the '@' symbol. - `((?:\/|\w+:\/\/)[^\s]+?|problems\b)`: - - **Capturing Group (`(...)`)**: Captures the part of the string that matches one of the specified patterns. - - `(?:\/|\w+:\/\/)`: - - **Non-Capturing Group (`(?:...)`)**: Groups the alternatives without capturing them for back-referencing. - - `\/`: - - **Slash (`/`)**: Indicates that the mention is a file or folder path starting with a '/'. - - `|`: Logical OR. - - `\w+:\/\/`: - - **Protocol (`\w+://`)**: Matches URLs that start with a word character sequence followed by '://', such as 'http://', 'https://', 'ftp://', etc. - - `[^\s]+?`: - - **Non-Whitespace Characters (`[^\s]+`)**: Matches one or more characters that are not whitespace. - - **Non-Greedy (`+?`)**: Ensures the smallest possible match, preventing the inclusion of trailing punctuation. - - `|`: Logical OR. - - `problems\b`: + - **Capturing Group (`(...)`)**: Captures the part of the string that matches one of the specified patterns. + - `(?:\/|\w+:\/\/)`: + - **Non-Capturing Group (`(?:...)`)**: Groups the alternatives without capturing them for back-referencing. + - `\/`: + - **Slash (`/`)**: Indicates that the mention is a file or folder path starting with a '/'. + - `|`: Logical OR. + - `\w+:\/\/`: + - **Protocol (`\w+://`)**: Matches URLs that start with a word character sequence followed by '://', such as 'http://', 'https://', 'ftp://', etc. + - `[^\s]+?`: + - **Non-Whitespace Characters (`[^\s]+`)**: Matches one or more characters that are not whitespace. + - **Non-Greedy (`+?`)**: Ensures the smallest possible match, preventing the inclusion of trailing punctuation. + - `|`: Logical OR. + - `problems\b`: - **Exact Word ('problems')**: Matches the exact word 'problems'. - **Word Boundary (`\b`)**: Ensures that 'problems' is matched as a whole word and not as part of another word (e.g., 'problematic'). - `terminal\b`: @@ -30,23 +30,25 @@ Mention regex: - **Word Boundary (`\b`)**: Ensures that 'terminal' is matched as a whole word and not as part of another word (e.g., 'terminals'). - `(?=[.,;:!?]?(?=[\s\r\n]|$))`: - - **Positive Lookahead (`(?=...)`)**: Ensures that the match is followed by specific patterns without including them in the match. - - `[.,;:!?]?`: - - **Optional Punctuation (`[.,;:!?]?`)**: Matches zero or one of the specified punctuation marks. - - `(?=[\s\r\n]|$)`: - - **Nested Positive Lookahead (`(?=[\s\r\n]|$)`)**: Ensures that the punctuation (if present) is followed by a whitespace character, a line break, or the end of the string. + - **Positive Lookahead (`(?=...)`)**: Ensures that the match is followed by specific patterns without including them in the match. + - `[.,;:!?]?`: + - **Optional Punctuation (`[.,;:!?]?`)**: Matches zero or one of the specified punctuation marks. + - `(?=[\s\r\n]|$)`: + - **Nested Positive Lookahead (`(?=[\s\r\n]|$)`)**: Ensures that the punctuation (if present) is followed by a whitespace character, a line break, or the end of the string. - **Summary**: - The regex effectively matches: - - Mentions that are file or folder paths starting with '/' and containing any non-whitespace characters (including periods within the path). - - URLs that start with a protocol (like 'http://') followed by any non-whitespace characters (including query parameters). - - The exact word 'problems'. - - The exact word 'terminal'. + - Mentions that are file or folder paths starting with '/' and containing any non-whitespace characters (including periods within the path). + - URLs that start with a protocol (like 'http://') followed by any non-whitespace characters (including query parameters). + - The exact word 'problems'. + - The exact word 'terminal'. + - The exact word 'git-changes'. - It ensures that any trailing punctuation marks (such as ',', '.', '!', etc.) are not included in the matched mention, allowing the punctuation to follow the mention naturally in the text. - **Global Regex**: - `mentionRegexGlobal`: Creates a global version of the `mentionRegex` to find all matches within a given string. */ -export const mentionRegex = /@((?:\/|\w+:\/\/)[^\s]+?|problems\b|terminal\b)(?=[.,;:!?]?(?=[\s\r\n]|$))/ +export const mentionRegex = + /@((?:\/|\w+:\/\/)[^\s]+?|[a-f0-9]{7,40}\b|problems\b|terminal\b|git-changes\b)(?=[.,;:!?]?(?=[\s\r\n]|$))/ export const mentionRegexGlobal = new RegExp(mentionRegex.source, "g") diff --git a/src/utils/git.ts b/src/utils/git.ts new file mode 100644 index 0000000000..7ab6d67a07 --- /dev/null +++ b/src/utils/git.ts @@ -0,0 +1,177 @@ +import { exec } from "child_process" +import { promisify } from "util" + +const execAsync = promisify(exec) +const GIT_OUTPUT_LINE_LIMIT = 500 + +export interface GitCommit { + hash: string + shortHash: string + subject: string + author: string + date: string +} + +async function checkGitRepo(cwd: string): Promise { + try { + await execAsync("git rev-parse --git-dir", { cwd }) + return true + } catch (error) { + return false + } +} + +async function checkGitInstalled(): Promise { + try { + await execAsync("git --version") + return true + } catch (error) { + return false + } +} + +export async function searchCommits(query: string, cwd: string): Promise { + try { + const isInstalled = await checkGitInstalled() + if (!isInstalled) { + console.error("Git is not installed") + return [] + } + + const isRepo = await checkGitRepo(cwd) + if (!isRepo) { + console.error("Not a git repository") + return [] + } + + // Search commits by hash or message, limiting to 10 results + const { stdout } = await execAsync( + `git log -n 10 --format="%H%n%h%n%s%n%an%n%ad" --date=short ` + `--grep="${query}" --regexp-ignore-case`, + { cwd }, + ) + + let output = stdout + if (!output.trim() && /^[a-f0-9]+$/i.test(query)) { + // If no results from grep search and query looks like a hash, try searching by hash + const { stdout: hashStdout } = await execAsync( + `git log -n 10 --format="%H%n%h%n%s%n%an%n%ad" --date=short ` + `--author-date-order ${query}`, + { cwd }, + ).catch(() => ({ stdout: "" })) + + if (!hashStdout.trim()) { + return [] + } + + output = hashStdout + } + + const commits: GitCommit[] = [] + const lines = output + .trim() + .split("\n") + .filter((line) => line !== "--") + + for (let i = 0; i < lines.length; i += 5) { + commits.push({ + hash: lines[i], + shortHash: lines[i + 1], + subject: lines[i + 2], + author: lines[i + 3], + date: lines[i + 4], + }) + } + + return commits + } catch (error) { + console.error("Error searching commits:", error) + return [] + } +} + +export async function getCommitInfo(hash: string, cwd: string): Promise { + try { + const isInstalled = await checkGitInstalled() + if (!isInstalled) { + return "Git is not installed" + } + + const isRepo = await checkGitRepo(cwd) + if (!isRepo) { + return "Not a git repository" + } + + // Get commit info, stats, and diff separately + const { stdout: info } = await execAsync(`git show --format="%H%n%h%n%s%n%an%n%ad%n%b" --no-patch ${hash}`, { + cwd, + }) + const [fullHash, shortHash, subject, author, date, body] = info.trim().split("\n") + + const { stdout: stats } = await execAsync(`git show --stat --format="" ${hash}`, { cwd }) + + const { stdout: diff } = await execAsync(`git show --format="" ${hash}`, { cwd }) + + const summary = [ + `Commit: ${shortHash} (${fullHash})`, + `Author: ${author}`, + `Date: ${date}`, + `\nMessage: ${subject}`, + body ? `\nDescription:\n${body}` : "", + "\nFiles Changed:", + stats.trim(), + "\nFull Changes:", + ].join("\n") + + const output = summary + "\n\n" + diff.trim() + return truncateOutput(output) + } catch (error) { + console.error("Error getting commit info:", error) + return `Failed to get commit info: ${error instanceof Error ? error.message : String(error)}` + } +} + +export async function getWorkingState(cwd: string): Promise { + try { + const isInstalled = await checkGitInstalled() + if (!isInstalled) { + return "Git is not installed" + } + + const isRepo = await checkGitRepo(cwd) + if (!isRepo) { + return "Not a git repository" + } + + // Get status of working directory + const { stdout: status } = await execAsync("git status --short", { cwd }) + if (!status.trim()) { + return "No changes in working directory" + } + + // Get all changes (both staged and unstaged) compared to HEAD + const { stdout: diff } = await execAsync("git diff HEAD", { cwd }) + const output = `Working directory changes:\n\n${status}\n\n${diff}`.trim() + return truncateOutput(output) + } catch (error) { + console.error("Error getting working state:", error) + return `Failed to get working state: ${error instanceof Error ? error.message : String(error)}` + } +} + +function truncateOutput(content: string): string { + if (!GIT_OUTPUT_LINE_LIMIT) { + return content + } + + const lines = content.split("\n") + if (lines.length <= GIT_OUTPUT_LINE_LIMIT) { + return content + } + + const beforeLimit = Math.floor(GIT_OUTPUT_LINE_LIMIT * 0.2) // 20% of lines before + const afterLimit = GIT_OUTPUT_LINE_LIMIT - beforeLimit // remaining 80% after + return [ + ...lines.slice(0, beforeLimit), + `\n[...${lines.length - GIT_OUTPUT_LINE_LIMIT} lines omitted...]\n`, + ...lines.slice(-afterLimit), + ].join("\n") +} diff --git a/webview-ui/package-lock.json b/webview-ui/package-lock.json index 4a06dc2e45..e1015aca29 100644 --- a/webview-ui/package-lock.json +++ b/webview-ui/package-lock.json @@ -13,6 +13,7 @@ "debounce": "^2.1.1", "fast-deep-equal": "^3.1.3", "fuse.js": "^7.0.0", + "fzf": "^0.5.2", "pretty-bytes": "^6.1.1", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -9825,6 +9826,12 @@ "node": ">=10" } }, + "node_modules/fzf": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fzf/-/fzf-0.5.2.tgz", + "integrity": "sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==", + "license": "BSD-3-Clause" + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", diff --git a/webview-ui/package.json b/webview-ui/package.json index 2f023747d1..8f15b44187 100644 --- a/webview-ui/package.json +++ b/webview-ui/package.json @@ -8,6 +8,7 @@ "debounce": "^2.1.1", "fast-deep-equal": "^3.1.3", "fuse.js": "^7.0.0", + "fzf": "^0.5.2", "pretty-bytes": "^6.1.1", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index 180332028b..2e642f9a46 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -1,9 +1,10 @@ import { VSCodeButton } from "@vscode/webview-ui-toolkit/react" import React, { forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react" import DynamicTextArea from "react-textarea-autosize" -import { useClickAway, useWindowSize } from "react-use" +import { useClickAway, useEvent, useWindowSize } from "react-use" import styled from "styled-components" import { mentionRegex, mentionRegexGlobal } from "../../../../src/shared/context-mentions" +import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage" import { useExtensionState } from "../../context/ExtensionStateContext" import { ContextMenuOptionType, @@ -12,16 +13,15 @@ import { removeMention, shouldShowContextMenu, } from "../../utils/context-mentions" +import { useMetaKeyDetection, useShortcut } from "../../utils/hooks" import { validateApiConfiguration, validateModelId } from "../../utils/validate" import { vscode } from "../../utils/vscode" import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock" import Thumbnails from "../common/Thumbnails" +import Tooltip from "../common/Tooltip" import ApiOptions, { normalizeApiConfiguration } from "../settings/ApiOptions" import { MAX_IMAGES_PER_MESSAGE } from "./ChatView" import ContextMenu from "./ContextMenu" -import { useShortcut } from "../../utils/hooks" -import Tooltip from "../common/Tooltip" -import { useMetaKeyDetection } from "../../utils/hooks" interface ChatTextAreaProps { inputValue: string @@ -215,6 +215,8 @@ const ChatTextArea = forwardRef( ) => { const { filePaths, chatSettings, apiConfiguration, openRouterModels, platform } = useExtensionState() const [isTextAreaFocused, setIsTextAreaFocused] = useState(false) + const [gitCommits, setGitCommits] = useState([]) + const [thumbnailsHeight, setThumbnailsHeight] = useState(0) const [textAreaBaseHeight, setTextAreaBaseHeight] = useState(undefined) const [showContextMenu, setShowContextMenu] = useState(false) @@ -240,10 +242,40 @@ const ChatTextArea = forwardRef( // Add a ref to track previous menu state const prevShowModelSelector = useRef(showModelSelector) + // Fetch git commits when Git is selected or when typing a hash + useEffect(() => { + if (selectedType === ContextMenuOptionType.Git || /^[a-f0-9]+$/i.test(searchQuery)) { + vscode.postMessage({ + type: "searchCommits", + text: searchQuery || "", + }) + } + }, [selectedType, searchQuery]) + + const handleMessage = useCallback((event: MessageEvent) => { + const message: ExtensionMessage = event.data + switch (message.type) { + case "commitSearchResults": { + const commits = + message.commits?.map((commit: any) => ({ + type: ContextMenuOptionType.Git, + value: commit.hash, + label: commit.subject, + description: `${commit.shortHash} by ${commit.author} on ${commit.date}`, + })) || [] + setGitCommits(commits) + break + } + } + }, []) + + useEvent("message", handleMessage) + const queryItems = useMemo(() => { return [ { type: ContextMenuOptionType.Problems, value: "problems" }, { type: ContextMenuOptionType.Terminal, value: "terminal" }, + ...gitCommits, ...filePaths .map((file) => "/" + file) .map((path) => ({ @@ -251,7 +283,7 @@ const ChatTextArea = forwardRef( value: path, })), ] - }, [filePaths]) + }, [filePaths, gitCommits]) useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -275,7 +307,11 @@ const ChatTextArea = forwardRef( return } - if (type === ContextMenuOptionType.File || type === ContextMenuOptionType.Folder) { + if ( + type === ContextMenuOptionType.File || + type === ContextMenuOptionType.Folder || + type === ContextMenuOptionType.Git + ) { if (!value) { setSelectedType(type) setSearchQuery("") @@ -296,6 +332,8 @@ const ChatTextArea = forwardRef( insertValue = "problems" } else if (type === ContextMenuOptionType.Terminal) { insertValue = "terminal" + } else if (type === ContextMenuOptionType.Git) { + insertValue = value || "" } const { newValue, mentionIndex } = insertMention(textAreaRef.current.value, cursorPosition, insertValue) @@ -898,7 +936,7 @@ const ChatTextArea = forwardRef( borderTop: 0, borderColor: "transparent", borderBottom: `${thumbnailsHeight + 6}px solid transparent`, - padding: "9px 49px 3px 9px", + padding: "9px 28px 3px 9px", }} /> = ({ return Paste URL to fetch contents case ContextMenuOptionType.NoResults: return No results found + case ContextMenuOptionType.Git: + if (option.value) { + return ( +
+ {option.label} + + {option.description} + +
+ ) + } else { + return Git Commits + } case ContextMenuOptionType.File: case ContextMenuOptionType.Folder: if (option.value) { @@ -91,6 +112,8 @@ const ContextMenu: React.FC = ({ return "terminal" case ContextMenuOptionType.URL: return "link" + case ContextMenuOptionType.Git: + return "git-commit" case ContextMenuOptionType.NoResults: return "info" default: @@ -165,7 +188,9 @@ const ContextMenu: React.FC = ({ /> {renderOptionContent(option)} - {(option.type === ContextMenuOptionType.File || option.type === ContextMenuOptionType.Folder) && + {(option.type === ContextMenuOptionType.File || + option.type === ContextMenuOptionType.Folder || + option.type === ContextMenuOptionType.Git) && !option.value && ( = ({ )} {(option.type === ContextMenuOptionType.Problems || option.type === ContextMenuOptionType.Terminal || - ((option.type === ContextMenuOptionType.File || option.type === ContextMenuOptionType.Folder) && + ((option.type === ContextMenuOptionType.File || + option.type === ContextMenuOptionType.Folder || + option.type === ContextMenuOptionType.Git) && option.value)) && ( 0 ? folders : [{ type: ContextMenuOptionType.NoResults }] } + if (selectedType === ContextMenuOptionType.Git) { + const commits = queryItems.filter((item) => item.type === ContextMenuOptionType.Git) + return commits.length > 0 ? [workingChanges, ...commits] : [workingChanges] + } + return [ { type: ContextMenuOptionType.URL }, { type: ContextMenuOptionType.Problems }, { type: ContextMenuOptionType.Terminal }, + { type: ContextMenuOptionType.Git }, { type: ContextMenuOptionType.Folder }, { type: ContextMenuOptionType.File }, ] } const lowerQuery = query.toLowerCase() + const suggestions: ContextMenuQueryItem[] = [] + // Check for top-level option matches + if ("git".startsWith(lowerQuery)) { + suggestions.push({ + type: ContextMenuOptionType.Git, + label: "Git Commits", + description: "Search repository history", + }) + } else if ("git-changes".startsWith(lowerQuery)) { + suggestions.push(workingChanges) + } + if ("problems".startsWith(lowerQuery)) { + suggestions.push({ type: ContextMenuOptionType.Problems }) + } if (query.startsWith("http")) { - return [{ type: ContextMenuOptionType.URL, value: query }] - } else { - const matchingItems = queryItems.filter((item) => item.value?.toLowerCase().includes(lowerQuery)) + suggestions.push({ type: ContextMenuOptionType.URL, value: query }) + } - if (matchingItems.length > 0) { - return matchingItems.map((item) => ({ - type: item.type, - value: item.value, - })) + // Add exact SHA matches to suggestions + if (/^[a-f0-9]{7,40}$/i.test(lowerQuery)) { + const exactMatches = queryItems.filter( + (item) => item.type === ContextMenuOptionType.Git && item.value?.toLowerCase() === lowerQuery, + ) + if (exactMatches.length > 0) { + suggestions.push(...exactMatches) } else { - return [{ type: ContextMenuOptionType.NoResults }] + // If no exact match but valid SHA format, add as option + suggestions.push({ + type: ContextMenuOptionType.Git, + value: lowerQuery, + label: `Commit ${lowerQuery}`, + description: "Git commit hash", + }) } } + + // Create searchable strings array for fzf + const searchableItems = queryItems.map((item) => ({ + original: item, + searchStr: [item.value, item.label, item.description].filter(Boolean).join(" "), + })) + + // Initialize fzf instance for fuzzy search + const fzf = new Fzf(searchableItems, { + selector: (item) => item.searchStr, + }) + + // Get fuzzy matching items + const matchingItems = query ? fzf.find(query).map((result) => result.item.original) : [] + + // Separate matches by type + const fileMatches = matchingItems.filter( + (item) => item.type === ContextMenuOptionType.File || item.type === ContextMenuOptionType.Folder, + ) + const gitMatches = matchingItems.filter((item) => item.type === ContextMenuOptionType.Git) + const otherMatches = matchingItems.filter( + (item) => + item.type !== ContextMenuOptionType.File && + item.type !== ContextMenuOptionType.Folder && + item.type !== ContextMenuOptionType.Git, + ) + + // Combine suggestions with matching items in the desired order + if (suggestions.length > 0 || matchingItems.length > 0) { + const allItems = [...suggestions, ...fileMatches, ...gitMatches, ...otherMatches] + + // Remove duplicates based on type and value + const seen = new Set() + const deduped = allItems.filter((item) => { + const key = `${item.type}-${item.value}` + if (seen.has(key)) return false + seen.add(key) + return true + }) + + return deduped + } + + return [{ type: ContextMenuOptionType.NoResults }] } export function shouldShowContextMenu(text: string, position: number): boolean { From a6e812824c77b0332abbbe34a5d82b2c5873f6ad Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Sat, 15 Feb 2025 23:42:12 -0800 Subject: [PATCH 05/12] Give attribution to Szpadel for reasoning update --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1146e374ae..549d4ff9f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,7 @@ ## [3.2.10] -- Improve support for DeepSeek-R1 (deepseek-reasoner) model for OpenRouter, OpenAI-compatible, and DeepSeek direct +- Improve support for DeepSeek-R1 (deepseek-reasoner) model for OpenRouter, OpenAI-compatible, and DeepSeek direct (thanks @Szpadel!) - Show Reasoning tokens for models that support it - Fix issues with switching models between Plan/Act modes From 3873908efbacd94cfb7951e5953bcdb587f0bfb8 Mon Sep 17 00:00:00 2001 From: Evan <58194240+celestial-vault@users.noreply.github.com> Date: Sun, 16 Feb 2025 14:20:27 -0800 Subject: [PATCH 06/12] Webpack Single Bundle (#1821) * formatting * changeset --------- Co-authored-by: Evan Fannin --- .changeset/smart-cycles-promise.md | 5 +++++ webview-ui/scripts/build-react-no-split.js | 26 ++++++++++++++-------- 2 files changed, 22 insertions(+), 9 deletions(-) create mode 100644 .changeset/smart-cycles-promise.md diff --git a/.changeset/smart-cycles-promise.md b/.changeset/smart-cycles-promise.md new file mode 100644 index 0000000000..f3df1948bb --- /dev/null +++ b/.changeset/smart-cycles-promise.md @@ -0,0 +1,5 @@ +--- +"claude-dev": patch +--- + +Force all JS code into a single bundle for better VS Code compatability diff --git a/webview-ui/scripts/build-react-no-split.js b/webview-ui/scripts/build-react-no-split.js index f021a12104..28f37108be 100644 --- a/webview-ui/scripts/build-react-no-split.js +++ b/webview-ui/scripts/build-react-no-split.js @@ -98,18 +98,26 @@ config.module.rules[1].oneOf.forEach((rule) => { } }) -// Disable code splitting -config.optimization.splitChunks = { - cacheGroups: { - default: false, +// Force all code into a single bundle for VS Code webview compatibility. +// This is necessary for: +// 1. Mermaid.js to work properly (prevents async chunk loading) +// 2. Consistent CSP nonce handling (single bundle = single nonce) +config.optimization = { + ...config.optimization, + splitChunks: { + cacheGroups: { + default: false, + }, + name: "main", // Forces all chunks (dynamic import() calls, for example those used by Mermaid) into one bundle - this is what actually prevents code splitting }, + runtimeChunk: false, } -// Disable code chunks -config.optimization.runtimeChunk = false - -// Rename main.{hash}.js to main.js -config.output.filename = "static/js/[name].js" +// Ensure all chunks are named 'main' to match our CSP nonce setup +config.output = { + ...config.output, + filename: "static/js/[name].js", +} // Rename main.{hash}.css to main.css config.plugins[5].options.filename = "static/css/[name].css" From 796eac558caf0408970a8902885fc76729529ff4 Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Sun, 16 Feb 2025 15:15:27 -0800 Subject: [PATCH 07/12] Fix sending message when toggling plan/act mode --- src/core/Cline.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 1e9ea6bbb4..3632f25c1b 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -2716,9 +2716,6 @@ export class Cline { this.isAwaitingPlanResponse = false if (this.didRespondToPlanAskBySwitchingMode) { - if (text) { - await this.say("user_feedback", text ?? "", images) - } pushToolResult( formatResponse.toolResult( `[The user has switched to ACT MODE, so you may now proceed with the task.]` + @@ -2728,6 +2725,13 @@ export class Cline { images, ), ) + } else { + // if we didn't switch to ACT MODE, then we can just send the user_feedback message + pushToolResult(formatResponse.toolResult(`\n${text}\n`, images)) + } + + if (text || images) { + await this.say("user_feedback", text ?? "", images) } // From aab26e9a386d5465bf4d833d29e66e34776d5be2 Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Sun, 16 Feb 2025 15:38:06 -0800 Subject: [PATCH 08/12] Fix empty message bubble after switching plan/act with a message --- src/core/Cline.ts | 9 +++++++-- src/core/webview/ClineProvider.ts | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 3632f25c1b..4bfcf59520 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -2712,9 +2712,14 @@ export class Cline { // } this.isAwaitingPlanResponse = true - const { text, images } = await this.ask("plan_mode_response", response, false) + let { text, images } = await this.ask("plan_mode_response", response, false) this.isAwaitingPlanResponse = false + // webview invoke sendMessage will send this marker in order to put webview into the proper state (responding to an ask) and as a flag to extension that the user switched to ACT mode. + if (text === "PLAN_MODE_TOGGLE_RESPONSE") { + text = "" + } + if (this.didRespondToPlanAskBySwitchingMode) { pushToolResult( formatResponse.toolResult( @@ -2730,7 +2735,7 @@ export class Cline { pushToolResult(formatResponse.toolResult(`\n${text}\n`, images)) } - if (text || images) { + if (text || images?.length) { await this.say("user_feedback", text ?? "", images) } diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 09a735ae1c..8ef786f4dc 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -627,7 +627,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.postMessageToWebview({ type: "invoke", invoke: "sendMessage", - text: message.chatContent?.message || "[Proceeding with the task...]", + text: message.chatContent?.message || "PLAN_MODE_TOGGLE_RESPONSE", images: message.chatContent?.images, }) } else { From a98269f6d5031db212dee9303badf771b57d999c Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Sun, 16 Feb 2025 20:38:31 -0800 Subject: [PATCH 09/12] Fix sending feedback with approve --- src/core/Cline.ts | 83 +++++++++++++++++------------------ src/core/prompts/responses.ts | 6 --- 2 files changed, 40 insertions(+), 49 deletions(-) diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 4bfcf59520..acecbd4fe6 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -1539,33 +1539,44 @@ export class Cline { this.didAlreadyUseTool = true } + // The user can approve, reject, or provide feedback (rejection). However the user may also send a message along with an approval, in which case we add a separate user message with this feedback. + const pushAdditionalToolFeedback = (feedback?: string, images?: string[]) => { + if (!feedback && !images) { + return + } + const content = formatResponse.toolResult( + `The user provided the following feedback:\n\n${feedback}\n`, + images, + ) + if (typeof content === "string") { + this.userMessageContent.push({ + type: "text", + text: content, + }) + } else { + this.userMessageContent.push(...content) + } + } + const askApproval = async (type: ClineAsk, partialMessage?: string) => { const { response, text, images } = await this.ask(type, partialMessage, false) if (response !== "yesButtonClicked") { - // User did NOT approve (rejected) - if (response === "messageResponse") { - // Rejection WITH feedback - await this.say("user_feedback", text, images) - pushToolResult(formatResponse.toolResult(formatResponse.toolDeniedWithFeedback(text), images)) - - this.didRejectTool = true - return false - } - // Rejection WITHOUT explicit feedback + // User pressed reject button or responded with a message, which we treat as a rejection pushToolResult(formatResponse.toolDenied()) - + if (text || images?.length) { + pushAdditionalToolFeedback(text, images) + await this.say("user_feedback", text, images) + } this.didRejectTool = true // Prevent further tool uses in this message return false + } else { + // User hit the approve button, and may have provided feedback + if (text || images?.length) { + pushAdditionalToolFeedback(text, images) + await this.say("user_feedback", text, images) + } + return true } - - // Handle yesButtonClicked with text (Acceptance WITH feedback) - if (text) { - await this.say("user_feedback", text, images) - pushToolResult(formatResponse.toolResult(formatResponse.toolApprovedWithFeedback(text), images)) // Structured feedback to model on approval - } - - // User approved without feedback - return true } const showNotificationForApprovalIfAutoApprovalEnabled = (message: string) => { @@ -1804,37 +1815,23 @@ export class Cline { let didApprove = true const { response, text, images } = await this.ask("tool", completeMessage, false) if (response !== "yesButtonClicked") { - // User did NOT approve (rejected) - + // User either sent a message or pressed reject button // TODO: add similar context for other tool denial responses, to emphasize ie that a command was not run const fileDeniedNote = fileExists ? "The file was not updated, and maintains its original contents." : "The file was not created." - if (response === "messageResponse") { - // Rejection WITH feedback + pushToolResult(`The user denied this operation. ${fileDeniedNote}`) + if (text || images?.length) { + pushAdditionalToolFeedback(text, images) await this.say("user_feedback", text, images) - pushToolResult( - formatResponse.toolResult( - `The user denied this operation. ${fileDeniedNote}\nThe user provided the following feedback:\n\n${text}\n`, - images, - ), - ) - this.didRejectTool = true - didApprove = false - } else { - pushToolResult(`The user denied this operation. ${fileDeniedNote}`) - this.didRejectTool = true - didApprove = false } + this.didRejectTool = true + didApprove = false } else { - // User approved - - // Handle yesButtonClicked with text (Acceptance WITH feedback) - if (text) { + // User hit the approve button, and may have provided feedback + if (text || images?.length) { + pushAdditionalToolFeedback(text, images) await this.say("user_feedback", text, images) - pushToolResult( - formatResponse.toolResult(formatResponse.toolApprovedWithFeedback(text), images), - ) } } diff --git a/src/core/prompts/responses.ts b/src/core/prompts/responses.ts index 8a14acc02b..bf5db6bbe7 100644 --- a/src/core/prompts/responses.ts +++ b/src/core/prompts/responses.ts @@ -6,12 +6,6 @@ import { ClineIgnoreController, LOCK_TEXT_SYMBOL } from "../ignore/ClineIgnoreCo export const formatResponse = { toolDenied: () => `The user denied this operation.`, - toolDeniedWithFeedback: (feedback?: string) => - `The user denied this operation and provided the following feedback:\n\n${feedback}\n`, - - toolApprovedWithFeedback: (feedback?: string) => - `The user approved this operation and provided the following feedback:\n\n${feedback}\n`, - toolError: (error?: string) => `The tool execution failed with the following error:\n\n${error}\n`, clineIgnoreError: (path: string) => From 217162ccba404a87fc780f4586fdab3ed872a2ce Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Sun, 16 Feb 2025 21:01:52 -0800 Subject: [PATCH 10/12] Prepare for release --- .changeset/angry-lions-sneeze.md | 5 ----- .changeset/five-flies-breathe.md | 5 ----- .changeset/fuzzy-moose-punch.md | 5 ----- .changeset/gentle-glasses-flow.md | 5 ----- .changeset/long-guests-occur.md | 5 ----- .changeset/long-masks-notice.md | 5 ----- .changeset/orange-eels-unite.md | 5 ----- .changeset/poor-cobras-destroy.md | 5 ----- .changeset/silly-cats-appear.md | 5 ----- .changeset/slimy-roses-dance.md | 5 ----- .changeset/slimy-toes-wait.md | 5 ----- .changeset/smart-cycles-promise.md | 5 ----- .changeset/stale-lizards-poke.md | 5 ----- .changeset/ten-books-act.md | 5 ----- .changeset/thirty-eyes-appear.md | 5 ----- .changeset/wise-phones-mate.md | 5 ----- CHANGELOG.md | 13 ++++++++++++- README.md | 2 +- package.json | 2 +- 19 files changed, 14 insertions(+), 83 deletions(-) delete mode 100644 .changeset/angry-lions-sneeze.md delete mode 100644 .changeset/five-flies-breathe.md delete mode 100644 .changeset/fuzzy-moose-punch.md delete mode 100644 .changeset/gentle-glasses-flow.md delete mode 100644 .changeset/long-guests-occur.md delete mode 100644 .changeset/long-masks-notice.md delete mode 100644 .changeset/orange-eels-unite.md delete mode 100644 .changeset/poor-cobras-destroy.md delete mode 100644 .changeset/silly-cats-appear.md delete mode 100644 .changeset/slimy-roses-dance.md delete mode 100644 .changeset/slimy-toes-wait.md delete mode 100644 .changeset/smart-cycles-promise.md delete mode 100644 .changeset/stale-lizards-poke.md delete mode 100644 .changeset/ten-books-act.md delete mode 100644 .changeset/thirty-eyes-appear.md delete mode 100644 .changeset/wise-phones-mate.md diff --git a/.changeset/angry-lions-sneeze.md b/.changeset/angry-lions-sneeze.md deleted file mode 100644 index 091739140b..0000000000 --- a/.changeset/angry-lions-sneeze.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"claude-dev": patch ---- - -Add translation to CODE_OF_CONDUCT, CONTRIBUTING and README to portuguese pt-BR. diff --git a/.changeset/five-flies-breathe.md b/.changeset/five-flies-breathe.md deleted file mode 100644 index aa13a9b71f..0000000000 --- a/.changeset/five-flies-breathe.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"claude-dev": patch ---- - -Add support for qwen vl models diff --git a/.changeset/fuzzy-moose-punch.md b/.changeset/fuzzy-moose-punch.md deleted file mode 100644 index 7034341375..0000000000 --- a/.changeset/fuzzy-moose-punch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"claude-dev": patch ---- - -Add terminal context mention diff --git a/.changeset/gentle-glasses-flow.md b/.changeset/gentle-glasses-flow.md deleted file mode 100644 index c3d9ed997c..0000000000 --- a/.changeset/gentle-glasses-flow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"claude-dev": minor ---- - -add alibaba qwen2.5 coder models diff --git a/.changeset/long-guests-occur.md b/.changeset/long-guests-occur.md deleted file mode 100644 index 34f6598335..0000000000 --- a/.changeset/long-guests-occur.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"claude-dev": patch ---- - -Add git context mention diff --git a/.changeset/long-masks-notice.md b/.changeset/long-masks-notice.md deleted file mode 100644 index 76079fbed6..0000000000 --- a/.changeset/long-masks-notice.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"claude-dev": minor ---- - -Advanced Setting to disable browser tool diff --git a/.changeset/orange-eels-unite.md b/.changeset/orange-eels-unite.md deleted file mode 100644 index e0eab5d394..0000000000 --- a/.changeset/orange-eels-unite.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"claude-dev": patch ---- - -Fix AWS Bedrock Profiles. When configuring the AnthropicBedrock Client you must pass AWS credentials in a specific way, otherwise the client will default to reading credentials from the default AWS profile. diff --git a/.changeset/poor-cobras-destroy.md b/.changeset/poor-cobras-destroy.md deleted file mode 100644 index 991c7324cc..0000000000 --- a/.changeset/poor-cobras-destroy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"claude-dev": minor ---- - -Add extension setting for chromium executable path diff --git a/.changeset/silly-cats-appear.md b/.changeset/silly-cats-appear.md deleted file mode 100644 index 6e6925a4c9..0000000000 --- a/.changeset/silly-cats-appear.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"claude-dev": minor ---- - -Add new ability to send message that is in Input field during Plan/Act Mode Change to Act. diff --git a/.changeset/slimy-roses-dance.md b/.changeset/slimy-roses-dance.md deleted file mode 100644 index e88ef3ca57..0000000000 --- a/.changeset/slimy-roses-dance.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"claude-dev": minor ---- - -add alibaba qwen2.5-coder models diff --git a/.changeset/slimy-toes-wait.md b/.changeset/slimy-toes-wait.md deleted file mode 100644 index 059978604f..0000000000 --- a/.changeset/slimy-toes-wait.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"claude-dev": minor ---- - -Added api key field for litellm api provider in settings diff --git a/.changeset/smart-cycles-promise.md b/.changeset/smart-cycles-promise.md deleted file mode 100644 index f3df1948bb..0000000000 --- a/.changeset/smart-cycles-promise.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"claude-dev": patch ---- - -Force all JS code into a single bundle for better VS Code compatability diff --git a/.changeset/stale-lizards-poke.md b/.changeset/stale-lizards-poke.md deleted file mode 100644 index 40e3308e8b..0000000000 --- a/.changeset/stale-lizards-poke.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"claude-dev": minor ---- - -qwen platform adds deepseek-r1/v3 support diff --git a/.changeset/ten-books-act.md b/.changeset/ten-books-act.md deleted file mode 100644 index f37fcf0e9a..0000000000 --- a/.changeset/ten-books-act.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"claude-dev": minor ---- - -Adding .clineignore guide diff --git a/.changeset/thirty-eyes-appear.md b/.changeset/thirty-eyes-appear.md deleted file mode 100644 index 2cfb8405d6..0000000000 --- a/.changeset/thirty-eyes-appear.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"claude-dev": minor ---- - -Advanced Configuration for OpenAI Compatible Providers diff --git a/.changeset/wise-phones-mate.md b/.changeset/wise-phones-mate.md deleted file mode 100644 index 7be57b25e9..0000000000 --- a/.changeset/wise-phones-mate.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"claude-dev": minor ---- - -Allowing the user to give feedback when approving a tool use. diff --git a/CHANGELOG.md b/CHANGELOG.md index 549d4ff9f3..0e9ef115f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,20 @@ # Changelog +## [3.4.0] + +- Send current textfield contents as additional feedback when toggling from Plan to Act Mode, or when hitting 'Approve' button +- Add 'Terminal' context mention to reference the active terminal's contents +- Add 'Git Commits' context mention to reference current working changes or specific commits (thanks @mrubens!) +- Add advanced configuration options for OpenAI Compatible (context window, max output, pricing, etc.) +- Add Alibaba Qwen 2.5 coder models, VL models, and DeepSeek-R1/V3 support +- Improve support for AWS Bedrock Profiles +- Add advanced setting to disable browser tool +- Add advanced setting to set chromium executable path for browser tool + ## [3.3.2] - Fix bug where OpenRouter requests would periodically not return cost/token stats, leading to context window limit errors -- Make checkpoints more visible and keep track of restored checkpoints +- Make checkpoints more visible and keep track of restored checkpoints ## [3.3.0] diff --git a/README.md b/README.md index f1c48eebfd..4f48c5ad41 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Cline – \#1 on OpenRouter diff --git a/package.json b/package.json index 0c9e7e41b0..a7dd3770d7 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.3.2", + "version": "3.4.0", "icon": "assets/icons/icon.png", "galleryBanner": { "color": "#617A91", From 5dea79c65f1abdc15ae7162091210ee8169be235 Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Mon, 17 Feb 2025 10:35:21 -0800 Subject: [PATCH 11/12] Fix Mistral provider server URL --- CHANGELOG.md | 1 + package-lock.json | 24 ++++++++++++++++++------ package.json | 2 +- src/api/providers/mistral.ts | 1 - 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e9ef115f3..a1eaebc759 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Add advanced configuration options for OpenAI Compatible (context window, max output, pricing, etc.) - Add Alibaba Qwen 2.5 coder models, VL models, and DeepSeek-R1/V3 support - Improve support for AWS Bedrock Profiles +- Fix Mistral provider support for non-codestral models - Add advanced setting to disable browser tool - Add advanced setting to set chromium executable path for browser tool diff --git a/package-lock.json b/package-lock.json index 7a4b80d77b..d89b7dc8dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,19 @@ { "name": "claude-dev", - "version": "3.3.2", + "version": "3.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "claude-dev", - "version": "3.3.2", + "version": "3.4.0", "license": "Apache-2.0", "dependencies": { "@anthropic-ai/bedrock-sdk": "^0.10.2", "@anthropic-ai/sdk": "^0.26.0", "@anthropic-ai/vertex-sdk": "^0.4.1", "@google/generative-ai": "^0.18.0", - "@mistralai/mistralai": "^1.3.6", + "@mistralai/mistralai": "^1.5.0", "@modelcontextprotocol/sdk": "^1.0.1", "@types/clone-deep": "^4.0.4", "@types/get-folder-size": "^3.0.4", @@ -4098,13 +4098,25 @@ } }, "node_modules/@mistralai/mistralai": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.4.0.tgz", - "integrity": "sha512-xA3DAtIDh4Qgr1EoSuiGVE+2ABNrxpcTeC0kSXYbkDNUGdthalLAH7DgbG0fkKZ7TN8xdWXQq2WiIghp/O96Eg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.5.0.tgz", + "integrity": "sha512-AIn8pwAwA/fDvEUvmkt+40zH1ZmfaG3Q7oUWl17GUEC1tU7ZPwYz8Cv9P59lyS1SisHdDSu81oknO7f1ywkz8Q==", + "dependencies": { + "zod-to-json-schema": "^3.24.1" + }, "peerDependencies": { "zod": ">= 3" } }, + "node_modules/@mistralai/mistralai/node_modules/zod-to-json-schema": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.1.tgz", + "integrity": "sha512-3h08nf3Vw3Wl3PK+q3ow/lIil81IT2Oa7YpQyUUDsEWbXveMesdfK1xBd2RhCkynwZndAxixji/7SYJJowr62w==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + }, "node_modules/@mixmark-io/domino": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz", diff --git a/package.json b/package.json index a7dd3770d7..5decc223d0 100644 --- a/package.json +++ b/package.json @@ -236,7 +236,7 @@ "@anthropic-ai/sdk": "^0.26.0", "@anthropic-ai/vertex-sdk": "^0.4.1", "@google/generative-ai": "^0.18.0", - "@mistralai/mistralai": "^1.3.6", + "@mistralai/mistralai": "^1.5.0", "@modelcontextprotocol/sdk": "^1.0.1", "@types/clone-deep": "^4.0.4", "@types/get-folder-size": "^3.0.4", diff --git a/src/api/providers/mistral.ts b/src/api/providers/mistral.ts index 28c2331c60..4be68e2e5e 100644 --- a/src/api/providers/mistral.ts +++ b/src/api/providers/mistral.ts @@ -22,7 +22,6 @@ export class MistralHandler implements ApiHandler { constructor(options: ApiHandlerOptions) { this.options = options this.client = new Mistral({ - serverURL: "https://api.mistral.ai", apiKey: this.options.mistralApiKey, }) } From cc166f41443bdda215eede4995f2fef3507f534b Mon Sep 17 00:00:00 2001 From: Daniel Trugman Date: Mon, 17 Feb 2025 18:37:23 +0000 Subject: [PATCH 12/12] Requesty provider improvements (#1829) * Requesty: Add cline headers to requests * Requesty: Add support for cache read/write tokens * Requesty: Remove deepseek reasoner conversions that are handled in-flight by Requesty * Requesty: Read o3-mini reasoning effort from Cline settings * Requesty: Add total cost per interaction * Add changeset --- .changeset/fair-snails-confess.md | 10 ++++++++++ src/api/providers/requesty.ts | 31 +++++++++++++++++++++++-------- 2 files changed, 33 insertions(+), 8 deletions(-) create mode 100644 .changeset/fair-snails-confess.md diff --git a/.changeset/fair-snails-confess.md b/.changeset/fair-snails-confess.md new file mode 100644 index 0000000000..f54c839141 --- /dev/null +++ b/.changeset/fair-snails-confess.md @@ -0,0 +1,10 @@ +--- +"claude-dev": patch +--- + +Improve Requesty provider integration + +- Adding Cline headers to API requests, to enable targeted optimizations +- Read o3 reasoning effort from Cline config, not model name +- Show token information in task header +- Get total cost from response when available diff --git a/src/api/providers/requesty.ts b/src/api/providers/requesty.ts index 34dd2b4d67..218d4785ad 100644 --- a/src/api/providers/requesty.ts +++ b/src/api/providers/requesty.ts @@ -5,7 +5,6 @@ import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../.. import { ApiHandler } from "../index" import { convertToOpenAiMessages } from "../transform/openai-format" import { ApiStream } from "../transform/stream" -import { convertToR1Format } from "../transform/r1-format" export class RequestyHandler implements ApiHandler { private options: ApiHandlerOptions @@ -16,30 +15,32 @@ export class RequestyHandler implements ApiHandler { this.client = new OpenAI({ baseURL: "https://router.requesty.ai/v1", apiKey: this.options.requestyApiKey, + defaultHeaders: { + "HTTP-Referer": "https://cline.bot", + "X-Title": "Cline", + }, }) } @withRetry() async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { const modelId = this.options.requestyModelId ?? "" - const isDeepseekReasoner = modelId.includes("deepseek-reasoner") let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [ { role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages), ] - if (isDeepseekReasoner) { - openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages]) - } - + // @ts-ignore-next-line const stream = await this.client.chat.completions.create({ model: modelId, messages: openAiMessages, temperature: 0, stream: true, stream_options: { include_usage: true }, + ...(modelId === "openai/o3-mini" ? { reasoning_effort: this.options.o3MiniReasoningEffort || "medium" } : {}), }) + for await (const chunk of stream) { const delta = chunk.choices[0]?.delta if (delta?.content) { @@ -56,11 +57,25 @@ export class RequestyHandler implements ApiHandler { } } + // Requesty usage includes an extra field for Anthropic use cases. + // Safely cast the prompt token details section to the appropriate structure. + interface RequestyUsage extends OpenAI.CompletionUsage { + prompt_tokens_details?: { + caching_tokens?: number + cached_tokens?: number + } + total_cost?: number + } + if (chunk.usage) { + const usage = chunk.usage as RequestyUsage yield { type: "usage", - inputTokens: chunk.usage.prompt_tokens || 0, - outputTokens: chunk.usage.completion_tokens || 0, + inputTokens: usage.prompt_tokens || 0, + outputTokens: usage.completion_tokens || 0, + cacheWriteTokens: usage.prompt_tokens_details?.caching_tokens || undefined, + cacheReadTokens: usage.prompt_tokens_details?.cached_tokens || undefined, + totalCost: usage.total_cost || undefined, } } }