diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6c3c07d673..de5566d05f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,21 @@
# Change Log
+## [3.2.3]
+
+- Add DeepSeek-R1 (deepseek-reasoner) model support with proper parameter handling (thanks @slavakurilyak!)
+
+## [3.2.0]
+
+- Add Plan/Act mode toggle to let you plan tasks with Cline before letting him get to work
+- Easily switch between API providers and models using a new popup menu under the chat field
+- Add VS Code LM API provider to run models provided by other VS Code extensions (e.g. GitHub Copilot). Shoutout to @julesmons, @RaySinner, and @MrUbens for putting this together!
+- Add on/off toggle for MCP servers to disable them when not in use. Thanks @MrUbens!
+- Add Auto-approve option for individual tools in MCP servers. Thanks @MrUbens!
+
+## [3.1.10]
+
+- New icon!
+
## [3.1.9]
- Add Mistral API provider with codestral-latest model
diff --git a/assets/icons/icon.png b/assets/icons/icon.png
index e8736aaa02..db6f1d8fd1 100644
Binary files a/assets/icons/icon.png and b/assets/icons/icon.png differ
diff --git a/assets/icons/icon.svg b/assets/icons/icon.svg
new file mode 100644
index 0000000000..2a3908aa4f
--- /dev/null
+++ b/assets/icons/icon.svg
@@ -0,0 +1,16 @@
+
+
\ No newline at end of file
diff --git a/assets/icons/robot_panel_dark.png b/assets/icons/robot_panel_dark.png
index 0ed7cc6274..36c37766f9 100644
Binary files a/assets/icons/robot_panel_dark.png and b/assets/icons/robot_panel_dark.png differ
diff --git a/assets/icons/robot_panel_light.png b/assets/icons/robot_panel_light.png
index bbc7fca4ac..2f028e0a20 100644
Binary files a/assets/icons/robot_panel_light.png and b/assets/icons/robot_panel_light.png differ
diff --git a/docs/mcp/mcp-quickstart.md b/docs/mcp/mcp-quickstart.md
index a62d5e7a47..13e194e47c 100644
--- a/docs/mcp/mcp-quickstart.md
+++ b/docs/mcp/mcp-quickstart.md
@@ -35,7 +35,7 @@ STOP! Before proceeding, you MUST verify these requirements:
1. From the Cline extension, click the `MCP Server` tab
1. Click the `Edit MCP Settings` button
-
+
1. The MCP settings files should be display in a tab in VS Code.
1. Replce the file's contents with this code:
diff --git a/package-lock.json b/package-lock.json
index b1de717b85..c4f4ef03ac 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,13 @@
{
"name": "claude-dev",
- "version": "3.1.8",
+ "version": "3.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
- "version": "3.1.8",
+ "version": "3.1.11",
+ "version": "3.2.0",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
diff --git a/package.json b/package.json
index daa4653dc8..b36fa00eba 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.1.9",
+ "version": "3.2.4",
"icon": "assets/icons/icon.png",
"galleryBanner": {
"color": "#617A91",
@@ -61,7 +61,7 @@
{
"id": "claude-dev-ActivityBar",
"title": "Cline",
- "icon": "$(robot)"
+ "icon": "assets/icons/icon.svg"
}
]
},
@@ -134,6 +134,25 @@
"when": "view == claude-dev.SidebarProvider"
}
]
+ },
+ "configuration": {
+ "title": "Cline",
+ "properties": {
+ "cline.vsCodeLmModelSelector": {
+ "type": "object",
+ "properties": {
+ "vendor": {
+ "type": "string",
+ "description": "The vendor of the language model (e.g. copilot)"
+ },
+ "family": {
+ "type": "string",
+ "description": "The family of the language model (e.g. gpt-4)"
+ }
+ },
+ "description": "Settings for VSCode Language Model API"
+ }
+ }
}
},
"scripts": {
diff --git a/src/api/index.ts b/src/api/index.ts
index d3308df5c6..2ef82f8659 100644
--- a/src/api/index.ts
+++ b/src/api/index.ts
@@ -12,12 +12,17 @@ import { OpenAiNativeHandler } from "./providers/openai-native"
import { ApiStream } from "./transform/stream"
import { DeepSeekHandler } from "./providers/deepseek"
import { MistralHandler } from "./providers/mistral"
+import { VsCodeLmHandler } from "./providers/vscode-lm"
export interface ApiHandler {
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream
getModel(): { id: string; info: ModelInfo }
}
+export interface SingleCompletionHandler {
+ completePrompt(prompt: string): Promise
+}
+
export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
const { apiProvider, ...options } = configuration
switch (apiProvider) {
@@ -43,6 +48,8 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
return new DeepSeekHandler(options)
case "mistral":
return new MistralHandler(options)
+ case "vscode-lm":
+ return new VsCodeLmHandler(options)
default:
return new AnthropicHandler(options)
}
diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts
index 6fbe1f2509..8c3fd1b87d 100644
--- a/src/api/providers/anthropic.ts
+++ b/src/api/providers/anthropic.ts
@@ -17,8 +17,9 @@ export class AnthropicHandler implements ApiHandler {
}
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
+ const model = this.getModel()
let stream: AnthropicStream
- const modelId = this.getModel().id
+ const modelId = model.id
switch (modelId) {
// 'latest' alias does not support cache_control
case "claude-3-5-sonnet-20241022":
@@ -37,7 +38,7 @@ export class AnthropicHandler implements ApiHandler {
stream = await this.client.beta.promptCaching.messages.create(
{
model: modelId,
- max_tokens: this.getModel().info.maxTokens || 8192,
+ max_tokens: model.info.maxTokens || 8192,
temperature: 0,
system: [
{
@@ -104,7 +105,7 @@ export class AnthropicHandler implements ApiHandler {
default: {
stream = (await this.client.messages.create({
model: modelId,
- max_tokens: this.getModel().info.maxTokens || 8192,
+ max_tokens: model.info.maxTokens || 8192,
temperature: 0,
system: [{ text: systemPrompt, type: "text" }],
messages,
diff --git a/src/api/providers/deepseek.ts b/src/api/providers/deepseek.ts
index a903ce2dd9..d68dc49bed 100644
--- a/src/api/providers/deepseek.ts
+++ b/src/api/providers/deepseek.ts
@@ -18,13 +18,15 @@ export class DeepSeekHandler implements ApiHandler {
}
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
+ const model = this.getModel()
const stream = await this.client.chat.completions.create({
- model: this.getModel().id,
- max_completion_tokens: this.getModel().info.maxTokens,
- temperature: 0,
+ model: model.id,
+ max_completion_tokens: model.info.maxTokens,
messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
stream: true,
stream_options: { include_usage: true },
+ // Only set temperature for non-reasoner models
+ ...(model.id === "deepseek-reasoner" ? {} : { temperature: 0 }),
})
for await (const chunk of stream) {
diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts
index 3b9d7a354a..e0bec2cf1c 100644
--- a/src/api/providers/openrouter.ts
+++ b/src/api/providers/openrouter.ts
@@ -24,6 +24,8 @@ export class OpenRouterHandler implements ApiHandler {
}
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
+ const model = this.getModel()
+
// Convert Anthropic messages to OpenAI format
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
@@ -32,7 +34,7 @@ export class OpenRouterHandler implements ApiHandler {
// prompt caching: https://openrouter.ai/docs/prompt-caching
// this is specifically for claude models (some models may 'support prompt caching' automatically without this)
- switch (this.getModel().id) {
+ switch (model.id) {
case "anthropic/claude-3.5-sonnet":
case "anthropic/claude-3.5-sonnet:beta":
case "anthropic/claude-3.5-sonnet-20240620":
@@ -83,7 +85,7 @@ export class OpenRouterHandler implements ApiHandler {
// Not sure how openrouter defaults max tokens when no value is provided, but the anthropic api requires this value and since they offer both 4096 and 8192 variants, we should ensure 8192.
// (models usually default to max tokens allowed)
let maxTokens: number | undefined
- switch (this.getModel().id) {
+ switch (model.id) {
case "anthropic/claude-3.5-sonnet":
case "anthropic/claude-3.5-sonnet:beta":
case "anthropic/claude-3.5-sonnet-20240620":
@@ -97,15 +99,15 @@ export class OpenRouterHandler implements ApiHandler {
}
// Removes messages in the middle when close to context window limit. Should not be applied to models that support prompt caching since it would continuously break the cache.
- let shouldApplyMiddleOutTransform = !this.getModel().info.supportsPromptCache
+ let shouldApplyMiddleOutTransform = !model.info.supportsPromptCache
// except for deepseek (which we set supportsPromptCache to true for), where because the context window is so small our truncation algo might miss and we should use openrouter's middle-out transform as a fallback to ensure we don't exceed the context window (FIXME: once we have a more robust token estimator we should not rely on this)
- if (this.getModel().id === "deepseek/deepseek-chat") {
+ if (model.id === "deepseek/deepseek-chat") {
shouldApplyMiddleOutTransform = true
}
// @ts-ignore-next-line
const stream = await this.client.chat.completions.create({
- model: this.getModel().id,
+ model: model.id,
max_tokens: maxTokens,
temperature: 0,
messages: openAiMessages,
diff --git a/src/api/providers/vscode-lm.ts b/src/api/providers/vscode-lm.ts
new file mode 100644
index 0000000000..f28075f1da
--- /dev/null
+++ b/src/api/providers/vscode-lm.ts
@@ -0,0 +1,639 @@
+import { Anthropic } from "@anthropic-ai/sdk"
+import * as vscode from "vscode"
+import { ApiHandler, SingleCompletionHandler } from "../"
+import { calculateApiCost } from "../../utils/cost"
+import { ApiStream } from "../transform/stream"
+import { convertToVsCodeLmMessages } from "../transform/vscode-lm-format"
+import { SELECTOR_SEPARATOR, stringifyVsCodeLmModelSelector } from "../../shared/vsCodeSelectorUtils"
+import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api"
+
+// Cline does not update VSCode type definitions or engine requirements to maintain compatibility.
+// This declaration (as seen in src/integrations/TerminalManager.ts) provides types for the Language Model API in newer versions of VSCode.
+// Extracted from https://github.com/microsoft/vscode/blob/131ee0ef660d600cd0a7e6058375b281553abe20/src/vscode-dts/vscode.d.ts
+declare module "vscode" {
+ enum LanguageModelChatMessageRole {
+ User = 1,
+ Assistant = 2,
+ }
+ enum LanguageModelChatToolMode {
+ Auto = 1,
+ Required = 2,
+ }
+ interface LanguageModelChatSelector {
+ vendor?: string
+ family?: string
+ version?: string
+ id?: string
+ }
+ interface LanguageModelChatTool {
+ name: string
+ description: string
+ inputSchema?: object
+ }
+ interface LanguageModelChatRequestOptions {
+ justification?: string
+ modelOptions?: { [name: string]: any }
+ tools?: LanguageModelChatTool[]
+ toolMode?: LanguageModelChatToolMode
+ }
+ class LanguageModelTextPart {
+ value: string
+ constructor(value: string)
+ }
+ class LanguageModelToolCallPart {
+ callId: string
+ name: string
+ input: object
+ constructor(callId: string, name: string, input: object)
+ }
+ interface LanguageModelChatResponse {
+ stream: AsyncIterable
+ text: AsyncIterable
+ }
+ interface LanguageModelChat {
+ readonly name: string
+ readonly id: string
+ readonly vendor: string
+ readonly family: string
+ readonly version: string
+ readonly maxInputTokens: number
+
+ sendRequest(
+ messages: LanguageModelChatMessage[],
+ options?: LanguageModelChatRequestOptions,
+ token?: CancellationToken,
+ ): Thenable
+ countTokens(text: string | LanguageModelChatMessage, token?: CancellationToken): Thenable
+ }
+ class LanguageModelPromptTsxPart {
+ value: unknown
+ constructor(value: unknown)
+ }
+ class LanguageModelToolResultPart {
+ callId: string
+ content: Array
+ constructor(callId: string, content: Array)
+ }
+ class LanguageModelChatMessage {
+ static User(
+ content: string | Array,
+ name?: string,
+ ): LanguageModelChatMessage
+ static Assistant(
+ content: string | Array,
+ name?: string,
+ ): LanguageModelChatMessage
+
+ role: LanguageModelChatMessageRole
+ content: Array
+ name: string | undefined
+
+ constructor(
+ role: LanguageModelChatMessageRole,
+ content: string | Array,
+ name?: string,
+ )
+ }
+ namespace lm {
+ function selectChatModels(selector?: LanguageModelChatSelector): Thenable
+ }
+}
+
+/**
+ * Handles interaction with VS Code's Language Model API for chat-based operations.
+ * This handler implements the ApiHandler interface to provide VS Code LM specific functionality.
+ *
+ * @implements {ApiHandler}
+ *
+ * @remarks
+ * The handler manages a VS Code language model chat client and provides methods to:
+ * - Create and manage chat client instances
+ * - Stream messages using VS Code's Language Model API
+ * - Retrieve model information
+ *
+ * @example
+ * ```typescript
+ * const options = {
+ * vsCodeLmModelSelector: { vendor: "copilot", family: "gpt-4" }
+ * };
+ * const handler = new VsCodeLmHandler(options);
+ *
+ * // Stream a conversation
+ * const systemPrompt = "You are a helpful assistant";
+ * const messages = [{ role: "user", content: "Hello!" }];
+ * for await (const chunk of handler.createMessage(systemPrompt, messages)) {
+ * console.log(chunk);
+ * }
+ * ```
+ */
+export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
+ private options: ApiHandlerOptions
+ private client: vscode.LanguageModelChat | null
+ private disposable: vscode.Disposable | null
+ private currentRequestCancellation: vscode.CancellationTokenSource | null
+
+ constructor(options: ApiHandlerOptions) {
+ this.options = options
+ this.client = null
+ this.disposable = null
+ this.currentRequestCancellation = null
+
+ try {
+ // Listen for model changes and reset client
+ this.disposable = vscode.workspace.onDidChangeConfiguration((event) => {
+ if (event.affectsConfiguration("lm")) {
+ try {
+ this.client = null
+ this.ensureCleanState()
+ } catch (error) {
+ console.error("Error during configuration change cleanup:", error)
+ }
+ }
+ })
+ } catch (error) {
+ // Ensure cleanup if constructor fails
+ this.dispose()
+
+ throw new Error(
+ `Cline : Failed to initialize handler: ${error instanceof Error ? error.message : "Unknown error"}`,
+ )
+ }
+ }
+
+ /**
+ * Creates a language model chat client based on the provided selector.
+ *
+ * @param selector - Selector criteria to filter language model chat instances
+ * @returns Promise resolving to the first matching language model chat instance
+ * @throws Error when no matching models are found with the given selector
+ *
+ * @example
+ * const selector = { vendor: "copilot", family: "gpt-4o" };
+ * const chatClient = await createClient(selector);
+ */
+ async createClient(selector: vscode.LanguageModelChatSelector): Promise {
+ try {
+ const models = await vscode.lm.selectChatModels(selector)
+
+ // Use first available model or create a minimal model object
+ if (models && Array.isArray(models) && models.length > 0) {
+ return models[0]
+ }
+
+ // Create a minimal model if no models are available
+ return {
+ id: "default-lm",
+ name: "Default Language Model",
+ vendor: "vscode",
+ family: "lm",
+ version: "1.0",
+ maxInputTokens: 8192,
+ sendRequest: async (messages, options, token) => {
+ // Provide a minimal implementation
+ return {
+ stream: (async function* () {
+ yield new vscode.LanguageModelTextPart(
+ "Language model functionality is limited. Please check VS Code configuration.",
+ )
+ })(),
+ text: (async function* () {
+ yield "Language model functionality is limited. Please check VS Code configuration."
+ })(),
+ }
+ },
+ countTokens: async () => 0,
+ }
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : "Unknown error"
+ throw new Error(`Cline : Failed to select model: ${errorMessage}`)
+ }
+ }
+
+ /**
+ * Creates and streams a message using the VS Code Language Model API.
+ *
+ * @param systemPrompt - The system prompt to initialize the conversation context
+ * @param messages - An array of message parameters following the Anthropic message format
+ *
+ * @yields {ApiStream} An async generator that yields either text chunks or tool calls from the model response
+ *
+ * @throws {Error} When vsCodeLmModelSelector option is not provided
+ * @throws {Error} When the response stream encounters an error
+ *
+ * @remarks
+ * This method handles the initialization of the VS Code LM client if not already created,
+ * converts the messages to VS Code LM format, and streams the response chunks.
+ * Tool calls handling is currently a work in progress.
+ */
+ dispose(): void {
+ if (this.disposable) {
+ this.disposable.dispose()
+ }
+
+ if (this.currentRequestCancellation) {
+ this.currentRequestCancellation.cancel()
+ this.currentRequestCancellation.dispose()
+ }
+ }
+
+ private async countTokens(text: string | vscode.LanguageModelChatMessage): Promise {
+ // Check for required dependencies
+ if (!this.client) {
+ console.warn("Cline : No client available for token counting")
+ return 0
+ }
+
+ if (!this.currentRequestCancellation) {
+ console.warn("Cline : No cancellation token available for token counting")
+ return 0
+ }
+
+ // Validate input
+ if (!text) {
+ console.debug("Cline : Empty text provided for token counting")
+ return 0
+ }
+
+ try {
+ // Handle different input types
+ let tokenCount: number
+
+ if (typeof text === "string") {
+ tokenCount = await this.client.countTokens(text, this.currentRequestCancellation.token)
+ } else if (text instanceof vscode.LanguageModelChatMessage) {
+ // For chat messages, ensure we have content
+ if (!text.content || (Array.isArray(text.content) && text.content.length === 0)) {
+ console.debug("Cline : Empty chat message content")
+ return 0
+ }
+ tokenCount = await this.client.countTokens(text, this.currentRequestCancellation.token)
+ } else {
+ console.warn("Cline : Invalid input type for token counting")
+ return 0
+ }
+
+ // Validate the result
+ if (typeof tokenCount !== "number") {
+ console.warn("Cline : Non-numeric token count received:", tokenCount)
+ return 0
+ }
+
+ if (tokenCount < 0) {
+ console.warn("Cline : Negative token count received:", tokenCount)
+ return 0
+ }
+
+ return tokenCount
+ } catch (error) {
+ // Handle specific error types
+ if (error instanceof vscode.CancellationError) {
+ console.debug("Cline : Token counting cancelled by user")
+ return 0
+ }
+
+ const errorMessage = error instanceof Error ? error.message : "Unknown error"
+ console.warn("Cline : Token counting failed:", errorMessage)
+
+ // Log additional error details if available
+ if (error instanceof Error && error.stack) {
+ console.debug("Token counting error stack:", error.stack)
+ }
+
+ return 0 // Fallback to prevent stream interruption
+ }
+ }
+
+ private async calculateTotalInputTokens(
+ systemPrompt: string,
+ vsCodeLmMessages: vscode.LanguageModelChatMessage[],
+ ): Promise {
+ const systemTokens: number = await this.countTokens(systemPrompt)
+
+ const messageTokens: number[] = await Promise.all(vsCodeLmMessages.map((msg) => this.countTokens(msg)))
+
+ return systemTokens + messageTokens.reduce((sum: number, tokens: number): number => sum + tokens, 0)
+ }
+
+ private ensureCleanState(): void {
+ if (this.currentRequestCancellation) {
+ this.currentRequestCancellation.cancel()
+ this.currentRequestCancellation.dispose()
+ this.currentRequestCancellation = null
+ }
+ }
+
+ private async getClient(): Promise {
+ if (!this.client) {
+ console.debug("Cline : Getting client with options:", {
+ vsCodeLmModelSelector: this.options.vsCodeLmModelSelector,
+ hasOptions: !!this.options,
+ selectorKeys: this.options.vsCodeLmModelSelector ? Object.keys(this.options.vsCodeLmModelSelector) : [],
+ })
+
+ try {
+ // Use default empty selector if none provided to get all available models
+ const selector = this.options?.vsCodeLmModelSelector || {}
+ console.debug("Cline : Creating client with selector:", selector)
+ this.client = await this.createClient(selector)
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Unknown error"
+ console.error("Cline : Client creation failed:", message)
+ throw new Error(`Cline : Failed to create client: ${message}`)
+ }
+ }
+
+ return this.client
+ }
+
+ private cleanTerminalOutput(text: string): string {
+ if (!text) {
+ return ""
+ }
+
+ return (
+ text
+ // Normalize line breaks
+ .replace(/\r\n/g, "\n")
+ .replace(/\r/g, "\n")
+
+ // Remove ANSI escape sequences
+ .replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "") // Full set of ANSI sequences
+ .replace(/\x9B[0-?]*[ -/]*[@-~]/g, "") // CSI sequences
+
+ // Remove terminal title setting sequences and other OSC sequences
+ .replace(/\x1B\][0-9;]*(?:\x07|\x1B\\)/g, "")
+
+ // Remove control characters
+ .replace(/[\x00-\x09\x0B-\x0C\x0E-\x1F\x7F]/g, "")
+
+ // Remove VS Code escape sequences
+ .replace(/\x1B[PD].*?\x1B\\/g, "") // DCS sequences
+ .replace(/\x1B_.*?\x1B\\/g, "") // APC sequences
+ .replace(/\x1B\^.*?\x1B\\/g, "") // PM sequences
+ .replace(/\x1B\[[\d;]*[HfABCDEFGJKST]/g, "") // Cursor movement and clear screen
+
+ // Remove Windows paths and service information
+ .replace(/^(?:PS )?[A-Z]:\\[^\n]*$/gm, "")
+ .replace(/^;?Cwd=.*$/gm, "")
+
+ // Clean escaped sequences
+ .replace(/\\x[0-9a-fA-F]{2}/g, "")
+ .replace(/\\u[0-9a-fA-F]{4}/g, "")
+
+ // Final cleanup
+ .replace(/\n{3,}/g, "\n\n") // Remove multiple empty lines
+ .trim()
+ )
+ }
+
+ private cleanMessageContent(content: any): any {
+ if (!content) {
+ return content
+ }
+
+ if (typeof content === "string") {
+ return this.cleanTerminalOutput(content)
+ }
+
+ if (Array.isArray(content)) {
+ return content.map((item) => this.cleanMessageContent(item))
+ }
+
+ if (typeof content === "object") {
+ const cleaned: any = {}
+ for (const [key, value] of Object.entries(content)) {
+ cleaned[key] = this.cleanMessageContent(value)
+ }
+ return cleaned
+ }
+
+ return content
+ }
+
+ async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
+ // Ensure clean state before starting a new request
+ this.ensureCleanState()
+ const client: vscode.LanguageModelChat = await this.getClient()
+
+ // Clean system prompt and messages
+ const cleanedSystemPrompt = this.cleanTerminalOutput(systemPrompt)
+ const cleanedMessages = messages.map((msg) => ({
+ ...msg,
+ content: this.cleanMessageContent(msg.content),
+ }))
+
+ // Convert Anthropic messages to VS Code LM messages
+ const vsCodeLmMessages: vscode.LanguageModelChatMessage[] = [
+ vscode.LanguageModelChatMessage.Assistant(cleanedSystemPrompt),
+ ...convertToVsCodeLmMessages(cleanedMessages),
+ ]
+
+ // Initialize cancellation token for the request
+ this.currentRequestCancellation = new vscode.CancellationTokenSource()
+
+ // Calculate input tokens before starting the stream
+ const totalInputTokens: number = await this.calculateTotalInputTokens(systemPrompt, vsCodeLmMessages)
+
+ // Accumulate the text and count at the end of the stream to reduce token counting overhead.
+ let accumulatedText: string = ""
+
+ try {
+ // Create the response stream with minimal required options
+ const requestOptions: vscode.LanguageModelChatRequestOptions = {
+ justification: `Cline would like to use '${client.name}' from '${client.vendor}', Click 'Allow' to proceed.`,
+ }
+
+ // Note: Tool support is currently provided by the VSCode Language Model API directly
+ // Extensions can register tools using vscode.lm.registerTool()
+
+ const response: vscode.LanguageModelChatResponse = await client.sendRequest(
+ vsCodeLmMessages,
+ requestOptions,
+ this.currentRequestCancellation.token,
+ )
+
+ // Consume the stream and handle both text and tool call chunks
+ for await (const chunk of response.stream) {
+ if (chunk instanceof vscode.LanguageModelTextPart) {
+ // Validate text part value
+ if (typeof chunk.value !== "string") {
+ console.warn("Cline : Invalid text part value received:", chunk.value)
+ continue
+ }
+
+ accumulatedText += chunk.value
+ yield {
+ type: "text",
+ text: chunk.value,
+ }
+ } else if (chunk instanceof vscode.LanguageModelToolCallPart) {
+ try {
+ // Validate tool call parameters
+ if (!chunk.name || typeof chunk.name !== "string") {
+ console.warn("Cline : Invalid tool name received:", chunk.name)
+ continue
+ }
+
+ if (!chunk.callId || typeof chunk.callId !== "string") {
+ console.warn("Cline : Invalid tool callId received:", chunk.callId)
+ continue
+ }
+
+ // Ensure input is a valid object
+ if (!chunk.input || typeof chunk.input !== "object") {
+ console.warn("Cline : Invalid tool input received:", chunk.input)
+ continue
+ }
+
+ // Convert tool calls to text format with proper error handling
+ const toolCall = {
+ type: "tool_call",
+ name: chunk.name,
+ arguments: chunk.input,
+ callId: chunk.callId,
+ }
+
+ const toolCallText = JSON.stringify(toolCall)
+ accumulatedText += toolCallText
+
+ // Log tool call for debugging
+ console.debug("Cline : Processing tool call:", {
+ name: chunk.name,
+ callId: chunk.callId,
+ inputSize: JSON.stringify(chunk.input).length,
+ })
+
+ yield {
+ type: "text",
+ text: toolCallText,
+ }
+ } catch (error) {
+ console.error("Cline : Failed to process tool call:", error)
+ // Continue processing other chunks even if one fails
+ continue
+ }
+ } else {
+ console.warn("Cline : Unknown chunk type received:", chunk)
+ }
+ }
+
+ // Count tokens in the accumulated text after stream completion
+ const totalOutputTokens: number = await this.countTokens(accumulatedText)
+
+ // Report final usage after stream completion
+ yield {
+ type: "usage",
+ inputTokens: totalInputTokens,
+ outputTokens: totalOutputTokens,
+ totalCost: calculateApiCost(this.getModel().info, totalInputTokens, totalOutputTokens),
+ }
+ } catch (error: unknown) {
+ this.ensureCleanState()
+
+ if (error instanceof vscode.CancellationError) {
+ throw new Error("Cline : Request cancelled by user")
+ }
+
+ if (error instanceof Error) {
+ console.error("Cline : Stream error details:", {
+ message: error.message,
+ stack: error.stack,
+ name: error.name,
+ })
+
+ // Return original error if it's already an Error instance
+ throw error
+ } else if (typeof error === "object" && error !== null) {
+ // Handle error-like objects
+ const errorDetails = JSON.stringify(error, null, 2)
+ console.error("Cline : Stream error object:", errorDetails)
+ throw new Error(`Cline : Response stream error: ${errorDetails}`)
+ } else {
+ // Fallback for unknown error types
+ const errorMessage = String(error)
+ console.error("Cline : Unknown stream error:", errorMessage)
+ throw new Error(`Cline : Response stream error: ${errorMessage}`)
+ }
+ }
+ }
+
+ // Return model information based on the current client state
+ getModel(): { id: string; info: ModelInfo } {
+ if (this.client) {
+ // Validate client properties
+ const requiredProps = {
+ id: this.client.id,
+ vendor: this.client.vendor,
+ family: this.client.family,
+ version: this.client.version,
+ maxInputTokens: this.client.maxInputTokens,
+ }
+
+ // Log any missing properties for debugging
+ for (const [prop, value] of Object.entries(requiredProps)) {
+ if (!value && value !== 0) {
+ console.warn(`Cline : Client missing ${prop} property`)
+ }
+ }
+
+ // Construct model ID using available information
+ const modelParts = [this.client.vendor, this.client.family, this.client.version].filter(Boolean)
+
+ const modelId = this.client.id || modelParts.join(SELECTOR_SEPARATOR)
+
+ // Build model info with conservative defaults for missing values
+ const modelInfo: ModelInfo = {
+ maxTokens: -1, // Unlimited tokens by default
+ contextWindow:
+ typeof this.client.maxInputTokens === "number"
+ ? Math.max(0, this.client.maxInputTokens)
+ : openAiModelInfoSaneDefaults.contextWindow,
+ supportsImages: false, // VSCode Language Model API currently doesn't support image inputs
+ supportsPromptCache: true,
+ inputPrice: 0,
+ outputPrice: 0,
+ description: `VSCode Language Model: ${modelId}`,
+ }
+
+ return { id: modelId, info: modelInfo }
+ }
+
+ // Fallback when no client is available
+ const fallbackId = this.options.vsCodeLmModelSelector
+ ? stringifyVsCodeLmModelSelector(this.options.vsCodeLmModelSelector)
+ : "vscode-lm"
+
+ console.debug("Cline : No client available, using fallback model info")
+
+ return {
+ id: fallbackId,
+ info: {
+ ...openAiModelInfoSaneDefaults,
+ description: `VSCode Language Model (Fallback): ${fallbackId}`,
+ },
+ }
+ }
+
+ async completePrompt(prompt: string): Promise {
+ try {
+ const client = await this.getClient()
+ const response = await client.sendRequest(
+ [vscode.LanguageModelChatMessage.User(prompt)],
+ {},
+ new vscode.CancellationTokenSource().token,
+ )
+ let result = ""
+ for await (const chunk of response.stream) {
+ if (chunk instanceof vscode.LanguageModelTextPart) {
+ result += chunk.value
+ }
+ }
+ return result
+ } catch (error) {
+ if (error instanceof Error) {
+ throw new Error(`VSCode LM completion error: ${error.message}`)
+ }
+ throw error
+ }
+ }
+}
diff --git a/src/api/transform/vscode-lm-format.ts b/src/api/transform/vscode-lm-format.ts
new file mode 100644
index 0000000000..acec3656e1
--- /dev/null
+++ b/src/api/transform/vscode-lm-format.ts
@@ -0,0 +1,200 @@
+import { Anthropic } from "@anthropic-ai/sdk"
+import * as vscode from "vscode"
+
+/**
+ * Safely converts a value into a plain object.
+ */
+function asObjectSafe(value: any): object {
+ // Handle null/undefined
+ if (!value) {
+ return {}
+ }
+
+ try {
+ // Handle strings that might be JSON
+ if (typeof value === "string") {
+ return JSON.parse(value)
+ }
+
+ // Handle pre-existing objects
+ if (typeof value === "object") {
+ return Object.assign({}, value)
+ }
+
+ return {}
+ } catch (error) {
+ console.warn("Cline : Failed to parse object:", error)
+ return {}
+ }
+}
+
+export function convertToVsCodeLmMessages(
+ anthropicMessages: Anthropic.Messages.MessageParam[],
+): vscode.LanguageModelChatMessage[] {
+ const vsCodeLmMessages: vscode.LanguageModelChatMessage[] = []
+
+ for (const anthropicMessage of anthropicMessages) {
+ // Handle simple string messages
+ if (typeof anthropicMessage.content === "string") {
+ vsCodeLmMessages.push(
+ anthropicMessage.role === "assistant"
+ ? vscode.LanguageModelChatMessage.Assistant(anthropicMessage.content)
+ : vscode.LanguageModelChatMessage.User(anthropicMessage.content),
+ )
+ continue
+ }
+
+ // Handle complex message structures
+ switch (anthropicMessage.role) {
+ case "user": {
+ const { nonToolMessages, toolMessages } = anthropicMessage.content.reduce<{
+ nonToolMessages: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[]
+ toolMessages: Anthropic.ToolResultBlockParam[]
+ }>(
+ (acc, part) => {
+ if (part.type === "tool_result") {
+ acc.toolMessages.push(part)
+ } else if (part.type === "text" || part.type === "image") {
+ acc.nonToolMessages.push(part)
+ }
+ return acc
+ },
+ { nonToolMessages: [], toolMessages: [] },
+ )
+
+ // Process tool messages first then non-tool messages
+ const contentParts = [
+ // Convert tool messages to ToolResultParts
+ ...toolMessages.map((toolMessage) => {
+ // Process tool result content into TextParts
+ const toolContentParts: vscode.LanguageModelTextPart[] =
+ typeof toolMessage.content === "string"
+ ? [new vscode.LanguageModelTextPart(toolMessage.content)]
+ : (toolMessage.content?.map((part) => {
+ if (part.type === "image") {
+ return new vscode.LanguageModelTextPart(
+ `[Image (${part.source?.type || "Unknown source-type"}): ${part.source?.media_type || "unknown media-type"} not supported by VSCode LM API]`,
+ )
+ }
+ return new vscode.LanguageModelTextPart(part.text)
+ }) ?? [new vscode.LanguageModelTextPart("")])
+
+ return new vscode.LanguageModelToolResultPart(toolMessage.tool_use_id, toolContentParts)
+ }),
+
+ // Convert non-tool messages to TextParts after tool messages
+ ...nonToolMessages.map((part) => {
+ if (part.type === "image") {
+ return new vscode.LanguageModelTextPart(
+ `[Image (${part.source?.type || "Unknown source-type"}): ${part.source?.media_type || "unknown media-type"} not supported by VSCode LM API]`,
+ )
+ }
+ return new vscode.LanguageModelTextPart(part.text)
+ }),
+ ]
+
+ // Add single user message with all content parts
+ vsCodeLmMessages.push(vscode.LanguageModelChatMessage.User(contentParts))
+ break
+ }
+
+ case "assistant": {
+ const { nonToolMessages, toolMessages } = anthropicMessage.content.reduce<{
+ nonToolMessages: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[]
+ toolMessages: Anthropic.ToolUseBlockParam[]
+ }>(
+ (acc, part) => {
+ if (part.type === "tool_use") {
+ acc.toolMessages.push(part)
+ } else if (part.type === "text" || part.type === "image") {
+ acc.nonToolMessages.push(part)
+ }
+ return acc
+ },
+ { nonToolMessages: [], toolMessages: [] },
+ )
+
+ // Process tool messages first then non-tool messages
+ const contentParts = [
+ // Convert tool messages to ToolCallParts first
+ ...toolMessages.map(
+ (toolMessage) =>
+ new vscode.LanguageModelToolCallPart(
+ toolMessage.id,
+ toolMessage.name,
+ asObjectSafe(toolMessage.input),
+ ),
+ ),
+
+ // Convert non-tool messages to TextParts after tool messages
+ ...nonToolMessages.map((part) => {
+ if (part.type === "image") {
+ return new vscode.LanguageModelTextPart("[Image generation not supported by VSCode LM API]")
+ }
+ return new vscode.LanguageModelTextPart(part.text)
+ }),
+ ]
+
+ // Add the assistant message to the list of messages
+ vsCodeLmMessages.push(vscode.LanguageModelChatMessage.Assistant(contentParts))
+ break
+ }
+ }
+ }
+
+ return vsCodeLmMessages
+}
+
+export function convertToAnthropicRole(vsCodeLmMessageRole: vscode.LanguageModelChatMessageRole): string | null {
+ switch (vsCodeLmMessageRole) {
+ case vscode.LanguageModelChatMessageRole.Assistant:
+ return "assistant"
+ case vscode.LanguageModelChatMessageRole.User:
+ return "user"
+ default:
+ return null
+ }
+}
+
+export async function convertToAnthropicMessage(
+ vsCodeLmMessage: vscode.LanguageModelChatMessage,
+): Promise {
+ const anthropicRole: string | null = convertToAnthropicRole(vsCodeLmMessage.role)
+ if (anthropicRole !== "assistant") {
+ throw new Error("Cline : Only assistant messages are supported.")
+ }
+
+ return {
+ id: crypto.randomUUID(),
+ type: "message",
+ model: "vscode-lm",
+ role: anthropicRole,
+ content: vsCodeLmMessage.content
+ .map((part): Anthropic.ContentBlock | null => {
+ if (part instanceof vscode.LanguageModelTextPart) {
+ return {
+ type: "text",
+ text: part.value,
+ }
+ }
+
+ if (part instanceof vscode.LanguageModelToolCallPart) {
+ return {
+ type: "tool_use",
+ id: part.callId || crypto.randomUUID(),
+ name: part.name,
+ input: asObjectSafe(part.input),
+ }
+ }
+
+ return null
+ })
+ .filter((part): part is Anthropic.ContentBlock => part !== null),
+ stop_reason: null,
+ stop_sequence: null,
+ usage: {
+ input_tokens: 0,
+ output_tokens: 0,
+ },
+ }
+}
diff --git a/src/core/Cline.ts b/src/core/Cline.ts
index 44e1d40ee5..947a5fae6f 100644
--- a/src/core/Cline.ts
+++ b/src/core/Cline.ts
@@ -2,16 +2,18 @@ import { Anthropic } from "@anthropic-ai/sdk"
import cloneDeep from "clone-deep"
import delay from "delay"
import fs from "fs/promises"
+import getFolderSize from "get-folder-size"
import os from "os"
import pWaitFor from "p-wait-for"
import * as path from "path"
import { serializeError } from "serialize-error"
import * as vscode from "vscode"
import { ApiHandler, buildApiHandler } from "../api"
-import { ApiStream } from "../api/transform/stream"
+import CheckpointTracker from "../integrations/checkpoints/CheckpointTracker"
import { DIFF_VIEW_URI_SCHEME, DiffViewProvider } from "../integrations/editor/DiffViewProvider"
import { findToolName, formatContentBlockToMarkdown } from "../integrations/misc/export-markdown"
import { extractTextFromFile } from "../integrations/misc/extract-text"
+import { showSystemNotification } from "../integrations/notifications"
import { TerminalManager } from "../integrations/terminal/TerminalManager"
import { BrowserSession } from "../services/browser/BrowserSession"
import { UrlContentFetcher } from "../services/browser/UrlContentFetcher"
@@ -21,6 +23,8 @@ import { parseSourceCodeForDefinitionsTopLevel } from "../services/tree-sitter"
import { ApiConfiguration } from "../shared/api"
import { findLast, findLastIndex } from "../shared/array"
import { AutoApprovalSettings } from "../shared/AutoApprovalSettings"
+import { BrowserSettings } from "../shared/BrowserSettings"
+import { ChatSettings } from "../shared/ChatSettings"
import { combineApiRequests } from "../shared/combineApiRequests"
import { combineCommandSequences, COMMAND_REQ_APP_STRING } from "../shared/combineCommandSequences"
import {
@@ -43,20 +47,18 @@ import { ClineAskResponse, ClineCheckpointRestore } from "../shared/WebviewMessa
import { calculateApiCost } from "../utils/cost"
import { fileExistsAtPath } from "../utils/fs"
import { arePathsEqual, getReadablePath } from "../utils/path"
+import { fixModelHtmlEscaping, removeInvalidChars } from "../utils/string"
import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from "./assistant-message"
import { constructNewFileContent } from "./assistant-message/diff"
import { parseMentions } from "./mentions"
import { formatResponse } from "./prompts/responses"
-import { addUserInstructions, SYSTEM_PROMPT } from "./prompts/system"
-import { getNextTruncationRange, getTruncatedMessages } from "./sliding-window"
import { ClineProvider, GlobalFileNames } from "./webview/ClineProvider"
-import { showSystemNotification } from "../integrations/notifications"
-import { removeInvalidChars } from "../utils/string"
-import { fixModelHtmlEscaping } from "../utils/string"
+import { OpenRouterHandler } from "../api/providers/openrouter"
+import { getNextTruncationRange, getTruncatedMessages } from "./sliding-window"
+import { SYSTEM_PROMPT } from "./prompts/system"
+import { addUserInstructions } from "./prompts/system"
import { OpenAiHandler } from "../api/providers/openai"
-import CheckpointTracker from "../integrations/checkpoints/CheckpointTracker"
-import getFolderSize from "get-folder-size"
-import { BrowserSettings } from "../shared/BrowserSettings"
+import { ApiStream } from "../api/transform/stream"
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution
@@ -75,6 +77,7 @@ export class Cline {
customInstructions?: string
autoApprovalSettings: AutoApprovalSettings
private browserSettings: BrowserSettings
+ private chatSettings: ChatSettings
apiConversationHistory: Anthropic.MessageParam[] = []
clineMessages: ClineMessage[] = []
private askResponse?: ClineAskResponse
@@ -92,8 +95,11 @@ export class Cline {
checkpointTrackerErrorMessage?: string
conversationHistoryDeletedRange?: [number, number]
isInitialized = false
+ isAwaitingPlanResponse = false
+ didRespondToPlanAskBySwitchingMode = false
// streaming
+ isWaitingForFirstChunk = false
isStreaming = false
private currentStreamingContentIndex = 0
private assistantMessageContent: AssistantMessageContent[] = []
@@ -104,12 +110,14 @@ export class Cline {
private didRejectTool = false
private didAlreadyUseTool = false
private didCompleteReadingStream = false
+ private didAutomaticallyRetryFailedApiRequest = false
constructor(
provider: ClineProvider,
apiConfiguration: ApiConfiguration,
autoApprovalSettings: AutoApprovalSettings,
browserSettings: BrowserSettings,
+ chatSettings: ChatSettings,
customInstructions?: string,
task?: string,
images?: string[],
@@ -124,6 +132,7 @@ export class Cline {
this.customInstructions = customInstructions
this.autoApprovalSettings = autoApprovalSettings
this.browserSettings = browserSettings
+ this.chatSettings = chatSettings
if (historyItem) {
this.taskId = historyItem.id
this.conversationHistoryDeletedRange = historyItem.conversationHistoryDeletedRange
@@ -141,6 +150,10 @@ export class Cline {
this.browserSession.browserSettings = browserSettings
}
+ updateChatSettings(chatSettings: ChatSettings) {
+ this.chatSettings = chatSettings
+ }
+
// Storing task to disk for history
private async ensureTaskDirectoryExists(): Promise {
@@ -977,14 +990,20 @@ export class Cline {
newUserContent.push({
type: "text",
text:
- `[TASK RESUMPTION] This task was interrupted ${agoText}. It may or may not be complete, so please reassess the task context. Be aware that the project state may have changed since then. The current working directory is now '${cwd.toPosix()}'. If the task has not been completed, retry the last step before interruption and proceed with completing the task.\n\nNote: If you previously attempted a tool use that the user did not provide a result for, you should assume the tool use was not successful and assess whether you should retry. If the last tool was a browser_action, the browser has been closed and you must launch a new browser if needed.${
+ `[TASK RESUMPTION] ${
+ this.chatSettings?.mode === "plan"
+ ? `This task was interrupted ${agoText}. The conversation may have been incomplete. Be aware that the project state may have changed since then. The current working directory is now '${cwd.toPosix()}'.\n\nNote: If you previously attempted a tool use that the user did not provide a result for, you should assume the tool use was not successful. However you are in PLAN MODE, so rather than continuing the task, you must respond to the user's message.`
+ : `This task was interrupted ${agoText}. It may or may not be complete, so please reassess the task context. Be aware that the project state may have changed since then. The current working directory is now '${cwd.toPosix()}'. If the task has not been completed, retry the last step before interruption and proceed with completing the task.\n\nNote: If you previously attempted a tool use that the user did not provide a result for, you should assume the tool use was not successful and assess whether you should retry. If the last tool was a browser_action, the browser has been closed and you must launch a new browser if needed.`
+ }${
wasRecent
? "\n\nIMPORTANT: If the last tool use was a replace_in_file or write_to_file that was interrupted, the file was reverted back to its original state before the interrupted edit, and you do NOT need to re-read the file as you already have its up-to-date contents."
: ""
}` +
(responseText
- ? `\n\nNew instructions for task continuation:\n\n${responseText}\n`
- : ""),
+ ? `\n\n${this.chatSettings?.mode === "plan" ? "New message to respond to with plan_mode_response tool (be sure to provide your response in the parameter)" : "New instructions for task continuation"}:\n\n${responseText}\n`
+ : this.chatSettings.mode === "plan"
+ ? "(The user did not provide a new message. Consider asking them how they'd like you to proceed, or to switch to Act mode to continue with the task.)"
+ : ""),
})
if (responseImages && responseImages.length > 0) {
@@ -1257,21 +1276,35 @@ export class Cline {
this.conversationHistoryDeletedRange,
)
- const stream = this.api.createMessage(systemPrompt, truncatedConversationHistory)
+ let stream = this.api.createMessage(systemPrompt, truncatedConversationHistory)
+
const iterator = stream[Symbol.asyncIterator]()
try {
// awaiting first chunk to see if it will throw an error
+ this.isWaitingForFirstChunk = true
const firstChunk = await iterator.next()
yield firstChunk.value
+ this.isWaitingForFirstChunk = false
} catch (error) {
- // 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))
- if (response !== "yesButtonClicked") {
- // this will never happen since if noButtonClicked, we will clear current task, aborting this instance
- throw new Error("API request failed")
+ const isOpenRouter = this.api instanceof OpenRouterHandler
+ if (isOpenRouter && !this.didAutomaticallyRetryFailedApiRequest) {
+ console.log("first chunk failed, waiting 1 second before retrying")
+ await delay(1000)
+ this.didAutomaticallyRetryFailedApiRequest = true
+ } 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),
+ )
+ if (response !== "yesButtonClicked") {
+ // this will never happen since if noButtonClicked, we will clear current task, aborting this instance
+ throw new Error("API request failed")
+ }
+ await this.say("api_req_retried")
}
- await this.say("api_req_retried")
// delegate generator output from the recursive call
yield* this.attemptApiRequest(previousApiReqIndex)
return
@@ -1391,6 +1424,8 @@ export class Cline {
return `[${block.name} for '${block.params.server_name}']`
case "ask_followup_question":
return `[${block.name} for '${block.params.question}']`
+ case "plan_mode_response":
+ return `[${block.name}]`
case "attempt_completion":
return `[${block.name}]`
}
@@ -2346,7 +2381,12 @@ export class Cline {
arguments: mcp_arguments,
} satisfies ClineAskUseMcpServer)
- if (this.shouldAutoApproveTool(block.name)) {
+ const isToolAutoApproved = this.providerRef
+ .deref()
+ ?.mcpHub?.connections?.find((conn) => conn.server.name === server_name)
+ ?.server.tools?.find((tool) => tool.name === tool_name)?.autoApprove
+
+ if (this.shouldAutoApproveTool(block.name) && isToolAutoApproved) {
this.removeLastPartialMessageIfExistsWithType("ask", "use_mcp_server")
await this.say("use_mcp_server", completeMessage, undefined, false)
this.consecutiveAutoApprovedRequestsCount++
@@ -2509,6 +2549,56 @@ export class Cline {
break
}
}
+ case "plan_mode_response": {
+ const response: string | undefined = block.params.response
+ try {
+ if (block.partial) {
+ await this.ask("plan_mode_response", removeClosingTag("response", response), block.partial).catch(
+ () => {},
+ )
+ break
+ } else {
+ if (!response) {
+ this.consecutiveMistakeCount++
+ pushToolResult(await this.sayAndCreateMissingParamError("plan_mode_response", "response"))
+ // await this.saveCheckpoint()
+ break
+ }
+ this.consecutiveMistakeCount = 0
+
+ // if (this.autoApprovalSettings.enabled && this.autoApprovalSettings.enableNotifications) {
+ // showSystemNotification({
+ // subtitle: "Cline has a response...",
+ // message: response.replace(/\n/g, " "),
+ // })
+ // }
+
+ this.isAwaitingPlanResponse = true
+ const { text, images } = await this.ask("plan_mode_response", response, false)
+ this.isAwaitingPlanResponse = false
+
+ if (this.didRespondToPlanAskBySwitchingMode) {
+ // 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.]`,
+ images,
+ ),
+ )
+ } else {
+ await this.say("user_feedback", text ?? "", images)
+ pushToolResult(formatResponse.toolResult(`\n${text}\n`, images))
+ }
+
+ // await this.saveCheckpoint()
+ break
+ }
+ } catch (error) {
+ await handleError("responding to inquiry", error)
+ // await this.saveCheckpoint()
+ break
+ }
+ }
case "attempt_completion": {
/*
this.consecutiveMistakeCount = 0
@@ -2878,6 +2968,7 @@ export class Cline {
this.didAlreadyUseTool = false
this.presentAssistantMessageLocked = false
this.presentAssistantMessageHasPendingUpdates = false
+ this.didAutomaticallyRetryFailedApiRequest = false
await this.diffViewProvider.reset()
const stream = this.attemptApiRequest(previousApiReqIndex) // yields only if the first chunk is successful, otherwise will allow the user to retry the request (most likely due to rate limit error, which gets thrown on the first chunk)
@@ -2988,7 +3079,9 @@ export class Cline {
// if the model did not tool use, then we need to tell it to either use a tool or attempt_completion
const didToolUse = this.assistantMessageContent.some((block) => block.type === "tool_use")
+
if (!didToolUse) {
+ // normal request where tool use is required
this.userMessageContent.push({
type: "text",
text: formatResponse.noToolsUsed(),
@@ -3165,20 +3258,20 @@ export class Cline {
}
// Add current time information with timezone
- // const now = new Date()
- // const formatter = new Intl.DateTimeFormat(undefined, {
- // year: "numeric",
- // month: "numeric",
- // day: "numeric",
- // hour: "numeric",
- // minute: "numeric",
- // second: "numeric",
- // hour12: true,
- // })
- // const timeZone = formatter.resolvedOptions().timeZone
- // const timeZoneOffset = -now.getTimezoneOffset() / 60 // Convert to hours and invert sign to match conventional notation
- // const timeZoneOffsetStr = `${timeZoneOffset >= 0 ? "+" : ""}${timeZoneOffset}:00`
- // details += `\n\n# Current Time\n${formatter.format(now)} (${timeZone}, UTC${timeZoneOffsetStr})`
+ const now = new Date()
+ const formatter = new Intl.DateTimeFormat(undefined, {
+ year: "numeric",
+ month: "numeric",
+ day: "numeric",
+ hour: "numeric",
+ minute: "numeric",
+ second: "numeric",
+ hour12: true,
+ })
+ const timeZone = formatter.resolvedOptions().timeZone
+ const timeZoneOffset = -now.getTimezoneOffset() / 60 // Convert to hours and invert sign to match conventional notation
+ const timeZoneOffsetStr = `${timeZoneOffset >= 0 ? "+" : ""}${timeZoneOffset}:00`
+ details += `\n\n# Current Time\n${formatter.format(now)} (${timeZone}, UTC${timeZoneOffsetStr})`
if (includeFileDetails) {
details += `\n\n# Current Working Directory (${cwd.toPosix()}) Files\n`
@@ -3193,6 +3286,17 @@ export class Cline {
}
}
+ details += "\n\n# Current Mode"
+ if (this.chatSettings.mode === "plan") {
+ details += "\nPLAN MODE"
+ details +=
+ "\nIn this mode you should focus on information gathering, asking questions, and architecting a solution. Once you have a plan, use the plan_mode_response tool to engage in a conversational back and forth with the user. Do not use the plan_mode_response tool until you've gathered all the information you need e.g. with read_file or ask_followup_question."
+ details +=
+ '\n(Remember: If it seems the user wants you to use tools only available in Act Mode, you should ask the user to "toggle to Act mode" (use those words) - they will have to manually do this themselves with the Plan/Act toggle button below. You do not have the ability to switch to Act Mode yourself, and must wait for the user to do it themselves once they are satisfied with the plan.)'
+ } else {
+ details += "\nACT MODE"
+ }
+
return `\n${details.trim()}\n`
}
}
diff --git a/src/core/assistant-message/index.ts b/src/core/assistant-message/index.ts
index 7ad2c27d7b..e3ba253e0e 100644
--- a/src/core/assistant-message/index.ts
+++ b/src/core/assistant-message/index.ts
@@ -20,6 +20,7 @@ export const toolUseNames = [
"use_mcp_tool",
"access_mcp_resource",
"ask_followup_question",
+ "plan_mode_response",
"attempt_completion",
] as const
@@ -44,6 +45,7 @@ export const toolParamNames = [
"arguments",
"uri",
"question",
+ "response",
"result",
] as const
diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts
index e3c44875f4..af3d9b9f46 100644
--- a/src/core/prompts/system.ts
+++ b/src/core/prompts/system.ts
@@ -235,6 +235,15 @@ Your final result description here
Command to demonstrate result (optional)
+## plan_mode_response
+Description: Respond to the user's inquiry in an effort to plan a solution to the user's task. This tool should be used when you need to provide a response to a question or statement from the user about how you plan to accomplish the task. This tool is only available in PLAN MODE. The environment_details will specify the current mode, if it is not PLAN MODE then you should not use this tool. Depending on the user's message, you may ask questions to get clarification about the user's request, architect a solution to the task, and to brainstorm ideas with the user. For example, if the user's task is to create a website, you may start by asking some clarifying questions, then present a detailed plan for how you will accomplish the task given the context, and perhaps engage in a back and forth to finalize the details before the user switches you to ACT MODE to implement the solution.
+Parameters:
+- response: (required) The response to provide to the user. Do not try to use tools in this parameter, this is simply a chat response.
+Usage:
+
+Your response here
+
+
# Tool Use Examples
## Example 1: Requesting to execute a command
@@ -717,6 +726,8 @@ npm run build
5. Install the MCP Server by adding the MCP server configuration to the settings file located at '${await mcpHub.getMcpSettingsFilePath()}'. The settings file may have other MCP servers already configured, so you would read it first and then add your new server to the existing \`mcpServers\` object.
+IMPORTANT: Regardless of what else you see in the MCP settings file, you must default any new MCP servers you create to disabled=false and autoApprove=[].
+
\`\`\`json
{
"mcpServers": {
@@ -740,9 +751,10 @@ npm run build
## Editing MCP Servers
-The user may ask to add tools or resources that may make sense to add to an existing MCP server (listed under 'Connected MCP Servers' above: ${
+The user may ask to add tools or resources that may make sense to add to an existing MCP server (listed under 'Connected MCP Servers' below: ${
mcpHub
.getServers()
+ .filter((server) => server.status === "connected")
.map((server) => server.name)
.join(", ") || "(None running currently)"
}, e.g. if it would use the same API. This would be possible if you can locate the MCP server repository on the user's system by looking at the server arguments for a filepath. You might then use list_files and read_file to explore the files in the repository, and use replace_in_file to make changes to the files.
@@ -834,6 +846,26 @@ By thoughtfully selecting between write_to_file and replace_in_file, you can mak
====
+ACT MODE V.S. PLAN MODE
+
+In each user message, the environment_details will specify the current mode. There are two modes:
+
+- ACT MODE: In this mode, you have access to all tools EXCEPT the plan_mode_response tool.
+ - In ACT MODE, you use tools to accomplish the user's task. Once you've completed the user's task, you use the attempt_completion tool to present the result of the task to the user.
+- PLAN MODE: In this special mode, you have access to the plan_mode_response tool.
+ - In PLAN MODE, the goal is to gather information and get context to create a detailed plan for accomplishing the task, which the user will review and approve before they switch you to ACT MODE to implement the solution.
+ - In PLAN MODE, when you need to converse with the user or present a plan, you should use the plan_mode_response tool to deliver your response directly, rather than using tags to analyze when to respond. Do not talk about using plan_mode_response - just use it directly to share your thoughts and provide helpful answers.
+
+## What is PLAN MODE?
+
+- While you are usually in ACT MODE, the user may switch to PLAN MODE in order to have a back and forth with you to plan how to best accomplish the task.
+- When starting in PLAN MODE, depending on the user's request, you may need to do some information gathering e.g. using read_file or search_files to get more context about the task. You may also ask the user clarifying questions to get a better understanding of the task.
+- Once you've gained more context about the user's request, you should architect a detailed plan for how you will accomplish the task.
+- Then you might ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and plan the best way to accomplish it.
+- Finally once it seems like you've reached a good plan, ask the user to switch you back to ACT MODE to implement the solution.
+
+====
+
CAPABILITIES
- You have access to tools that let you execute CLI commands on the user's computer, list files, view source code definitions, regex search${
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index 13d630cae0..8199d06a96 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -24,6 +24,7 @@ import { getNonce } from "./getNonce"
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"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -64,6 +65,8 @@ type GlobalStateKey =
| "openRouterModelInfo"
| "autoApprovalSettings"
| "browserSettings"
+ | "chatSettings"
+ | "vsCodeLmModelSelector"
export const GlobalFileNames = {
apiConversationHistory: "api_conversation_history.json",
@@ -82,11 +85,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
private cline?: Cline
private workspaceTracker?: WorkspaceTracker
mcpHub?: McpHub
- private latestAnnouncementId = "jan-6-2025" // update to some unique identifier when we add a new announcement
-
- public log(message: string) {
- this.outputChannel.appendLine(message)
- }
+ private latestAnnouncementId = "jan-20-2025" // update to some unique identifier when we add a new announcement
constructor(
readonly context: vscode.ExtensionContext,
@@ -217,18 +216,30 @@ 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
- const { apiConfiguration, customInstructions, autoApprovalSettings, browserSettings } = await this.getState()
- this.cline = new Cline(this, apiConfiguration, autoApprovalSettings, browserSettings, customInstructions, task, images)
- }
-
- async initClineWithHistoryItem(historyItem: HistoryItem) {
- await this.clearTask()
- const { apiConfiguration, customInstructions, autoApprovalSettings, browserSettings } = await this.getState()
+ const { apiConfiguration, customInstructions, autoApprovalSettings, browserSettings, chatSettings } =
+ await this.getState()
this.cline = new Cline(
this,
apiConfiguration,
autoApprovalSettings,
browserSettings,
+ chatSettings,
+ customInstructions,
+ task,
+ images,
+ )
+ }
+
+ async initClineWithHistoryItem(historyItem: HistoryItem) {
+ await this.clearTask()
+ const { apiConfiguration, customInstructions, autoApprovalSettings, browserSettings, chatSettings } =
+ await this.getState()
+ this.cline = new Cline(
+ this,
+ apiConfiguration,
+ autoApprovalSettings,
+ browserSettings,
+ chatSettings,
customInstructions,
undefined,
undefined,
@@ -401,6 +412,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
azureApiVersion,
openRouterModelId,
openRouterModelInfo,
+ vsCodeLmModelSelector,
} = message.apiConfiguration
await this.updateGlobalState("apiProvider", apiProvider)
await this.updateGlobalState("apiModelId", apiModelId)
@@ -428,6 +440,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.updateGlobalState("azureApiVersion", azureApiVersion)
await this.updateGlobalState("openRouterModelId", openRouterModelId)
await this.updateGlobalState("openRouterModelInfo", openRouterModelInfo)
+ await this.updateGlobalState("vsCodeLmModelSelector", vsCodeLmModelSelector)
if (this.cline) {
this.cline.api = buildApiHandler(message.apiConfiguration)
}
@@ -455,6 +468,27 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.postStateToWebview()
}
break
+ case "chatSettings":
+ if (message.chatSettings) {
+ const didSwitchToActMode = message.chatSettings.mode === "act"
+ await this.updateGlobalState("chatSettings", message.chatSettings)
+ await this.postStateToWebview()
+ if (this.cline) {
+ this.cline.updateChatSettings(message.chatSettings)
+ if (this.cline.isAwaitingPlanResponse && didSwitchToActMode) {
+ this.cline.didRespondToPlanAskBySwitchingMode = true
+ // this is necessary for the webview to update accordingly, but Cline instance will not send text back as feedback message
+ await this.postMessageToWebview({
+ type: "invoke",
+ invoke: "sendMessage",
+ text: "[Proceeding with the task...]",
+ })
+ } else {
+ this.cancelTask()
+ }
+ }
+ }
+ break
// case "relaunchChromeDebugMode":
// if (this.cline) {
// this.cline.browserSession.relaunchChromeDebugMode()
@@ -511,6 +545,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
lmStudioModels,
})
break
+ case "requestVsCodeLmModels":
+ const vsCodeLmModels = await this.getVsCodeLmModels()
+ this.postMessageToWebview({ type: "vsCodeLmModels", vsCodeLmModels })
+ break
case "refreshOpenRouterModels":
await this.refreshOpenRouterModels()
break
@@ -553,6 +591,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
case "cancelTask":
this.cancelTask()
break
+ case "getLatestState":
+ await this.postStateToWebview()
+ break
case "openMcpSettings": {
const mcpSettingsFilePath = await this.mcpHub?.getMcpSettingsFilePath()
if (mcpSettingsFilePath) {
@@ -560,6 +601,22 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
break
}
+ case "toggleMcpServer": {
+ try {
+ await this.mcpHub?.toggleServerDisabled(message.serverName!, message.disabled!)
+ } catch (error) {
+ console.error(`Failed to toggle MCP server ${message.serverName}:`, error)
+ }
+ break
+ }
+ case "toggleToolAutoApprove": {
+ try {
+ await this.mcpHub?.toggleToolAutoApprove(message.serverName!, message.toolName!, message.autoApprove!)
+ } catch (error) {
+ console.error(`Failed to toggle auto-approve for tool ${message.toolName}:`, error)
+ }
+ break
+ }
case "restartMcpServer": {
try {
await this.mcpHub?.restartConnection(message.text!)
@@ -590,7 +647,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
console.error("Failed to abort task", error)
}
await pWaitFor(
- () => this.cline === undefined || this.cline.isStreaming === false || this.cline.didFinishAbortingStream,
+ () =>
+ this.cline === undefined ||
+ this.cline.isStreaming === false ||
+ this.cline.didFinishAbortingStream ||
+ this.cline.isWaitingForFirstChunk, // if only first chunk is processed, then there's no need to wait for graceful abort (closes edits, browser, etc)
{
timeout: 3_000,
},
@@ -633,6 +694,18 @@ export class ClineProvider implements vscode.WebviewViewProvider {
return settingsDir
}
+ // VSCode LM API
+
+ private async getVsCodeLmModels() {
+ try {
+ const models = await vscode.lm.selectChatModels({})
+ return models || []
+ } catch (error) {
+ console.error("Error fetching VS Code LM models:", error)
+ return []
+ }
+ }
+
// Ollama
async getOllamaModels(baseUrl?: string) {
@@ -943,6 +1016,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
taskHistory,
autoApprovalSettings,
browserSettings,
+ chatSettings,
} = await this.getState()
return {
version: this.context.extension?.packageJSON?.version ?? "",
@@ -956,6 +1030,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId,
autoApprovalSettings,
browserSettings,
+ chatSettings,
}
}
@@ -1043,6 +1118,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
taskHistory,
autoApprovalSettings,
browserSettings,
+ chatSettings,
+ vsCodeLmModelSelector,
] = await Promise.all([
this.getGlobalState("apiProvider") as Promise,
this.getGlobalState("apiModelId") as Promise,
@@ -1075,6 +1152,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getGlobalState("taskHistory") as Promise,
this.getGlobalState("autoApprovalSettings") as Promise,
this.getGlobalState("browserSettings") as Promise,
+ this.getGlobalState("chatSettings") as Promise,
+ this.getGlobalState("vsCodeLmModelSelector") as Promise,
])
let apiProvider: ApiProvider
@@ -1119,12 +1198,14 @@ export class ClineProvider implements vscode.WebviewViewProvider {
azureApiVersion,
openRouterModelId,
openRouterModelInfo,
+ vsCodeLmModelSelector,
},
lastShownAnnouncementId,
customInstructions,
taskHistory,
autoApprovalSettings: autoApprovalSettings || DEFAULT_AUTO_APPROVAL_SETTINGS, // default value can be 0 or empty string
browserSettings: browserSettings || DEFAULT_BROWSER_SETTINGS,
+ chatSettings: chatSettings || DEFAULT_CHAT_SETTINGS,
}
}
diff --git a/src/services/mcp/McpHub.ts b/src/services/mcp/McpHub.ts
index a12d489671..dba50d54f6 100644
--- a/src/services/mcp/McpHub.ts
+++ b/src/services/mcp/McpHub.ts
@@ -25,11 +25,15 @@ export type McpConnection = {
transport: StdioClientTransport
}
+const AutoApproveSchema = z.array(z.string()).default([])
+
// StdioServerParameters
const StdioConfigSchema = z.object({
command: z.string(),
args: z.array(z.string()).optional(),
env: z.record(z.string()).optional(),
+ autoApprove: AutoApproveSchema.optional(),
+ disabled: z.boolean().optional(),
})
const McpSettingsSchema = z.object({
@@ -51,7 +55,8 @@ export class McpHub {
}
getServers(): McpServer[] {
- return this.connections.map((conn) => conn.server)
+ // Only return enabled servers
+ return this.connections.filter((conn) => !conn.server.disabled).map((conn) => conn.server)
}
isMcpEnabled(): boolean {
@@ -193,11 +198,13 @@ export class McpHub {
}
// valid schema
+ const parsedConfig = StdioConfigSchema.parse(config)
const connection: McpConnection = {
server: {
name,
config: JSON.stringify(config),
status: "connecting",
+ disabled: parsedConfig.disabled,
},
client,
transport,
@@ -279,7 +286,21 @@ export class McpHub {
const response = await this.connections
.find((conn) => conn.server.name === serverName)
?.client.request({ method: "tools/list" }, ListToolsResultSchema)
- return response?.tools || []
+
+ // Get autoApprove settings
+ const settingsPath = await this.getMcpSettingsFilePath()
+ const content = await fs.readFile(settingsPath, "utf-8")
+ const config = JSON.parse(content)
+ const autoApproveConfig = config.mcpServers[serverName]?.autoApprove || []
+
+ // Mark tools as always allowed based on settings
+ const tools = (response?.tools || []).map((tool) => ({
+ ...tool,
+ autoApprove: autoApproveConfig.includes(tool.name),
+ }))
+
+ // console.log(`[MCP] Fetched tools for ${serverName}:`, tools)
+ return tools
} catch (error) {
// console.error(`Failed to fetch tools for ${serverName}:`, error)
return []
@@ -445,11 +466,91 @@ export class McpHub {
// Using server
+ // Public methods for server management
+
+ public async toggleServerDisabled(serverName: string, disabled: boolean): Promise {
+ let settingsPath: string
+ try {
+ settingsPath = await this.getMcpSettingsFilePath()
+
+ // Ensure the settings file exists and is accessible
+ try {
+ await fs.access(settingsPath)
+ } catch (error) {
+ console.error("Settings file not accessible:", error)
+ throw new Error("Settings file not accessible")
+ }
+ const content = await fs.readFile(settingsPath, "utf-8")
+ const config = JSON.parse(content)
+
+ // Validate the config structure
+ if (!config || typeof config !== "object") {
+ throw new Error("Invalid config structure")
+ }
+
+ if (!config.mcpServers || typeof config.mcpServers !== "object") {
+ config.mcpServers = {}
+ }
+
+ if (config.mcpServers[serverName]) {
+ // Create a new server config object to ensure clean structure
+ const serverConfig = {
+ ...config.mcpServers[serverName],
+ disabled,
+ }
+
+ // Ensure required fields exist
+ if (!serverConfig.autoApprove) {
+ serverConfig.autoApprove = []
+ }
+
+ config.mcpServers[serverName] = serverConfig
+
+ // Write the entire config back
+ const updatedConfig = {
+ mcpServers: config.mcpServers,
+ }
+
+ await fs.writeFile(settingsPath, JSON.stringify(updatedConfig, null, 2))
+
+ const connection = this.connections.find((conn) => conn.server.name === serverName)
+ if (connection) {
+ try {
+ connection.server.disabled = disabled
+
+ // Only refresh capabilities if connected
+ if (connection.server.status === "connected") {
+ connection.server.tools = await this.fetchToolsList(serverName)
+ connection.server.resources = await this.fetchResourcesList(serverName)
+ connection.server.resourceTemplates = await this.fetchResourceTemplatesList(serverName)
+ }
+ } catch (error) {
+ console.error(`Failed to refresh capabilities for ${serverName}:`, error)
+ }
+ }
+
+ await this.notifyWebviewOfServerChanges()
+ }
+ } catch (error) {
+ console.error("Failed to update server disabled state:", error)
+ if (error instanceof Error) {
+ console.error("Error details:", error.message, error.stack)
+ }
+ vscode.window.showErrorMessage(
+ `Failed to update server state: ${error instanceof Error ? error.message : String(error)}`,
+ )
+ throw error
+ }
+ }
+
async readResource(serverName: string, uri: string): Promise {
const connection = this.connections.find((conn) => conn.server.name === serverName)
if (!connection) {
throw new Error(`No connection found for server: ${serverName}`)
}
+ if (connection.server.disabled) {
+ throw new Error(`Server "${serverName}" is disabled`)
+ }
return await connection.client.request(
{
method: "resources/read",
@@ -468,6 +569,11 @@ export class McpHub {
`No connection found for server: ${serverName}. Please make sure to use MCP servers available under 'Connected MCP Servers'.`,
)
}
+
+ if (connection.server.disabled) {
+ throw new Error(`Server "${serverName}" is disabled and cannot be used`)
+ }
+
return await connection.client.request(
{
method: "tools/call",
@@ -480,6 +586,44 @@ export class McpHub {
)
}
+ async toggleToolAutoApprove(serverName: string, toolName: string, shouldAllow: boolean): Promise {
+ try {
+ const settingsPath = await this.getMcpSettingsFilePath()
+ const content = await fs.readFile(settingsPath, "utf-8")
+ const config = JSON.parse(content)
+
+ // Initialize autoApprove if it doesn't exist
+ if (!config.mcpServers[serverName].autoApprove) {
+ config.mcpServers[serverName].autoApprove = []
+ }
+
+ const autoApprove = config.mcpServers[serverName].autoApprove
+ const toolIndex = autoApprove.indexOf(toolName)
+
+ if (shouldAllow && toolIndex === -1) {
+ // Add tool to autoApprove list
+ autoApprove.push(toolName)
+ } else if (!shouldAllow && toolIndex !== -1) {
+ // Remove tool from autoApprove list
+ autoApprove.splice(toolIndex, 1)
+ }
+
+ // Write updated config back to file
+ await fs.writeFile(settingsPath, JSON.stringify(config, null, 2))
+
+ // Update the tools list to reflect the change
+ const connection = this.connections.find((conn) => conn.server.name === serverName)
+ if (connection) {
+ connection.server.tools = await this.fetchToolsList(serverName)
+ await this.notifyWebviewOfServerChanges()
+ }
+ } catch (error) {
+ console.error("Failed to update autoApprove settings:", error)
+ vscode.window.showErrorMessage("Failed to update autoApprove settings")
+ throw error // Re-throw to ensure the error is properly handled
+ }
+ }
+
async dispose(): Promise {
this.removeAllFileWatchers()
for (const connection of this.connections) {
diff --git a/src/shared/ChatSettings.ts b/src/shared/ChatSettings.ts
new file mode 100644
index 0000000000..1632082db1
--- /dev/null
+++ b/src/shared/ChatSettings.ts
@@ -0,0 +1,7 @@
+export interface ChatSettings {
+ mode: "plan" | "act"
+}
+
+export const DEFAULT_CHAT_SETTINGS: ChatSettings = {
+ mode: "act",
+}
diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts
index fe5584c54d..ce6502774e 100644
--- a/src/shared/ExtensionMessage.ts
+++ b/src/shared/ExtensionMessage.ts
@@ -3,6 +3,7 @@
import { ApiConfiguration, ModelInfo } from "./api"
import { AutoApprovalSettings } from "./AutoApprovalSettings"
import { BrowserSettings } from "./BrowserSettings"
+import { ChatSettings } from "./ChatSettings"
import { HistoryItem } from "./HistoryItem"
import { McpServer } from "./mcp"
@@ -21,6 +22,8 @@ export interface ExtensionMessage {
| "openRouterModels"
| "mcpServers"
| "relinquishControl"
+ | "vsCodeLmModels"
+ | "requestVsCodeLmModels"
text?: string
action?: "chatButtonClicked" | "mcpButtonClicked" | "settingsButtonClicked" | "historyButtonClicked" | "didBecomeVisible"
invoke?: "sendMessage" | "primaryButtonClick" | "secondaryButtonClick"
@@ -28,6 +31,7 @@ export interface ExtensionMessage {
images?: string[]
ollamaModels?: string[]
lmStudioModels?: string[]
+ vsCodeLmModels?: { vendor?: string; family?: string; version?: string; id?: string }[]
filePaths?: string[]
partialMessage?: ClineMessage
openRouterModels?: Record
@@ -46,6 +50,7 @@ export interface ExtensionState {
shouldShowAnnouncement: boolean
autoApprovalSettings: AutoApprovalSettings
browserSettings: BrowserSettings
+ chatSettings: ChatSettings
}
export interface ClineMessage {
@@ -63,6 +68,7 @@ export interface ClineMessage {
export type ClineAsk =
| "followup"
+ | "plan_mode_response"
| "command"
| "command_output"
| "completion_result"
diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts
index 1344b652f9..e40125dd23 100644
--- a/src/shared/WebviewMessage.ts
+++ b/src/shared/WebviewMessage.ts
@@ -1,6 +1,7 @@
import { ApiConfiguration } from "./api"
import { AutoApprovalSettings } from "./AutoApprovalSettings"
import { BrowserSettings } from "./BrowserSettings"
+import { ChatSettings } from "./ChatSettings"
export interface WebviewMessage {
type:
@@ -28,12 +29,18 @@ export interface WebviewMessage {
| "restartMcpServer"
| "autoApprovalSettings"
| "browserSettings"
+ | "chatSettings"
| "checkpointDiff"
| "checkpointRestore"
| "taskCompletionViewChanges"
| "openExtensionSettings"
+ | "requestVsCodeLmModels"
+ | "toggleToolAutoApprove"
+ | "toggleMcpServer"
+ | "getLatestState"
// | "relaunchChromeDebugMode"
text?: string
+ disabled?: boolean
askResponse?: ClineAskResponse
apiConfiguration?: ApiConfiguration
images?: string[]
@@ -41,6 +48,12 @@ export interface WebviewMessage {
number?: number
autoApprovalSettings?: AutoApprovalSettings
browserSettings?: BrowserSettings
+ chatSettings?: ChatSettings
+
+ // For toggleToolAutoApprove
+ serverName?: string
+ toolName?: string
+ autoApprove?: boolean
}
export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse"
diff --git a/src/shared/api.ts b/src/shared/api.ts
index f5ff3017fe..2eeb6387ed 100644
--- a/src/shared/api.ts
+++ b/src/shared/api.ts
@@ -10,6 +10,7 @@ export type ApiProvider =
| "openai-native"
| "deepseek"
| "mistral"
+ | "vscode-lm"
export interface ApiHandlerOptions {
apiModelId?: string
@@ -37,6 +38,7 @@ export interface ApiHandlerOptions {
deepSeekApiKey?: string
mistralApiKey?: string
azureApiVersion?: string
+ vsCodeLmModelSelector?: any
}
export type ApiConfiguration = ApiHandlerOptions & {
@@ -375,6 +377,16 @@ export const deepSeekModels = {
cacheWritesPrice: 0.14,
cacheReadsPrice: 0.014,
},
+ "deepseek-reasoner": {
+ maxTokens: 8_000,
+ contextWindow: 64_000,
+ supportsImages: false,
+ supportsPromptCache: true, // supports context caching, but not in the way anthropic does it (deepseek reports input tokens and reads/writes in the same usage report) FIXME: we need to show users cache stats how deepseek does it
+ inputPrice: 0, // technically there is no input price, it's all either a cache hit or miss (ApiOptions will not show this)
+ outputPrice: 2.19,
+ cacheWritesPrice: 0.55,
+ cacheReadsPrice: 0.14,
+ },
} as const satisfies Record
// Mistral
diff --git a/src/shared/mcp.ts b/src/shared/mcp.ts
index 82efae2f72..b84f33d21a 100644
--- a/src/shared/mcp.ts
+++ b/src/shared/mcp.ts
@@ -6,12 +6,14 @@ export type McpServer = {
tools?: McpTool[]
resources?: McpResource[]
resourceTemplates?: McpResourceTemplate[]
+ disabled?: boolean
}
export type McpTool = {
name: string
description?: string
inputSchema?: object
+ autoApprove?: boolean
}
export type McpResource = {
diff --git a/src/shared/vsCodeSelectorUtils.ts b/src/shared/vsCodeSelectorUtils.ts
new file mode 100644
index 0000000000..620fccccd8
--- /dev/null
+++ b/src/shared/vsCodeSelectorUtils.ts
@@ -0,0 +1,7 @@
+import { LanguageModelChatSelector } from "vscode"
+
+export const SELECTOR_SEPARATOR = "/"
+
+export function stringifyVsCodeLmModelSelector(selector: LanguageModelChatSelector): string {
+ return [selector.vendor, selector.family, selector.version, selector.id].filter(Boolean).join(SELECTOR_SEPARATOR)
+}
diff --git a/webview-ui/src/components/chat/Announcement.tsx b/webview-ui/src/components/chat/Announcement.tsx
index da4c002e98..793a899296 100644
--- a/webview-ui/src/components/chat/Announcement.tsx
+++ b/webview-ui/src/components/chat/Announcement.tsx
@@ -31,39 +31,29 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {