Compare commits

..
76 changed files with 729 additions and 2012 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
updating o3 model pricing
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Sorting mcp marketplace by newest listings by default
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Migrate focusChatInput message to protobus
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
fix menu for setting terminal timeout
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Add prompt caching indicator to grok 3
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Supporting Notifications MCP with Cline
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Adding Thinking UX for Gemini
+3 -47
View File
@@ -1,46 +1,10 @@
<!-- ⚠️ Important: Discussion Required Before PR (Community Contributors) -->
**For community contributors, before submitting this PR, please ensure you have:**
- [ ] **Opened an issue** to discuss your proposed changes with the community
- [ ] **Received approval** from a core Cline contributor to proceed with the implementation
- [ ] **Linked the issue below** in the "Related Issue" section
**Exceptions:** Small bug fixes, typo corrections, minor wording improvements, or simple type fixes that don't change functionality may be submitted directly.
**Why this requirement?** We deeply appreciate all community contributions - they are the core reason we're able to operate successfully and keep innovating! We welcome community input and want to make it as easy as possible for people to submit quality work. This process helps our core maintainers review new ideas faster and saves contributor time by ensuring you have the go-ahead before spending time on implementation.
---
### Related Issue
<!-- Replace XXXX with the issue number that this PR addresses -->
**Issue:** #XXXX
### Description
<!--
Help reviewers understand your changes by making this PR readable and well-organized:
- What problem does this PR solve?
- Why were these changes introduced and what purpose do they serve?
- For larger changes, provide context about your approach and reasoning
Small PRs may need minimal description, but larger changes benefit from explaining where you're coming from. Much of this context can be in the linked issue above, so feel free to reference it rather than repeating everything here.
-->
<!-- Describe your changes in detail. What problem does this PR solve? -->
### Test Procedure
<!--
Please walk us through your testing approach and thought process. This helps reviewers understand that you've thoroughly considered the impact of your changes:
- How did you test this change?
- What could potentially break and how did you verify it doesn't?
- What existing functionality might be affected and how did you check it still works?
- Why are you confident this is ready for merge?
We're not looking for exhaustive documentation - just evidence that you've thought through the implications of your changes and tested accordingly.
-->
<!-- How did you test this? Are you confident that it will not introduce bugs? If so, why? -->
### Type of Change
@@ -65,15 +29,7 @@ We're not looking for exhaustive documentation - just evidence that you've thoug
### Screenshots
<!--
Help reviewers quickly understand your changes:
- **UI Changes**: Please include screenshots showing before/after states
- **Complex Workflows**: Consider uploading a screen recording (video) if your changes involve multiple steps or state transitions
- **Backend Changes**: Not required, but feel free to include terminal output or other evidence that demonstrates functionality
This helps reviewers see what you've built without having to pull down and test your branch first.
-->
<!-- For UI changes, add screenshots here -->
### Additional Notes
+4 -4
View File
@@ -32,8 +32,8 @@ src/shared/proto/host/*.ts
webview-ui/src/services/grpc-client.ts
# Standalone
src/standalone/server-setup.ts
src/standalone/services/host-grpc-client.ts
# Host bridge
src/hosts/vscode/*/methods.ts
src/hosts/vscode/*/index.ts
src/hosts/vscode/client/host-grpc-client.ts
src/hosts/vscode/host-grpc-service-config.ts
hosts/vscode/*/methods.ts
hosts/vscode/*/index.ts
hosts/vscode/host-grpc-service-config.ts
+2 -38
View File
@@ -128,25 +128,7 @@
"type": "npm",
"script": "watch:esbuild",
"group": "build",
"problemMatcher": {
"pattern": [
{
"regexp": "^✘ \\[ERROR\\] (.*)$",
"message": 1
},
{
"regexp": "^\\s+(.*):(\\d+):(\\d+):$",
"file": 1,
"line": 2,
"column": 3
}
],
"background": {
"activeOnStart": true,
"beginsPattern": "^\\[watch\\] build started$",
"endsPattern": "^\\[watch\\] build finished$"
}
},
"problemMatcher": "$esbuild-watch",
"isBackground": true,
"label": "npm: watch:esbuild",
"dependsOn": ["npm: protos"],
@@ -164,25 +146,7 @@
"type": "npm",
"script": "watch:esbuild:test",
"group": "build",
"problemMatcher": {
"pattern": [
{
"regexp": "^✘ \\[ERROR\\] (.*)$",
"message": 1
},
{
"regexp": "^\\s+(.*):(\\d+):(\\d+):$",
"file": 1,
"line": 2,
"column": 3
}
],
"background": {
"activeOnStart": true,
"beginsPattern": "^\\[watch\\] build started$",
"endsPattern": "^\\[watch\\] build finished$"
}
},
"problemMatcher": "$esbuild-watch",
"isBackground": true,
"label": "npm: watch:esbuild:test",
"dependsOn": ["npm: protos"],
-22
View File
@@ -1,22 +0,0 @@
version: v2
modules:
- path: proto
name: cline/cline/lint
lint:
use:
- STANDARD
except: # Add exceptions for current patterns that contradict STANDARD settings
- RPC_PASCAL_CASE # rpcs are camel case (start with lowercase)
- PACKAGE_DIRECTORY_MATCH # the protos in the cline package are not in a dir named cline.
- RPC_REQUEST_RESPONSE_UNIQUE # request messages are not unique.
- RPC_REQUEST_STANDARD_NAME # request messages dont all end with Request
- RPC_RESPONSE_STANDARD_NAME # response messages dont all end with Response
- PACKAGE_VERSION_SUFFIX # package name does not contain version.
- ENUM_VALUE_PREFIX # enum values dont start with the enum name.
- ENUM_ZERO_VALUE_SUFFIX # first value does not have to be UNSPECIFIED.
# breaking:
# use:
# - WIRE_JSON # Detect changes that break the json wire format (this is the minimum recommended level.)
-5
View File
@@ -13,7 +13,6 @@ interface RunDiffEvalOptions {
verbose: boolean
testPath: string
outputPath: string
replay: boolean
}
export async function runDiffEvalHandler(options: RunDiffEvalOptions) {
@@ -51,10 +50,6 @@ export async function runDiffEvalHandler(options: RunDiffEvalOptions) {
args.push("--parallel")
}
if (options.replay) {
args.push("--replay")
}
if (options.verbose) {
args.push("--verbose")
}
-1
View File
@@ -91,7 +91,6 @@ program
.option("--diff-edit-function <name>", "The diff editing function to use", "constructNewFileContentV2")
.option("--thinking-budget <tokens>", "Set the thinking tokens budget", "0")
.option("--parallel", "Run tests in parallel", false)
.option("--replay", "Run evaluation from a pre-recorded LLM output, skipping the API call", false)
.option("-v, --verbose", "Enable verbose logging", false)
.action(async (options) => {
try {
+11 -21
View File
@@ -8,7 +8,7 @@ import {
parseAssistantMessageV3,
AssistantMessageContent,
} from "./parsing/parse-assistant-message-06-06-25" // "../../src/core/assistant-message"
import { constructNewFileContent as constructNewFileContentV1, constructNewFileContentV2 } from "./diff-apply/diff-06-06-25"
import { constructNewFileContentV2 } from "./diff-apply/diff-06-06-25"
import { constructNewFileContent as constructNewFileContentV3 } from "../../src/core/assistant-message/diff" // this defaults to the new v1 when called
type ParseAssistantMessageFn = (message: string) => AssistantMessageContent[]
@@ -21,7 +21,6 @@ const parsingFunctions: Record<string, ParseAssistantMessageFn> = {
}
const diffEditingFunctions: Record<string, ConstructNewFileContentFn> = {
constructNewFileContentV1: constructNewFileContentV1,
constructNewFileContentV2: constructNewFileContentV2,
constructNewFileContentV3: constructNewFileContentV3, // position invariant diff
}
@@ -115,10 +114,10 @@ export async function runSingleEvaluation(input: TestInput): Promise<TestResult>
parsingFunction,
diffEditFunction,
thinkingBudgetTokens,
originalDiffEditToolCallMessage,
} = input
const requiredParams = {
apiKey,
systemPrompt,
messages,
modelId,
@@ -164,26 +163,17 @@ export async function runSingleEvaluation(input: TestInput): Promise<TestResult>
},
}
const openRouterHandler = new OpenRouterHandler(options)
// Get the output of streaming output of this llm call
let streamResult: StreamResult
if (originalDiffEditToolCallMessage !== undefined) {
// Replay mode: mock the stream result
streamResult = {
assistantMessage: originalDiffEditToolCallMessage,
reasoningMessage: "",
usage: { inputTokens: 0, outputTokens: 0, cacheWriteTokens: 0, cacheReadTokens: 0, totalCost: 0 },
}
} else {
// Live mode: existing API call logic
try {
const openRouterHandler = new OpenRouterHandler(options)
streamResult = await processStream(openRouterHandler, systemPrompt, messages)
} catch (error: any) {
return {
success: false,
error: "llm_stream_error",
errorString: error.message || error.toString(),
}
try {
streamResult = await processStream(openRouterHandler, systemPrompt, messages)
} catch (error: any) {
return {
success: false,
error: "llm_stream_error",
errorString: error.message || error.toString(),
}
}
+6 -22
View File
@@ -22,14 +22,12 @@ const systemPromptGeneratorLookup: Record<string, ConstructSystemPromptFn> = {
type TestResultSet = { [test_id: string]: (TestResult & { test_id?: string })[] }
class NodeTestRunner {
private apiKey: string | undefined
private apiKey: string
constructor(isReplay: boolean) {
if (!isReplay) {
this.apiKey = process.env.OPENROUTER_API_KEY
if (!this.apiKey) {
throw new Error("OPENROUTER_API_KEY environment variable not set for a non-replay run.")
}
constructor() {
this.apiKey = process.env.OPENROUTER_API_KEY!
if (!this.apiKey) {
throw new Error("OPENROUTER_API_KEY environment variable not set")
}
}
@@ -127,14 +125,6 @@ class NodeTestRunner {
* Run a single test example
*/
async runSingleTest(testCase: ProcessedTestCase, testConfig: TestConfig): Promise<TestResult> {
if (testConfig.replay && !testCase.original_diff_edit_tool_call_message) {
return {
success: false,
error: "missing_original_diff_edit_tool_call_message",
errorString: `Test case ${testCase.test_id} is missing 'original_diff_edit_tool_call_message' for replay.`,
}
}
const customSystemPrompt = this.constructSystemPrompt(testCase.system_prompt_details, testConfig.system_prompt_name)
// messages don't include system prompt and are everything up to the first replace_in_file tool call which results in a diff edit error
@@ -148,7 +138,6 @@ class NodeTestRunner {
parsingFunction: testConfig.parsing_function,
diffEditFunction: testConfig.diff_edit_function,
thinkingBudgetTokens: testConfig.thinking_tokens_budget,
originalDiffEditToolCallMessage: testConfig.replay ? testCase.original_diff_edit_tool_call_message : undefined,
}
return await runSingleEvaluation(input)
@@ -331,7 +320,6 @@ async function main() {
.option("--diff-edit-function <name>", "The diff editing function to use", "constructNewFileContentV2")
.option("--thinking-budget <tokens>", "Set the thinking tokens budget", "0")
.option("--parallel", "Run tests in parallel", false)
.option("--replay", "Run evaluation from a pre-recorded LLM output, skipping the API call", false)
.option("-v, --verbose", "Enable verbose logging", false)
program.parse(process.argv)
@@ -348,13 +336,12 @@ async function main() {
parsing_function: options.parsingFunction,
diff_edit_function: options.diffEditFunction,
thinking_tokens_budget: parseInt(options.thinkingBudget, 10),
replay: options.replay,
}
try {
const startTime = Date.now()
const runner = new NodeTestRunner(testConfig.replay)
const runner = new NodeTestRunner()
const testCases = runner.loadTestCases(testPath)
const processedTestCases: ProcessedTestCase[] = testCases.map((tc) => ({
@@ -364,9 +351,6 @@ async function main() {
log(isVerbose, `-Loaded ${testCases.length} test cases.`)
log(isVerbose, `-Executing ${testConfig.number_of_runs} run(s) per test case.`)
if (testConfig.replay) {
log(isVerbose, `-Running in REPLAY mode. No API calls will be made.`)
}
log(isVerbose, "Starting tests...\n")
const results = options.parallel
@@ -211,7 +211,7 @@ export async function constructNewFileContent(
diffContent: string,
originalContent: string,
isFinal: boolean,
version: "v1" | "v2" = "v1",
version: "v1" | "v2" = "v2",
): Promise<string> {
const constructor = constructNewFileContentVersionMapping[version]
if (!constructor) {
+1 -5
View File
@@ -13,7 +13,6 @@ export interface ProcessedTestCase {
file_contents: string
file_path: string
system_prompt_details: SystemPromptDetails
original_diff_edit_tool_call_message: string
}
export interface TestCase {
@@ -22,7 +21,6 @@ export interface TestCase {
file_contents: string
file_path: string
system_prompt_details: SystemPromptDetails
original_diff_edit_tool_call_message: string
}
export interface TestConfig {
@@ -32,7 +30,6 @@ export interface TestConfig {
parsing_function: string
diff_edit_function: string
thinking_tokens_budget: number
replay: boolean
}
export interface SystemPromptDetails {
@@ -75,7 +72,7 @@ export interface ExtractedToolCall {
}
export interface TestInput {
apiKey?: string
apiKey: string
systemPrompt: string
messages: Anthropic.Messages.MessageParam[]
modelId: string
@@ -84,5 +81,4 @@ export interface TestInput {
parsingFunction: string
diffEditFunction: string
thinkingBudgetTokens: number
originalDiffEditToolCallMessage?: string
}
@@ -1,6 +1,6 @@
import { v4 as uuidv4 } from "uuid"
import { hostServiceHandlers } from "./host-grpc-service-config"
import { GrpcRequestRegistry } from "@core/controller/grpc-request-registry"
import { GrpcRequestRegistry } from "../../src/core/controller/grpc-request-registry"
/**
* Type definition for a streaming response handler
@@ -1,6 +1,6 @@
import * as vscode from "vscode"
import { Uri } from "@shared/proto/host/uri"
import { StringRequest } from "@shared/proto/common"
import { Uri } from "../../../src/shared/proto/host/uri"
import { StringRequest } from "../../../src/shared/proto/common"
/**
* Creates a file URI from a file path
@@ -1,5 +1,5 @@
import * as vscode from "vscode"
import { JoinPathRequest, Uri } from "@shared/proto/host/uri"
import { JoinPathRequest, Uri } from "../../../src/shared/proto/host/uri"
/**
* Joins a URI with additional path segments
@@ -1,6 +1,6 @@
import * as vscode from "vscode"
import { Uri } from "@shared/proto/host/uri"
import { StringRequest } from "@shared/proto/common"
import { Uri } from "../../../src/shared/proto/host/uri"
import { StringRequest } from "../../../src/shared/proto/common"
/**
* Parses a string URI into a Uri object
@@ -1,6 +1,6 @@
import * as fs from "fs/promises"
import * as fsSync from "fs"
import { SubscribeToFileRequest, FileChangeEvent, FileChangeEvent_ChangeType } from "@shared/proto/host/watch"
import { SubscribeToFileRequest, FileChangeEvent, FileChangeEvent_ChangeType } from "../../../src/shared/proto/host/watch"
import { StreamingResponseHandler, getRequestRegistry } from "../host-grpc-handler"
// Debounce configuration
+10 -216
View File
@@ -16,7 +16,7 @@
"@bufbuild/protobuf": "^2.2.5",
"@cerebras/cerebras_cloud_sdk": "^1.35.0",
"@google-cloud/vertexai": "^1.9.3",
"@google/genai": "^0.13.0",
"@google/genai": "1.0.0",
"@grpc/grpc-js": "^1.9.15",
"@grpc/reflection": "^1.0.4",
"@mistralai/mistralai": "^1.5.0",
@@ -76,7 +76,6 @@
"zod": "^3.24.2"
},
"devDependencies": {
"@bufbuild/buf": "^1.54.0",
"@changesets/cli": "^2.27.12",
"@types/chai": "^5.0.1",
"@types/clone-deep": "^4.0.4",
@@ -4219,150 +4218,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/@bufbuild/buf": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/@bufbuild/buf/-/buf-1.54.0.tgz",
"integrity": "sha512-UkjZmVslA7YAxhUQVxE2O4HX4qD7aMspjkuG3vsjnvmAkiV6Jhz47z3focCuPI28e59H20TiQNhc9Y3fkffWPw==",
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"bin": {
"buf": "bin/buf",
"protoc-gen-buf-breaking": "bin/protoc-gen-buf-breaking",
"protoc-gen-buf-lint": "bin/protoc-gen-buf-lint"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@bufbuild/buf-darwin-arm64": "1.54.0",
"@bufbuild/buf-darwin-x64": "1.54.0",
"@bufbuild/buf-linux-aarch64": "1.54.0",
"@bufbuild/buf-linux-armv7": "1.54.0",
"@bufbuild/buf-linux-x64": "1.54.0",
"@bufbuild/buf-win32-arm64": "1.54.0",
"@bufbuild/buf-win32-x64": "1.54.0"
}
},
"node_modules/@bufbuild/buf-darwin-arm64": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/@bufbuild/buf-darwin-arm64/-/buf-darwin-arm64-1.54.0.tgz",
"integrity": "sha512-MkwlxcuHH8YO2wyQ2nGAv5SwBRCR4PtA8zcQb7AR6q93Cgy314ac8blGjfpenprjI3kAAhxc9BQK4t+/hkIS/A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@bufbuild/buf-darwin-x64": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/@bufbuild/buf-darwin-x64/-/buf-darwin-x64-1.54.0.tgz",
"integrity": "sha512-59Z+6BxvVwBbcpLOAwD8TLobngb9YUvUZ1nnP1IyIJnay/tIY+yfmgAdgMwm3VUZlbaFlURGmD34UAwEsxodGQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@bufbuild/buf-linux-aarch64": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/@bufbuild/buf-linux-aarch64/-/buf-linux-aarch64-1.54.0.tgz",
"integrity": "sha512-cUbvujfoAQGsnRH/+UfKxt0Hfe6PGHjM/gLiC2Kgv8fcoIWjPJMBBgdl/TLbq1QrVcCXSvMc16hW5ias7Jdyfw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@bufbuild/buf-linux-armv7": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/@bufbuild/buf-linux-armv7/-/buf-linux-armv7-1.54.0.tgz",
"integrity": "sha512-xdKjzPsOo6E2eth3uGIRoVG9TpPVHOUucr0MeCRVhM2hb5gbM8KQLn6iDxVGbQFq6eL2qe+B0b8k9HfuwzirWA==",
"cpu": [
"arm"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@bufbuild/buf-linux-x64": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/@bufbuild/buf-linux-x64/-/buf-linux-x64-1.54.0.tgz",
"integrity": "sha512-ZnfaE5GLAhyvR/ponDgG+s6FbtMEm+RaS2f0EoBLORYC7sK/Elfmw2Q0XcjHyEl83u4hELCqej9T0eUxbgxtow==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@bufbuild/buf-win32-arm64": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/@bufbuild/buf-win32-arm64/-/buf-win32-arm64-1.54.0.tgz",
"integrity": "sha512-N5YlX8c6p+KZIWYmx03viYF/FLuY5GyzHgor17nuJUYhF1xFyIJL8v4mhqcQ8Pq0xua9IyRwmSxHJKyrdNatcg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@bufbuild/buf-win32-x64": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/@bufbuild/buf-win32-x64/-/buf-win32-x64-1.54.0.tgz",
"integrity": "sha512-PepTA9RcLCjukQhFPFBqKXF9mVwct+ZSBeuLjFuUVcHovdGUZXspNTb5LnuIDjWXx2fcALs0xb/FNUNd6pNjbA==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@bufbuild/protobuf": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.2.5.tgz",
@@ -5643,9 +5498,9 @@
}
},
"node_modules/@google/genai": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-0.13.0.tgz",
"integrity": "sha512-eaEncWt875H7046T04mOpxpHJUM+jLIljEf+5QctRyOeChylE/nhpwm1bZWTRWoOu/t46R9r+PmgsJFhTpE7tQ==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.0.0.tgz",
"integrity": "sha512-IQiL8UlPblGDrMhTuiHZbfMDVx0KY3eYkmB5Ro9wwyXovYCFIhL5ZC7LP42FjFUj0eWUa4Auo8Ixqf2dqx9JjA==",
"license": "Apache-2.0",
"dependencies": {
"google-auth-library": "^9.14.2",
@@ -5655,6 +5510,9 @@
},
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@modelcontextprotocol/sdk": "^1.11.0"
}
},
"node_modules/@grpc/grpc-js": {
@@ -30226,70 +30084,6 @@
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
"dev": true
},
"@bufbuild/buf": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/@bufbuild/buf/-/buf-1.54.0.tgz",
"integrity": "sha512-UkjZmVslA7YAxhUQVxE2O4HX4qD7aMspjkuG3vsjnvmAkiV6Jhz47z3focCuPI28e59H20TiQNhc9Y3fkffWPw==",
"dev": true,
"requires": {
"@bufbuild/buf-darwin-arm64": "1.54.0",
"@bufbuild/buf-darwin-x64": "1.54.0",
"@bufbuild/buf-linux-aarch64": "1.54.0",
"@bufbuild/buf-linux-armv7": "1.54.0",
"@bufbuild/buf-linux-x64": "1.54.0",
"@bufbuild/buf-win32-arm64": "1.54.0",
"@bufbuild/buf-win32-x64": "1.54.0"
}
},
"@bufbuild/buf-darwin-arm64": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/@bufbuild/buf-darwin-arm64/-/buf-darwin-arm64-1.54.0.tgz",
"integrity": "sha512-MkwlxcuHH8YO2wyQ2nGAv5SwBRCR4PtA8zcQb7AR6q93Cgy314ac8blGjfpenprjI3kAAhxc9BQK4t+/hkIS/A==",
"dev": true,
"optional": true
},
"@bufbuild/buf-darwin-x64": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/@bufbuild/buf-darwin-x64/-/buf-darwin-x64-1.54.0.tgz",
"integrity": "sha512-59Z+6BxvVwBbcpLOAwD8TLobngb9YUvUZ1nnP1IyIJnay/tIY+yfmgAdgMwm3VUZlbaFlURGmD34UAwEsxodGQ==",
"dev": true,
"optional": true
},
"@bufbuild/buf-linux-aarch64": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/@bufbuild/buf-linux-aarch64/-/buf-linux-aarch64-1.54.0.tgz",
"integrity": "sha512-cUbvujfoAQGsnRH/+UfKxt0Hfe6PGHjM/gLiC2Kgv8fcoIWjPJMBBgdl/TLbq1QrVcCXSvMc16hW5ias7Jdyfw==",
"dev": true,
"optional": true
},
"@bufbuild/buf-linux-armv7": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/@bufbuild/buf-linux-armv7/-/buf-linux-armv7-1.54.0.tgz",
"integrity": "sha512-xdKjzPsOo6E2eth3uGIRoVG9TpPVHOUucr0MeCRVhM2hb5gbM8KQLn6iDxVGbQFq6eL2qe+B0b8k9HfuwzirWA==",
"dev": true,
"optional": true
},
"@bufbuild/buf-linux-x64": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/@bufbuild/buf-linux-x64/-/buf-linux-x64-1.54.0.tgz",
"integrity": "sha512-ZnfaE5GLAhyvR/ponDgG+s6FbtMEm+RaS2f0EoBLORYC7sK/Elfmw2Q0XcjHyEl83u4hELCqej9T0eUxbgxtow==",
"dev": true,
"optional": true
},
"@bufbuild/buf-win32-arm64": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/@bufbuild/buf-win32-arm64/-/buf-win32-arm64-1.54.0.tgz",
"integrity": "sha512-N5YlX8c6p+KZIWYmx03viYF/FLuY5GyzHgor17nuJUYhF1xFyIJL8v4mhqcQ8Pq0xua9IyRwmSxHJKyrdNatcg==",
"dev": true,
"optional": true
},
"@bufbuild/buf-win32-x64": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/@bufbuild/buf-win32-x64/-/buf-win32-x64-1.54.0.tgz",
"integrity": "sha512-PepTA9RcLCjukQhFPFBqKXF9mVwct+ZSBeuLjFuUVcHovdGUZXspNTb5LnuIDjWXx2fcALs0xb/FNUNd6pNjbA==",
"dev": true,
"optional": true
},
"@bufbuild/protobuf": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.2.5.tgz",
@@ -31391,9 +31185,9 @@
}
},
"@google/genai": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-0.13.0.tgz",
"integrity": "sha512-eaEncWt875H7046T04mOpxpHJUM+jLIljEf+5QctRyOeChylE/nhpwm1bZWTRWoOu/t46R9r+PmgsJFhTpE7tQ==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.0.0.tgz",
"integrity": "sha512-IQiL8UlPblGDrMhTuiHZbfMDVx0KY3eYkmB5Ro9wwyXovYCFIhL5ZC7LP42FjFUj0eWUa4Auo8Ixqf2dqx9JjA==",
"requires": {
"google-auth-library": "^9.14.2",
"ws": "^8.18.0",
+3 -4
View File
@@ -331,12 +331,12 @@
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
"package": "npm run check-types && npm run build:webview && npm run lint && node esbuild.js --production",
"protos": "node proto/build-proto.js && node scripts/generate-server-setup.mjs",
"postprotos": "prettier src/shared/proto src/core/controller src/hosts/ webview-ui/src/services src/standalone/server-setup.ts --write --log-level silent",
"postprotos": "prettier src/shared/proto src/core/controller webview-ui/src/services src/standalone/server-setup.ts --write --log-level silent",
"compile-tests": "node ./scripts/build-tests.js",
"watch-tests": "tsc -p . -w --outDir out",
"pretest": "npm run compile-tests && npm run compile && npm run compile-standalone && npm run lint",
"check-types": "npm run protos && tsc --noEmit",
"lint": "eslint src --ext ts && eslint webview-ui/src --ext ts && buf lint && cd webview-ui && npm run lint",
"lint": "eslint src --ext ts && eslint webview-ui/src --ext ts && cd webview-ui && npm run lint",
"format": "prettier . --check",
"format:fix": "prettier . --write",
"test": "npm-run-all test:unit test:integration",
@@ -359,7 +359,6 @@
"report-issue": "node scripts/report-issue.js"
},
"devDependencies": {
"@bufbuild/buf": "^1.54.0",
"@changesets/cli": "^2.27.12",
"@types/chai": "^5.0.1",
"@types/clone-deep": "^4.0.4",
@@ -405,7 +404,7 @@
"@bufbuild/protobuf": "^2.2.5",
"@cerebras/cerebras_cloud_sdk": "^1.35.0",
"@google-cloud/vertexai": "^1.9.3",
"@google/genai": "^0.13.0",
"@google/genai": "1.0.0",
"@grpc/grpc-js": "^1.9.15",
"@grpc/reflection": "^1.0.4",
"@mistralai/mistralai": "^1.5.0",
+16 -48
View File
@@ -15,24 +15,12 @@ const protoc = path.join(require.resolve("grpc-tools"), "../bin/protoc")
const __filename = fileURLToPath(import.meta.url)
const SCRIPT_DIR = path.dirname(__filename)
const ROOT_DIR = path.resolve(SCRIPT_DIR, "..")
const TS_OUT_DIR = path.join(ROOT_DIR, "src", "shared", "proto")
const isWindows = process.platform === "win32"
const tsProtoPlugin = isWindows
? path.join(ROOT_DIR, "node_modules", ".bin", "protoc-gen-ts_proto.cmd") // Use the .bin directory path for Windows
: require.resolve("ts-proto/protoc-gen-ts_proto")
const TS_PROTO_OPTIONS = [
"env=node",
"esModuleInterop=true",
"outputIndex=true", // output an index file for each package which exports all protos in the package.
"outputServices=generic-definitions",
"useOptionals=messages", // Message fields are optional, scalars are not.
"useDate=false", // Timestamp fields will not be automatically converted to Date.
]
// List of gRPC services
// To add a new service, simply add it to this map and run this script
// The service handler will be automatically discovered and used by grpc-handler.ts
@@ -59,9 +47,7 @@ const hostServiceNameMap = {
watch: "host.WatchService",
// Add new host services here
}
const hostServiceDirs = Object.keys(hostServiceNameMap).map((serviceKey) =>
path.join(ROOT_DIR, "src", "hosts", "vscode", serviceKey),
)
const hostServiceDirs = Object.keys(hostServiceNameMap).map((serviceKey) => path.join(ROOT_DIR, "hosts", "vscode", serviceKey))
async function main() {
console.log(chalk.bold.blue("Starting Protocol Buffer code generation..."))
@@ -69,9 +55,18 @@ async function main() {
// Check for Apple Silicon compatibility before proceeding
checkAppleSiliconCompatibility()
// Define output directories
const TS_OUT_DIR = path.join(ROOT_DIR, "src", "shared", "proto")
// Create output directories if they don't exist
await fs.mkdir(TS_OUT_DIR, { recursive: true })
await cleanup()
// Clean up existing generated files
console.log(chalk.cyan("Cleaning up existing generated TypeScript files..."))
const existingFiles = await globby("**/*.ts", { cwd: TS_OUT_DIR })
for (const file of existingFiles) {
await fs.unlink(path.join(TS_OUT_DIR, file))
}
// Check for missing proto files for services in serviceNameMap
await ensureProtoFilesExist()
@@ -86,7 +81,9 @@ async function main() {
`--proto_path="${SCRIPT_DIR}"`,
`--plugin=protoc-gen-ts_proto="${tsProtoPlugin}"`,
`--ts_proto_out="${TS_OUT_DIR}"`,
`--ts_proto_opt=${TS_PROTO_OPTIONS.join(",")} `,
"--ts_proto_opt=exportCommonSymbols=false",
"--ts_proto_opt=outputIndex=true",
"--ts_proto_opt=outputServices=generic-definitions,env=node,esModuleInterop=true,useDate=false,useOptionals=messages",
...protoFiles,
].join(" ")
try {
@@ -595,7 +592,7 @@ export interface HostServiceHandlerConfig {
export const hostServiceHandlers: Record<string, HostServiceHandlerConfig> = {${serviceConfigs.join(",")}
};`
const configPath = path.join(ROOT_DIR, "src", "hosts", "vscode", "host-grpc-service-config.ts")
const configPath = path.join(ROOT_DIR, "hosts", "vscode", "host-grpc-service-config.ts")
await fs.mkdir(path.dirname(configPath), { recursive: true })
await fs.writeFile(configPath, content)
console.log(chalk.green(`Generated host service configuration at ${configPath}`))
@@ -640,41 +637,12 @@ export {
${serviceExports.join(",\n\t")}
}`
const configPath = path.join(ROOT_DIR, "src", "hosts", "vscode", "client", "host-grpc-client.ts")
const configPath = path.join(ROOT_DIR, "src", "standalone", "services", "host-grpc-client.ts")
await fs.mkdir(path.dirname(configPath), { recursive: true })
await fs.writeFile(configPath, content)
console.log(chalk.green(`Generated host gRPC client at ${configPath}`))
}
async function cleanup() {
// Clean up existing generated files
console.log(chalk.cyan("Cleaning up existing generated TypeScript files..."))
const existingFiles = await globby("**/*.ts", { cwd: TS_OUT_DIR })
for (const file of existingFiles) {
await fs.unlink(path.join(TS_OUT_DIR, file))
}
// Clean up generated files that were moved.
await fs.rm(path.join(ROOT_DIR, "src", "standalone", "services", "host-grpc-client.ts"), { force: true })
await rmdir(path.join(ROOT_DIR, "src", "standalone", "services"))
await fs.rm(path.join(ROOT_DIR, "hosts", "vscode"), { force: true, recursive: true })
await rmdir(path.join(ROOT_DIR, "hosts"))
}
/**
* Remove an empty dir, do nothing if the directory doesn't exist or is not empty.
*/
async function rmdir(path) {
try {
await fs.rmdir(path)
} catch (error) {
if (error.code !== "ENOTEMPTY" && error.code !== "ENOENT") {
// Only re-throw if it's not "not empty" or "doesn't exist"
throw error
}
}
}
// Check for Apple Silicon compatibility
function checkAppleSiliconCompatibility() {
// Only run check on macOS
+2 -2
View File
@@ -25,12 +25,12 @@ message Uri {
string path = 3;
string query = 4;
string fragment = 5;
string fs_path = 6;
string fsPath = 6;
}
// Request for joining path segments to a URI
message JoinPathRequest {
cline.Metadata metadata = 1;
Uri base = 2;
repeated string path_segments = 3;
repeated string pathSegments = 3;
}
+9 -154
View File
@@ -22,21 +22,19 @@ service ModelsService {
rpc refreshRequestyModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
// Subscribe to OpenRouter models updates
rpc subscribeToOpenRouterModels(EmptyRequest) returns (stream OpenRouterCompatibleModelInfo);
// Updates API configuration
rpc updateApiConfigurationProto(UpdateApiConfigurationRequest) returns (Empty);
}
// List of VS Code LM models
message VsCodeLmModelsArray {
repeated LanguageModelChatSelector models = 1;
repeated VsCodeLmModel models = 1;
}
// Structure representing a language model chat selector
message LanguageModelChatSelector {
optional string vendor = 1;
optional string family = 2;
optional string version = 3;
optional string id = 4;
// Structure representing a VS Code LM model
message VsCodeLmModel {
string vendor = 1;
string family = 2;
string version = 3;
string id = 4;
}
// Price tier for tiered pricing models
@@ -85,149 +83,6 @@ message OpenRouterCompatibleModelInfo {
// Request for fetching OpenAI models
message OpenAiModelsRequest {
Metadata metadata = 1;
string base_url = 2;
string api_key = 3;
}
// Request for updating API configuration
message UpdateApiConfigurationRequest {
Metadata metadata = 1;
ModelsApiConfiguration api_configuration = 2;
}
// API Provider enumeration
enum ApiProvider {
ANTHROPIC = 0;
OPENROUTER = 1;
BEDROCK = 2;
VERTEX = 3;
OPENAI = 4;
OLLAMA = 5;
LMSTUDIO = 6;
GEMINI = 7;
OPENAI_NATIVE = 8;
REQUESTY = 9;
TOGETHER = 10;
DEEPSEEK = 11;
QWEN = 12;
DOUBAO = 13;
MISTRAL = 14;
VSCODE_LM = 15;
CLINE = 16;
LITELLM = 17;
NEBIUS = 18;
FIREWORKS = 19;
ASKSAGE = 20;
XAI = 21;
SAMBANOVA = 22;
CEREBRAS = 23;
}
// Model info for OpenAI-compatible models
message OpenAiCompatibleModelInfo {
optional int32 max_tokens = 1;
optional int32 context_window = 2;
optional bool supports_images = 3;
bool supports_prompt_cache = 4;
optional double input_price = 5;
optional double output_price = 6;
optional ThinkingConfig thinking_config = 7;
optional bool supports_global_endpoint = 8;
optional double cache_writes_price = 9;
optional double cache_reads_price = 10;
optional string description = 11;
repeated ModelTier tiers = 12;
optional double temperature = 13;
optional bool is_r1_format_required = 14;
}
// Model info for LiteLLM models
message LiteLLMModelInfo {
optional int32 max_tokens = 1;
optional int32 context_window = 2;
optional bool supports_images = 3;
bool supports_prompt_cache = 4;
optional double input_price = 5;
optional double output_price = 6;
optional ThinkingConfig thinking_config = 7;
optional bool supports_global_endpoint = 8;
optional double cache_writes_price = 9;
optional double cache_reads_price = 10;
optional string description = 11;
repeated ModelTier tiers = 12;
optional double temperature = 13;
}
// Main ApiConfiguration message
message ModelsApiConfiguration {
// From ApiHandlerOptions (excluding onRetryAttempt function)
optional string api_model_id = 1;
optional string api_key = 2;
optional string cline_api_key = 3;
optional string task_id = 4;
optional string lite_llm_base_url = 5;
optional string lite_llm_model_id = 6;
optional string lite_llm_api_key = 7;
optional bool lite_llm_use_prompt_cache = 8;
map<string, string> open_ai_headers = 9;
optional LiteLLMModelInfo lite_llm_model_info = 10;
optional string anthropic_base_url = 11;
optional string open_router_api_key = 12;
optional string open_router_model_id = 13;
optional OpenRouterModelInfo open_router_model_info = 14;
optional string open_router_provider_sorting = 15;
optional string aws_access_key = 16;
optional string aws_secret_key = 17;
optional string aws_session_token = 18;
optional string aws_region = 19;
optional bool aws_use_cross_region_inference = 20;
optional bool aws_bedrock_use_prompt_cache = 21;
optional bool aws_use_profile = 22;
optional string aws_profile = 23;
optional string aws_bedrock_endpoint = 24;
optional bool aws_bedrock_custom_selected = 25;
optional string aws_bedrock_custom_model_base_id = 26;
optional string vertex_project_id = 27;
optional string vertex_region = 28;
optional string open_ai_base_url = 29;
optional string open_ai_api_key = 30;
optional string open_ai_model_id = 31;
optional OpenAiCompatibleModelInfo open_ai_model_info = 32;
optional string ollama_model_id = 33;
optional string ollama_base_url = 34;
optional string ollama_api_options_ctx_num = 35;
optional string lm_studio_model_id = 36;
optional string lm_studio_base_url = 37;
optional string gemini_api_key = 38;
optional string gemini_base_url = 39;
optional string open_ai_native_api_key = 40;
optional string deep_seek_api_key = 41;
optional string requesty_api_key = 42;
optional string requesty_model_id = 43;
optional OpenRouterModelInfo requesty_model_info = 44;
optional string together_api_key = 45;
optional string together_model_id = 46;
optional string fireworks_api_key = 47;
optional string fireworks_model_id = 48;
optional int32 fireworks_model_max_completion_tokens = 49;
optional int32 fireworks_model_max_tokens = 50;
optional string qwen_api_key = 51;
optional string doubao_api_key = 52;
optional string mistral_api_key = 53;
optional string azure_api_version = 54;
optional LanguageModelChatSelector vs_code_lm_model_selector = 55;
optional string qwen_api_line = 56;
optional string nebius_api_key = 57;
optional string asksage_api_url = 58;
optional string asksage_api_key = 59;
optional string xai_api_key = 60;
optional int32 thinking_budget_tokens = 61;
optional string reasoning_effort = 62;
optional string sambanova_api_key = 63;
optional string cerebras_api_key = 64;
optional int32 request_timeout_ms = 65;
// From ApiConfiguration (additional fields)
optional ApiProvider api_provider = 66;
repeated string favorited_model_ids = 67;
string baseUrl = 2;
string apiKey = 3;
}
+10 -8
View File
@@ -11,6 +11,7 @@ service StateService {
rpc toggleFavoriteModel(StringRequest) returns (Empty);
rpc resetState(EmptyRequest) returns (Empty);
rpc togglePlanActMode(TogglePlanActModeRequest) returns (Empty);
rpc updateTerminalConnectionTimeout(Int64Request) returns (Int64);
rpc updateAutoApprovalSettings(AutoApprovalSettingsRequest) returns (Empty);
rpc updateSettings(UpdateSettingsRequest) returns (Empty);
}
@@ -69,14 +70,15 @@ message AutoApprovalSettingsRequest {
message UpdateSettingsRequest {
Metadata metadata = 1;
optional ApiConfiguration api_configuration = 2;
optional string telemetry_setting = 3;
optional bool plan_act_separate_models_setting = 4;
optional bool enable_checkpoints_setting = 5;
optional bool mcp_marketplace_enabled = 6;
optional ChatSettings chat_settings = 7;
optional int64 shell_integration_timeout = 8;
optional bool terminal_reuse_enabled = 9;
optional bool mcp_responses_collapsed = 10;
optional string custom_instructions_setting = 3;
optional string telemetry_setting = 4;
optional bool plan_act_separate_models_setting = 5;
optional bool enable_checkpoints_setting = 6;
optional bool mcp_marketplace_enabled = 7;
optional ChatSettings chat_settings = 8;
optional int64 shell_integration_timeout = 9;
optional bool terminal_reuse_enabled = 10;
optional bool mcp_responses_collapsed = 11;
}
// Complete API Configuration message
+8 -12
View File
@@ -15,7 +15,7 @@ enum WebviewProviderType {
// Define a new message type for webview provider info
message WebviewProviderTypeRequest {
Metadata metadata = 1;
WebviewProviderType provider_type = 2;
WebviewProviderType providerType = 2;
}
// Enum for ClineMessage type
@@ -65,14 +65,13 @@ enum ClineSay {
BROWSER_ACTION_RESULT = 16;
MCP_SERVER_REQUEST_STARTED = 17;
MCP_SERVER_RESPONSE = 18;
MCP_NOTIFICATION = 19;
USE_MCP_SERVER_SAY = 20;
DIFF_ERROR = 21;
DELETED_API_REQS = 22;
CLINEIGNORE_ERROR = 23;
CHECKPOINT_CREATED = 24;
LOAD_MCP_DOCUMENTATION = 25;
INFO = 26;
USE_MCP_SERVER_SAY = 19;
DIFF_ERROR = 20;
DELETED_API_REQS = 21;
CLINEIGNORE_ERROR = 22;
CHECKPOINT_CREATED = 23;
LOAD_MCP_DOCUMENTATION = 24;
INFO = 25;
}
// Enum for ClineSayTool tool types
@@ -256,7 +255,4 @@ service UiService {
// Subscribe to relinquish control events
rpc subscribeToRelinquishControl(EmptyRequest) returns (stream Empty);
// Subscribe to focus chat input events with client ID
rpc subscribeToFocusChatInput(StringRequest) returns (stream Empty);
}
+6 -59
View File
@@ -35,59 +35,6 @@ interface ExtendedMetadata {
}
}
// Define types for stream response content blocks
interface ContentBlockStart {
contentBlockIndex?: number
start?: {
type?: string
thinking?: string
}
contentBlock?: {
type?: string
thinking?: string
}
type?: string
thinking?: string
}
// Define types for stream response deltas
interface ContentBlockDelta {
contentBlockIndex?: number
delta?: {
type?: string
thinking?: string
text?: string
reasoningContent?: {
text?: string
}
}
}
// Define types for supported content types
type SupportedContentType = "text" | "image" | "thinking"
interface ContentItem {
type: SupportedContentType
text?: string
source?: {
data: string | Buffer | Uint8Array
media_type?: string
}
}
// Define cache point type for AWS Bedrock
interface CachePointContentBlock {
cachePoint: {
type: "default"
}
}
// Define provider options type based on AWS SDK patterns
interface ProviderChainOptions {
ignoreCache?: boolean
profile?: string
}
// https://docs.anthropic.com/en/api/claude-on-amazon-bedrock
export class AwsBedrockHandler implements ApiHandler {
private options: ApiHandlerOptions
@@ -160,7 +107,7 @@ export class AwsBedrockHandler implements ApiHandler {
sessionToken?: string
}> {
// Configure provider options
const providerOptions: ProviderChainOptions = {}
const providerOptions: any = {}
if (this.options.awsUseProfile) {
// For profile-based auth, always use ignoreCache to detect credential file changes
// This solves the AWS Identity Manager issue where credential files change externally
@@ -514,7 +461,7 @@ export class AwsBedrockHandler implements ApiHandler {
// Handle content block start - check if Bedrock uses Anthropic SDK format
if (chunk.contentBlockStart) {
const blockStart = chunk.contentBlockStart as ContentBlockStart
const blockStart = chunk.contentBlockStart as any
const blockIndex = chunk.contentBlockStart.contentBlockIndex
// Check for thinking block in various possible formats
@@ -550,7 +497,7 @@ export class AwsBedrockHandler implements ApiHandler {
// Check if this is a thinking block
const blockType = blockTypes.get(blockIndex)
const delta = chunk.contentBlockDelta.delta as ContentBlockDelta["delta"]
const delta = chunk.contentBlockDelta.delta as any
// Handle thinking delta (Anthropic SDK format)
if (delta?.type === "thinking_delta" || delta?.thinking) {
@@ -780,7 +727,7 @@ export class AwsBedrockHandler implements ApiHandler {
}
// Log unsupported content types for debugging
console.warn(`Unsupported content type: ${(item as ContentItem).type}`)
console.warn(`Unsupported content type: ${(item as any).type}`)
return null
})
.filter((item): item is ContentBlock => item !== null)
@@ -824,7 +771,7 @@ export class AwsBedrockHandler implements ApiHandler {
imageData = new Uint8Array(Buffer.from(base64Data, "base64"))
} else if (item.source.data && typeof item.source.data === "object") {
// Try to convert to Uint8Array
imageData = new Uint8Array(Buffer.from(item.source.data as Buffer | Uint8Array))
imageData = new Uint8Array(Buffer.from(item.source.data as any))
} else {
throw new Error("Unsupported image data format")
}
@@ -870,7 +817,7 @@ export class AwsBedrockHandler implements ApiHandler {
cachePoint: {
type: "default",
},
} as CachePointContentBlock, // Properly typed cache point for AWS SDK
} as any, // Type assertion needed for AWS SDK compatibility
]
}
+37 -4
View File
@@ -2,6 +2,7 @@ import type { Anthropic } from "@anthropic-ai/sdk"
// Restore GenerateContentConfig import and add GenerateContentResponseUsageMetadata
import { GoogleGenAI, type GenerateContentConfig, type GenerateContentResponseUsageMetadata } from "@google/genai"
import { withRetry } from "../retry"
import { Part } from "@google/genai"
import { ApiHandler } from "../"
import { ApiHandlerOptions, geminiDefaultModelId, GeminiModelId, geminiModels, ModelInfo } from "@shared/api"
import { convertAnthropicMessageToGemini } from "../transform/gemini-format"
@@ -96,9 +97,10 @@ export class GeminiHandler implements ApiHandler {
}
// Add thinking config if the model supports it
if (info.thinkingConfig?.outputPrice !== undefined && maxBudget > 0) {
if (thinkingBudget > 0) {
requestConfig.thinkingConfig = {
thinkingBudget: thinkingBudget,
includeThoughts: true,
}
}
@@ -111,6 +113,7 @@ export class GeminiHandler implements ApiHandler {
let promptTokens = 0
let outputTokens = 0
let cacheReadTokens = 0
let thoughtsTokenCount = 0 // Initialize thought token counts
let lastUsageMetadata: GenerateContentResponseUsageMetadata | undefined
try {
@@ -130,6 +133,31 @@ export class GeminiHandler implements ApiHandler {
isFirstSdkChunk = false
}
// Handle thinking content from Gemini's response
const candidateForThoughts = chunk?.candidates?.[0]
const partsForThoughts = candidateForThoughts?.content?.parts
let thoughts = "" // Initialize as empty string
if (partsForThoughts) {
// This ensures partsForThoughts is a Part[] array
for (const part of partsForThoughts) {
const { thought, text } = part as Part
if (thought && text) {
// Ensure part.text exists
// Handle the thought part
thoughts += text + "\n" // Append thought and a newline
}
}
}
if (thoughts.trim() !== "") {
yield {
type: "reasoning",
reasoning: thoughts.trim(),
}
thoughts = "" // Reset thoughts after yielding
}
if (chunk.text) {
yield {
type: "text",
@@ -141,6 +169,7 @@ export class GeminiHandler implements ApiHandler {
lastUsageMetadata = chunk.usageMetadata
promptTokens = lastUsageMetadata.promptTokenCount ?? promptTokens
outputTokens = lastUsageMetadata.candidatesTokenCount ?? outputTokens
thoughtsTokenCount = lastUsageMetadata.thoughtsTokenCount ?? thoughtsTokenCount
cacheReadTokens = lastUsageMetadata.cachedContentTokenCount ?? cacheReadTokens
}
}
@@ -151,12 +180,14 @@ export class GeminiHandler implements ApiHandler {
info,
inputTokens: promptTokens,
outputTokens,
thoughtsTokenCount,
cacheReadTokens,
})
yield {
type: "usage",
inputTokens: promptTokens,
outputTokens,
thoughtsTokenCount,
cacheReadTokens,
cacheWriteTokens: 0,
totalCost,
@@ -239,11 +270,13 @@ export class GeminiHandler implements ApiHandler {
info,
inputTokens,
outputTokens,
thoughtsTokenCount = 0,
cacheReadTokens = 0,
}: {
info: ModelInfo
inputTokens: number
outputTokens: number
thoughtsTokenCount: number
cacheReadTokens?: number
}) {
// Exit early if any required pricing information is missing
@@ -275,18 +308,18 @@ export class GeminiHandler implements ApiHandler {
const inputTokensCost = inputPrice * (uncachedInputTokens / 1_000_000)
// 2. Output token costs
const outputTokensCost = outputPrice * (outputTokens / 1_000_000)
const responseTokensCost = outputPrice * ((outputTokens + thoughtsTokenCount) / 1_000_000)
// 3. Cache read costs (immediate)
const cacheReadCost = (cacheReadTokens ?? 0) > 0 ? cacheReadsPrice * ((cacheReadTokens ?? 0) / 1_000_000) : 0
// Calculate total immediate cost (excluding cache write/storage costs)
const totalCost = inputTokensCost + outputTokensCost + cacheReadCost
const totalCost = inputTokensCost + responseTokensCost + cacheReadCost
// Create the trace object for debugging
const trace: Record<string, { price: number; tokens: number; cost: number }> = {
input: { price: inputPrice, tokens: uncachedInputTokens, cost: inputTokensCost },
output: { price: outputPrice, tokens: outputTokens, cost: outputTokensCost },
output: { price: outputPrice, tokens: outputTokens, cost: responseTokensCost },
}
// Only include cache read costs in the trace (cache write costs are tracked separately)
+1
View File
@@ -17,5 +17,6 @@ export interface ApiStreamUsageChunk {
outputTokens: number
cacheWriteTokens?: number
cacheReadTokens?: number
thoughtsTokenCount?: number // openrouter
totalCost?: number // openrouter
}
+1 -1
View File
@@ -3,7 +3,7 @@ import { RelativePathsRequest, RelativePaths } from "@shared/proto/file"
import { FileMethodHandler } from "./index"
import * as vscode from "vscode"
import * as path from "path"
import { UriServiceClient } from "@hosts/host-bridge-client"
import { UriServiceClient } from "../../../standalone/services/host-grpc-client"
import { Metadata, StringRequest } from "@shared/proto/common"
/**
+61 -41
View File
@@ -114,7 +114,7 @@ export class Controller {
try {
await storeSecret(this.context, "clineApiKey", undefined)
await updateGlobalState(this.context, "userInfo", undefined)
await updateWorkspaceState(this.context, "apiProvider", "openrouter")
await updateGlobalState(this.context, "apiProvider", "openrouter")
await this.postStateToWebview()
vscode.window.showInformationMessage("Successfully logged out of Cline")
} catch (error) {
@@ -130,6 +130,7 @@ export class Controller {
await this.clearTask() // ensures that an existing task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one
const {
apiConfiguration,
customInstructions,
autoApprovalSettings,
browserSettings,
chatSettings,
@@ -171,6 +172,7 @@ export class Controller {
shellIntegrationTimeout,
terminalReuseEnabled ?? true,
enableCheckpointsSetting ?? true,
customInstructions,
task,
images,
files,
@@ -202,7 +204,15 @@ export class Controller {
await this.setUserInfo(message.user || undefined)
await this.postStateToWebview()
break
case "apiConfiguration":
if (message.apiConfiguration) {
await updateApiConfiguration(this.context, message.apiConfiguration)
if (this.task) {
this.task.api = buildApiHandler(message.apiConfiguration)
}
}
await this.postStateToWebview()
break
case "fetchUserCreditsData": {
await this.fetchUserCreditsData()
break
@@ -287,9 +297,9 @@ export class Controller {
if (shouldSwitchModel) {
// Save the last model used in this mode
await updateWorkspaceState(this.context, "previousModeApiProvider", apiConfiguration.apiProvider)
await updateWorkspaceState(this.context, "previousModeThinkingBudgetTokens", apiConfiguration.thinkingBudgetTokens)
await updateWorkspaceState(this.context, "previousModeReasoningEffort", apiConfiguration.reasoningEffort)
await updateGlobalState(this.context, "previousModeApiProvider", apiConfiguration.apiProvider)
await updateGlobalState(this.context, "previousModeThinkingBudgetTokens", apiConfiguration.thinkingBudgetTokens)
await updateGlobalState(this.context, "previousModeReasoningEffort", apiConfiguration.reasoningEffort)
switch (apiConfiguration.apiProvider) {
case "anthropic":
case "vertex":
@@ -299,16 +309,16 @@ export class Controller {
case "qwen":
case "deepseek":
case "xai":
await updateWorkspaceState(this.context, "previousModeModelId", apiConfiguration.apiModelId)
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.apiModelId)
break
case "bedrock":
await updateWorkspaceState(this.context, "previousModeModelId", apiConfiguration.apiModelId)
await updateWorkspaceState(
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.apiModelId)
await updateGlobalState(
this.context,
"previousModeAwsBedrockCustomSelected",
apiConfiguration.awsBedrockCustomSelected,
)
await updateWorkspaceState(
await updateGlobalState(
this.context,
"previousModeAwsBedrockCustomModelBaseId",
apiConfiguration.awsBedrockCustomModelBaseId,
@@ -316,34 +326,34 @@ export class Controller {
break
case "openrouter":
case "cline":
await updateWorkspaceState(this.context, "previousModeModelId", apiConfiguration.openRouterModelId)
await updateWorkspaceState(this.context, "previousModeModelInfo", apiConfiguration.openRouterModelInfo)
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.openRouterModelId)
await updateGlobalState(this.context, "previousModeModelInfo", apiConfiguration.openRouterModelInfo)
break
case "vscode-lm":
// Important we don't set modelId to this, as it's an object not string (webview expects model id to be a string)
await updateWorkspaceState(
await updateGlobalState(
this.context,
"previousModeVsCodeLmModelSelector",
apiConfiguration.vsCodeLmModelSelector,
)
break
case "openai":
await updateWorkspaceState(this.context, "previousModeModelId", apiConfiguration.openAiModelId)
await updateWorkspaceState(this.context, "previousModeModelInfo", apiConfiguration.openAiModelInfo)
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.openAiModelId)
await updateGlobalState(this.context, "previousModeModelInfo", apiConfiguration.openAiModelInfo)
break
case "ollama":
await updateWorkspaceState(this.context, "previousModeModelId", apiConfiguration.ollamaModelId)
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.ollamaModelId)
break
case "lmstudio":
await updateWorkspaceState(this.context, "previousModeModelId", apiConfiguration.lmStudioModelId)
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.lmStudioModelId)
break
case "litellm":
await updateWorkspaceState(this.context, "previousModeModelId", apiConfiguration.liteLlmModelId)
await updateWorkspaceState(this.context, "previousModeModelInfo", apiConfiguration.liteLlmModelInfo)
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.liteLlmModelId)
await updateGlobalState(this.context, "previousModeModelInfo", apiConfiguration.liteLlmModelInfo)
break
case "requesty":
await updateWorkspaceState(this.context, "previousModeModelId", apiConfiguration.requestyModelId)
await updateWorkspaceState(this.context, "previousModeModelInfo", apiConfiguration.requestyModelInfo)
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.requestyModelId)
await updateGlobalState(this.context, "previousModeModelInfo", apiConfiguration.requestyModelInfo)
break
}
@@ -355,9 +365,9 @@ export class Controller {
newReasoningEffort ||
newVsCodeLmModelSelector
) {
await updateWorkspaceState(this.context, "apiProvider", newApiProvider)
await updateWorkspaceState(this.context, "thinkingBudgetTokens", newThinkingBudgetTokens)
await updateWorkspaceState(this.context, "reasoningEffort", newReasoningEffort)
await updateGlobalState(this.context, "apiProvider", newApiProvider)
await updateGlobalState(this.context, "thinkingBudgetTokens", newThinkingBudgetTokens)
await updateGlobalState(this.context, "reasoningEffort", newReasoningEffort)
switch (newApiProvider) {
case "anthropic":
case "vertex":
@@ -367,38 +377,38 @@ export class Controller {
case "qwen":
case "deepseek":
case "xai":
await updateWorkspaceState(this.context, "apiModelId", newModelId)
await updateGlobalState(this.context, "apiModelId", newModelId)
break
case "bedrock":
await updateWorkspaceState(this.context, "apiModelId", newModelId)
await updateWorkspaceState(this.context, "awsBedrockCustomSelected", newAwsBedrockCustomSelected)
await updateWorkspaceState(this.context, "awsBedrockCustomModelBaseId", newAwsBedrockCustomModelBaseId)
await updateGlobalState(this.context, "apiModelId", newModelId)
await updateGlobalState(this.context, "awsBedrockCustomSelected", newAwsBedrockCustomSelected)
await updateGlobalState(this.context, "awsBedrockCustomModelBaseId", newAwsBedrockCustomModelBaseId)
break
case "openrouter":
case "cline":
await updateWorkspaceState(this.context, "openRouterModelId", newModelId)
await updateWorkspaceState(this.context, "openRouterModelInfo", newModelInfo)
await updateGlobalState(this.context, "openRouterModelId", newModelId)
await updateGlobalState(this.context, "openRouterModelInfo", newModelInfo)
break
case "vscode-lm":
await updateWorkspaceState(this.context, "vsCodeLmModelSelector", newVsCodeLmModelSelector)
await updateGlobalState(this.context, "vsCodeLmModelSelector", newVsCodeLmModelSelector)
break
case "openai":
await updateWorkspaceState(this.context, "openAiModelId", newModelId)
await updateWorkspaceState(this.context, "openAiModelInfo", newModelInfo)
await updateGlobalState(this.context, "openAiModelId", newModelId)
await updateGlobalState(this.context, "openAiModelInfo", newModelInfo)
break
case "ollama":
await updateWorkspaceState(this.context, "ollamaModelId", newModelId)
await updateGlobalState(this.context, "ollamaModelId", newModelId)
break
case "lmstudio":
await updateWorkspaceState(this.context, "lmStudioModelId", newModelId)
await updateGlobalState(this.context, "lmStudioModelId", newModelId)
break
case "litellm":
await updateWorkspaceState(this.context, "previousModeModelId", apiConfiguration.liteLlmModelId)
await updateWorkspaceState(this.context, "previousModeModelInfo", apiConfiguration.liteLlmModelInfo)
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.liteLlmModelId)
await updateGlobalState(this.context, "previousModeModelInfo", apiConfiguration.liteLlmModelInfo)
break
case "requesty":
await updateWorkspaceState(this.context, "requestyModelId", newModelId)
await updateWorkspaceState(this.context, "requestyModelInfo", newModelInfo)
await updateGlobalState(this.context, "requestyModelId", newModelId)
await updateGlobalState(this.context, "requestyModelInfo", newModelInfo)
break
}
@@ -409,7 +419,7 @@ export class Controller {
}
}
await updateWorkspaceState(this.context, "chatSettings", chatSettings)
await updateGlobalState(this.context, "chatSettings", chatSettings)
await this.postStateToWebview()
if (this.task) {
@@ -458,6 +468,14 @@ export class Controller {
}
}
async updateCustomInstructions(instructions?: string) {
// User may be clearing the field
await updateGlobalState(this.context, "customInstructions", instructions || undefined)
if (this.task) {
this.task.customInstructions = instructions || undefined
}
}
// Account
async fetchUserCreditsData() {
@@ -492,7 +510,7 @@ export class Controller {
await sendAuthCallbackEvent(customToken)
const clineProvider: ApiProvider = "cline"
await updateWorkspaceState(this.context, "apiProvider", clineProvider)
await updateGlobalState(this.context, "apiProvider", clineProvider)
// Update API configuration with the new provider and API key
const { apiConfiguration } = await getAllExtensionState(this.context)
@@ -651,7 +669,7 @@ export class Controller {
}
const openrouter: ApiProvider = "openrouter"
await updateWorkspaceState(this.context, "apiProvider", openrouter)
await updateGlobalState(this.context, "apiProvider", openrouter)
await storeSecret(this.context, "openRouterApiKey", apiKey)
await this.postStateToWebview()
if (this.task) {
@@ -939,6 +957,7 @@ export class Controller {
const {
apiConfiguration,
lastShownAnnouncementId,
customInstructions,
taskHistory,
autoApprovalSettings,
browserSettings,
@@ -970,6 +989,7 @@ export class Controller {
return {
version: this.context.extension?.packageJSON?.version ?? "",
apiConfiguration,
customInstructions,
uriScheme: vscode.env.uriScheme,
currentTaskItem: this.task?.taskId ? (taskHistory || []).find((item) => item.id === this.task?.taskId) : undefined,
checkpointTrackerErrorMessage: this.task?.checkpointTrackerErrorMessage,
@@ -99,11 +99,6 @@ export async function refreshOpenRouterModels(
modelInfo.cacheWritesPrice = 0.14
modelInfo.cacheReadsPrice = 0.014
break
case "x-ai/grok-3-beta":
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = 0
modelInfo.cacheReadsPrice = 0
break
default:
if (rawModel.id.startsWith("openai/")) {
modelInfo.cacheReadsPrice = parsePrice(rawModel.pricing?.input_cache_read)
@@ -1,43 +0,0 @@
import type { Controller } from "../index"
import { Empty } from "@shared/proto/common"
import { UpdateApiConfigurationRequest } from "@shared/proto/models"
import { updateApiConfiguration } from "../../storage/state"
import { buildApiHandler } from "@api/index"
import { convertProtoToApiConfiguration } from "@shared/proto-conversions/models/api-configuration-conversion"
/**
* Updates API configuration
* @param controller The controller instance
* @param request The update API configuration request
* @returns Empty response
*/
export async function updateApiConfigurationProto(
controller: Controller,
request: UpdateApiConfigurationRequest,
): Promise<Empty> {
try {
if (!request.apiConfiguration) {
console.log("[APICONFIG: updateApiConfigurationProto] API configuration is required")
throw new Error("API configuration is required")
}
// Convert proto ApiConfiguration to application ApiConfiguration
const appApiConfiguration = convertProtoToApiConfiguration(request.apiConfiguration)
// Update the API configuration in storage
await updateApiConfiguration(controller.context, appApiConfiguration)
// Update the task's API handler if there's an active task
if (controller.task) {
controller.task.api = buildApiHandler(appApiConfiguration)
}
// Post updated state to webview
await controller.postStateToWebview()
return Empty.create()
} catch (error) {
console.error(`Failed to update API configuration: ${error}`)
throw error
}
}
@@ -25,6 +25,11 @@ export async function updateSettings(controller: Controller, request: UpdateSett
}
}
// Update custom instructions
if (request.customInstructionsSetting !== undefined) {
await controller.updateCustomInstructions(request.customInstructionsSetting)
}
// Update telemetry setting
if (request.telemetrySetting) {
await controller.updateTelemetrySetting(request.telemetrySetting as TelemetrySetting)
@@ -0,0 +1,27 @@
import { Controller } from ".."
import { Int64, Int64Request } from "../../../shared/proto/common"
import { updateGlobalState } from "../../storage/state"
/**
* Updates the terminal connection timeout setting
* @param controller The controller instance
* @param request The request containing the timeout value in milliseconds
* @returns The updated timeout value
*/
export async function updateTerminalConnectionTimeout(controller: Controller, request: Int64Request): Promise<Int64> {
try {
const timeout = request.value
if (typeof timeout === "number" && !isNaN(timeout) && timeout > 0) {
// Update the global state directly
await updateGlobalState(controller.context, "shellIntegrationTimeout", timeout)
return Int64.create({ value: timeout })
} else {
console.warn(`Invalid shell integration timeout value received: ${timeout}. Expected a positive number.`)
throw new Error("Invalid timeout value. Expected a positive number.")
}
} catch (error) {
console.error(`Failed to update terminal connection timeout: ${error}`)
throw error
}
}
+2 -2
View File
@@ -1,7 +1,7 @@
import type { Controller } from "../index"
import { EmptyRequest, Empty } from "@shared/proto/common"
import { handleModelsServiceRequest } from "../models"
import { getAllExtensionState, getGlobalState, updateWorkspaceState } from "../../storage/state"
import { getAllExtensionState, getGlobalState, updateGlobalState } from "../../storage/state"
import { sendOpenRouterModelsEvent } from "../models/subscribeToOpenRouterModels"
import { sendMcpMarketplaceCatalogEvent } from "../mcp/subscribeToMcpMarketplaceCatalog"
import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
@@ -32,7 +32,7 @@ export async function initializeWebview(controller: Controller, request: EmptyRe
// Update model info in state (this needs to be done here since we don't want to update state while settings is open, and we may refresh models there)
const { apiConfiguration } = await getAllExtensionState(controller.context)
if (apiConfiguration.openRouterModelId && response.models[apiConfiguration.openRouterModelId]) {
await updateWorkspaceState(
await updateGlobalState(
controller.context,
"openRouterModelInfo",
response.models[apiConfiguration.openRouterModelId],
@@ -1,62 +0,0 @@
import { StringRequest, Empty } from "@shared/proto/common"
import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler"
import type { Controller } from "../index"
// Map client IDs to their subscription handlers
const focusChatInputSubscriptions = new Map<string, StreamingResponseHandler>()
/**
* Subscribe to focus chat input events
* @param controller The controller instance
* @param request The request containing the client ID
* @param responseStream The streaming response handler
* @param requestId The ID of the request
*/
export async function subscribeToFocusChatInput(
controller: Controller,
request: StringRequest,
responseStream: StreamingResponseHandler,
requestId?: string,
): Promise<void> {
const clientId = request.value
if (!clientId) {
throw new Error("Client ID is required for focusChatInput subscription")
}
// Store this subscription with its client ID
focusChatInputSubscriptions.set(clientId, responseStream)
// Register cleanup when the connection is closed
const cleanup = () => {
focusChatInputSubscriptions.delete(clientId)
}
// Register the cleanup function with the request registry if we have a requestId
if (requestId) {
getRequestRegistry().registerRequest(requestId, cleanup, { type: "focus_chat_input_subscription" }, responseStream)
}
}
/**
* Send a focus chat input event to a specific webview by client ID
* @param clientId The ID of the client to send the event to
*/
export async function sendFocusChatInputEvent(clientId: string): Promise<void> {
const responseStream = focusChatInputSubscriptions.get(clientId)
if (!responseStream) {
console.warn(`No subscription found for client ID: ${clientId}`)
return
}
try {
const event = Empty.create({})
await responseStream(
event,
false, // Not the last message
)
} catch (error) {
console.error(`Error sending focus chat input event to client ${clientId}:`, error)
// Remove the subscription if there was an error
focusChatInputSubscriptions.delete(clientId)
}
}
@@ -663,6 +663,7 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
}
export function addUserInstructions(
settingsCustomInstructions?: string,
globalClineRulesFileInstructions?: string,
localClineRulesFileInstructions?: string,
localCursorRulesFileInstructions?: string,
@@ -675,6 +676,9 @@ export function addUserInstructions(
if (preferredLanguageInstructions) {
customInstructions += preferredLanguageInstructions + "\n\n"
}
if (settingsCustomInstructions) {
customInstructions += settingsCustomInstructions + "\n\n"
}
if (globalClineRulesFileInstructions) {
customInstructions += globalClineRulesFileInstructions + "\n\n"
}
+4
View File
@@ -651,6 +651,7 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
export function addUserInstructions(
settingsCustomInstructions?: string,
globalClineRulesFileInstructions?: string,
localClineRulesFileInstructions?: string,
localCursorRulesFileInstructions?: string,
@@ -663,6 +664,9 @@ export function addUserInstructions(
if (preferredLanguageInstructions) {
customInstructions += preferredLanguageInstructions + "\n\n"
}
if (settingsCustomInstructions) {
customInstructions += settingsCustomInstructions + "\n\n"
}
if (globalClineRulesFileInstructions) {
customInstructions += globalClineRulesFileInstructions + "\n\n"
}
+27 -32
View File
@@ -24,20 +24,26 @@ export type SecretKey =
| "cerebrasApiKey"
export type GlobalStateKey =
| "apiProvider"
| "apiModelId"
| "awsRegion"
| "awsUseCrossRegionInference"
| "awsBedrockUsePromptCache"
| "awsBedrockEndpoint"
| "awsProfile"
| "awsUseProfile"
| "awsBedrockCustomSelected"
| "awsBedrockCustomModelBaseId"
| "vertexProjectId"
| "vertexRegion"
| "lastShownAnnouncementId"
| "customInstructions"
| "taskHistory"
| "openAiBaseUrl"
| "openAiModelId"
| "openAiModelInfo"
| "openAiHeaders"
| "ollamaModelId"
| "ollamaBaseUrl"
| "ollamaApiOptionsCtxNum"
| "lmStudioModelId"
@@ -45,20 +51,40 @@ export type GlobalStateKey =
| "anthropicBaseUrl"
| "geminiBaseUrl"
| "azureApiVersion"
| "openRouterModelId"
| "openRouterModelInfo"
| "openRouterProviderSorting"
| "autoApprovalSettings"
| "globalClineRulesToggles"
| "globalWorkflowToggles"
| "browserSettings"
| "chatSettings"
| "vsCodeLmModelSelector"
| "userInfo"
| "previousModeApiProvider"
| "previousModeModelId"
| "previousModeThinkingBudgetTokens"
| "previousModeReasoningEffort"
| "previousModeVsCodeLmModelSelector"
| "previousModeAwsBedrockCustomSelected"
| "previousModeAwsBedrockCustomModelBaseId"
| "previousModeModelInfo"
| "liteLlmBaseUrl"
| "liteLlmModelId"
| "liteLlmModelInfo"
| "liteLlmUsePromptCache"
| "fireworksModelId"
| "fireworksModelMaxCompletionTokens"
| "fireworksModelMaxTokens"
| "qwenApiLine"
| "requestyModelId"
| "requestyModelInfo"
| "togetherModelId"
| "mcpMarketplaceCatalog"
| "telemetrySetting"
| "asksageApiUrl"
| "thinkingBudgetTokens"
| "reasoningEffort"
| "planActSeparateModelsSetting"
| "enableCheckpointsSetting"
| "mcpMarketplaceEnabled"
@@ -69,35 +95,4 @@ export type GlobalStateKey =
| "terminalReuseEnabled"
| "isNewUser"
export type LocalStateKey =
| "localClineRulesToggles"
| "chatSettings"
// Current active model configuration (per workspace)
| "apiProvider"
| "apiModelId"
| "thinkingBudgetTokens"
| "reasoningEffort"
| "vsCodeLmModelSelector"
| "awsBedrockCustomSelected"
| "awsBedrockCustomModelBaseId"
| "openRouterModelId"
| "openRouterModelInfo"
| "openAiModelId"
| "openAiModelInfo"
| "ollamaModelId"
| "lmStudioModelId"
| "liteLlmModelId"
| "liteLlmModelInfo"
| "requestyModelId"
| "requestyModelInfo"
| "togetherModelId"
| "fireworksModelId"
// Previous mode saved configurations (per workspace)
| "previousModeApiProvider"
| "previousModeModelId"
| "previousModeModelInfo"
| "previousModeVsCodeLmModelSelector"
| "previousModeThinkingBudgetTokens"
| "previousModeReasoningEffort"
| "previousModeAwsBedrockCustomSelected"
| "previousModeAwsBedrockCustomModelBaseId"
export type LocalStateKey = "localClineRulesToggles"
+96 -209
View File
@@ -11,9 +11,6 @@ import { ChatSettings } from "@shared/ChatSettings"
import { TelemetrySetting } from "@shared/TelemetrySetting"
import { UserInfo } from "@shared/UserInfo"
import { ClineRulesToggles } from "@shared/cline-rules"
import { ensureRulesDirectoryExists } from "./disk"
import fs from "fs/promises"
import path from "path"
/*
Storage
https://dev.to/kompotkot/how-to-use-secretstorage-in-your-vscode-extensions-2hco
@@ -54,57 +51,6 @@ export async function getWorkspaceState(context: vscode.ExtensionContext, key: s
return await context.workspaceState.get(key)
}
export async function migratePlanActGlobalToWorkspaceStorage(context: vscode.ExtensionContext) {
// Keys that were migrated from global storage to workspace storage
const keysToMigrate = [
// Core settings
"apiProvider",
"apiModelId",
"thinkingBudgetTokens",
"reasoningEffort",
"chatSettings",
"vsCodeLmModelSelector",
// Provider-specific model keys
"awsBedrockCustomSelected",
"awsBedrockCustomModelBaseId",
"openRouterModelId",
"openRouterModelInfo",
"openAiModelId",
"openAiModelInfo",
"ollamaModelId",
"lmStudioModelId",
"liteLlmModelId",
"liteLlmModelInfo",
"requestyModelId",
"requestyModelInfo",
"togetherModelId",
"fireworksModelId",
// Previous mode settings
"previousModeApiProvider",
"previousModeModelId",
"previousModeModelInfo",
"previousModeVsCodeLmModelSelector",
"previousModeThinkingBudgetTokens",
"previousModeReasoningEffort",
"previousModeAwsBedrockCustomSelected",
"previousModeAwsBedrockCustomModelBaseId",
]
for (const key of keysToMigrate) {
const globalValue = await getGlobalState(context, key as GlobalStateKey)
if (globalValue !== undefined) {
const workspaceValue = await getWorkspaceState(context, key)
if (workspaceValue === undefined) {
await updateWorkspaceState(context, key, globalValue)
}
// Delete from global storage regardless of whether we copied it
await updateGlobalState(context, key as GlobalStateKey, undefined)
}
}
}
async function migrateMcpMarketplaceEnableSetting(mcpMarketplaceEnabledRaw: boolean | undefined): Promise<boolean> {
const config = vscode.workspace.getConfiguration("cline")
const mcpMarketplaceEnabled = config.get<boolean>("mcpMarketplace.enabled")
@@ -128,54 +74,11 @@ async function migrateEnableCheckpointsSetting(enableCheckpointsSettingRaw: bool
return enableCheckpointsSettingRaw ?? true
}
export async function migrateCustomInstructionsToGlobalRules(context: vscode.ExtensionContext) {
try {
const customInstructions = (await context.globalState.get("customInstructions")) as string | undefined
if (customInstructions?.trim()) {
console.log("Migrating custom instructions to global Cline rules...")
// Create global .clinerules directory if it doesn't exist
const globalRulesDir = await ensureRulesDirectoryExists()
// Use a fixed filename for custom instructions
const migrationFileName = "custom_instructions.md"
const migrationFilePath = path.join(globalRulesDir, migrationFileName)
try {
// Check if file already exists to determine if we should append
let existingContent = ""
try {
existingContent = await fs.readFile(migrationFilePath, "utf8")
} catch (readError) {
// File doesn't exist, which is fine
}
// Append or create the file with custom instructions
const contentToWrite = existingContent
? `${existingContent}\n\n---\n\n${customInstructions.trim()}`
: customInstructions.trim()
await fs.writeFile(migrationFilePath, contentToWrite)
console.log(`Successfully ${existingContent ? "appended to" : "created"} migration file: ${migrationFilePath}`)
} catch (fileError) {
console.error("Failed to write migration file:", fileError)
return
}
// Remove customInstructions from global state only after successful file creation
await context.globalState.update("customInstructions", undefined)
console.log("Successfully migrated custom instructions to global Cline rules")
}
} catch (error) {
console.error("Failed to migrate custom instructions to global rules:", error)
// Continue execution - migration failure shouldn't break extension startup
}
}
export async function getAllExtensionState(context: vscode.ExtensionContext) {
const [
isNewUser,
storedApiProvider,
apiModelId,
apiKey,
openRouterApiKey,
clineApiKey,
@@ -188,13 +91,19 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
awsBedrockEndpoint,
awsProfile,
awsUseProfile,
awsBedrockCustomSelected,
awsBedrockCustomModelBaseId,
vertexProjectId,
vertexRegion,
openAiBaseUrl,
openAiApiKey,
openAiModelId,
openAiModelInfo,
openAiHeaders,
ollamaModelId,
ollamaBaseUrl,
ollamaApiOptionsCtxNum,
lmStudioModelId,
lmStudioBaseUrl,
anthropicBaseUrl,
geminiApiKey,
@@ -202,28 +111,49 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
openAiNativeApiKey,
deepSeekApiKey,
requestyApiKey,
requestyModelId,
requestyModelInfo,
togetherApiKey,
togetherModelId,
qwenApiKey,
doubaoApiKey,
mistralApiKey,
azureApiVersion,
openRouterModelId,
openRouterModelInfo,
openRouterProviderSorting,
lastShownAnnouncementId,
customInstructions,
taskHistory,
autoApprovalSettings,
browserSettings,
chatSettings,
vsCodeLmModelSelector,
liteLlmBaseUrl,
liteLlmModelId,
liteLlmModelInfo,
liteLlmUsePromptCache,
fireworksApiKey,
fireworksModelId,
fireworksModelMaxCompletionTokens,
fireworksModelMaxTokens,
userInfo,
previousModeApiProvider,
previousModeModelId,
previousModeModelInfo,
previousModeVsCodeLmModelSelector,
previousModeThinkingBudgetTokens,
previousModeReasoningEffort,
previousModeAwsBedrockCustomSelected,
previousModeAwsBedrockCustomModelBaseId,
qwenApiLine,
liteLlmApiKey,
telemetrySetting,
asksageApiKey,
asksageApiUrl,
xaiApiKey,
thinkingBudgetTokens,
reasoningEffort,
sambanovaApiKey,
cerebrasApiKey,
nebiusApiKey,
@@ -239,6 +169,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
terminalReuseEnabled,
] = await Promise.all([
getGlobalState(context, "isNewUser") as Promise<boolean | undefined>,
getGlobalState(context, "apiProvider") as Promise<ApiProvider | undefined>,
getGlobalState(context, "apiModelId") as Promise<string | undefined>,
getSecret(context, "apiKey") as Promise<string | undefined>,
getSecret(context, "openRouterApiKey") as Promise<string | undefined>,
getSecret(context, "clineApiKey") as Promise<string | undefined>,
@@ -251,13 +183,19 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
getGlobalState(context, "awsBedrockEndpoint") as Promise<string | undefined>,
getGlobalState(context, "awsProfile") as Promise<string | undefined>,
getGlobalState(context, "awsUseProfile") as Promise<boolean | undefined>,
getGlobalState(context, "awsBedrockCustomSelected") as Promise<boolean | undefined>,
getGlobalState(context, "awsBedrockCustomModelBaseId") as Promise<BedrockModelId | undefined>,
getGlobalState(context, "vertexProjectId") as Promise<string | undefined>,
getGlobalState(context, "vertexRegion") as Promise<string | undefined>,
getGlobalState(context, "openAiBaseUrl") as Promise<string | undefined>,
getSecret(context, "openAiApiKey") as Promise<string | undefined>,
getGlobalState(context, "openAiModelId") as Promise<string | undefined>,
getGlobalState(context, "openAiModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "openAiHeaders") as Promise<Record<string, string> | undefined>,
getGlobalState(context, "ollamaModelId") as Promise<string | undefined>,
getGlobalState(context, "ollamaBaseUrl") as Promise<string | undefined>,
getGlobalState(context, "ollamaApiOptionsCtxNum") as Promise<string | undefined>,
getGlobalState(context, "lmStudioModelId") as Promise<string | undefined>,
getGlobalState(context, "lmStudioBaseUrl") as Promise<string | undefined>,
getGlobalState(context, "anthropicBaseUrl") as Promise<string | undefined>,
getSecret(context, "geminiApiKey") as Promise<string | undefined>,
@@ -265,28 +203,49 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
getSecret(context, "openAiNativeApiKey") as Promise<string | undefined>,
getSecret(context, "deepSeekApiKey") as Promise<string | undefined>,
getSecret(context, "requestyApiKey") as Promise<string | undefined>,
getGlobalState(context, "requestyModelId") as Promise<string | undefined>,
getGlobalState(context, "requestyModelInfo") as Promise<ModelInfo | undefined>,
getSecret(context, "togetherApiKey") as Promise<string | undefined>,
getGlobalState(context, "togetherModelId") as Promise<string | undefined>,
getSecret(context, "qwenApiKey") as Promise<string | undefined>,
getSecret(context, "doubaoApiKey") as Promise<string | undefined>,
getSecret(context, "mistralApiKey") as Promise<string | undefined>,
getGlobalState(context, "azureApiVersion") as Promise<string | undefined>,
getGlobalState(context, "openRouterModelId") as Promise<string | undefined>,
getGlobalState(context, "openRouterModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "openRouterProviderSorting") as Promise<string | undefined>,
getGlobalState(context, "lastShownAnnouncementId") as Promise<string | undefined>,
getGlobalState(context, "customInstructions") as Promise<string | undefined>,
getGlobalState(context, "taskHistory") as Promise<HistoryItem[] | undefined>,
getGlobalState(context, "autoApprovalSettings") as Promise<AutoApprovalSettings | undefined>,
getGlobalState(context, "browserSettings") as Promise<BrowserSettings | undefined>,
getGlobalState(context, "chatSettings") as Promise<ChatSettings | undefined>,
getGlobalState(context, "vsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
getGlobalState(context, "liteLlmBaseUrl") as Promise<string | undefined>,
getGlobalState(context, "liteLlmModelId") as Promise<string | undefined>,
getGlobalState(context, "liteLlmModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "liteLlmUsePromptCache") as Promise<boolean | undefined>,
getSecret(context, "fireworksApiKey") as Promise<string | undefined>,
getGlobalState(context, "fireworksModelId") as Promise<string | undefined>,
getGlobalState(context, "fireworksModelMaxCompletionTokens") as Promise<number | undefined>,
getGlobalState(context, "fireworksModelMaxTokens") as Promise<number | undefined>,
getGlobalState(context, "userInfo") as Promise<UserInfo | undefined>,
getGlobalState(context, "previousModeApiProvider") as Promise<ApiProvider | undefined>,
getGlobalState(context, "previousModeModelId") as Promise<string | undefined>,
getGlobalState(context, "previousModeModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "previousModeVsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
getGlobalState(context, "previousModeThinkingBudgetTokens") as Promise<number | undefined>,
getGlobalState(context, "previousModeReasoningEffort") as Promise<string | undefined>,
getGlobalState(context, "previousModeAwsBedrockCustomSelected") as Promise<boolean | undefined>,
getGlobalState(context, "previousModeAwsBedrockCustomModelBaseId") as Promise<BedrockModelId | undefined>,
getGlobalState(context, "qwenApiLine") as Promise<string | undefined>,
getSecret(context, "liteLlmApiKey") as Promise<string | undefined>,
getGlobalState(context, "telemetrySetting") as Promise<TelemetrySetting | undefined>,
getSecret(context, "asksageApiKey") as Promise<string | undefined>,
getGlobalState(context, "asksageApiUrl") as Promise<string | undefined>,
getSecret(context, "xaiApiKey") as Promise<string | undefined>,
getGlobalState(context, "thinkingBudgetTokens") as Promise<number | undefined>,
getGlobalState(context, "reasoningEffort") as Promise<string | undefined>,
getSecret(context, "sambanovaApiKey") as Promise<string | undefined>,
getSecret(context, "cerebrasApiKey") as Promise<string | undefined>,
getSecret(context, "nebiusApiKey") as Promise<string | undefined>,
@@ -302,68 +261,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
getGlobalState(context, "terminalReuseEnabled") as Promise<boolean | undefined>,
])
const localClineRulesToggles = (await getWorkspaceState(context, "localClineRulesToggles")) as ClineRulesToggles
const [
chatSettings,
storedApiProvider,
apiModelId,
thinkingBudgetTokens,
reasoningEffort,
vsCodeLmModelSelector,
awsBedrockCustomSelected,
awsBedrockCustomModelBaseId,
openRouterModelId,
openRouterModelInfo,
openAiModelId,
openAiModelInfo,
ollamaModelId,
lmStudioModelId,
liteLlmModelId,
liteLlmModelInfo,
requestyModelId,
requestyModelInfo,
togetherModelId,
fireworksModelId,
previousModeApiProvider,
previousModeModelId,
previousModeModelInfo,
previousModeVsCodeLmModelSelector,
previousModeThinkingBudgetTokens,
previousModeReasoningEffort,
previousModeAwsBedrockCustomSelected,
previousModeAwsBedrockCustomModelBaseId,
] = await Promise.all([
getWorkspaceState(context, "chatSettings") as Promise<ChatSettings | undefined>,
getWorkspaceState(context, "apiProvider") as Promise<ApiProvider | undefined>,
getWorkspaceState(context, "apiModelId") as Promise<string | undefined>,
getWorkspaceState(context, "thinkingBudgetTokens") as Promise<number | undefined>,
getWorkspaceState(context, "reasoningEffort") as Promise<string | undefined>,
getWorkspaceState(context, "vsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
getWorkspaceState(context, "awsBedrockCustomSelected") as Promise<boolean | undefined>,
getWorkspaceState(context, "awsBedrockCustomModelBaseId") as Promise<BedrockModelId | undefined>,
getWorkspaceState(context, "openRouterModelId") as Promise<string | undefined>,
getWorkspaceState(context, "openRouterModelInfo") as Promise<ModelInfo | undefined>,
getWorkspaceState(context, "openAiModelId") as Promise<string | undefined>,
getWorkspaceState(context, "openAiModelInfo") as Promise<ModelInfo | undefined>,
getWorkspaceState(context, "ollamaModelId") as Promise<string | undefined>,
getWorkspaceState(context, "lmStudioModelId") as Promise<string | undefined>,
getWorkspaceState(context, "liteLlmModelId") as Promise<string | undefined>,
getWorkspaceState(context, "liteLlmModelInfo") as Promise<ModelInfo | undefined>,
getWorkspaceState(context, "requestyModelId") as Promise<string | undefined>,
getWorkspaceState(context, "requestyModelInfo") as Promise<ModelInfo | undefined>,
getWorkspaceState(context, "togetherModelId") as Promise<string | undefined>,
getWorkspaceState(context, "fireworksModelId") as Promise<string | undefined>,
getWorkspaceState(context, "previousModeApiProvider") as Promise<ApiProvider | undefined>,
getWorkspaceState(context, "previousModeModelId") as Promise<string | undefined>,
getWorkspaceState(context, "previousModeModelInfo") as Promise<ModelInfo | undefined>,
getWorkspaceState(context, "previousModeVsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
getWorkspaceState(context, "previousModeThinkingBudgetTokens") as Promise<number | undefined>,
getWorkspaceState(context, "previousModeReasoningEffort") as Promise<string | undefined>,
getWorkspaceState(context, "previousModeAwsBedrockCustomSelected") as Promise<boolean | undefined>,
getWorkspaceState(context, "previousModeAwsBedrockCustomModelBaseId") as Promise<BedrockModelId | undefined>,
])
let apiProvider: ApiProvider
if (storedApiProvider) {
apiProvider = storedApiProvider
@@ -378,6 +275,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
}
}
const localClineRulesToggles = (await getWorkspaceState(context, "localClineRulesToggles")) as ClineRulesToggles
const mcpMarketplaceEnabled = await migrateMcpMarketplaceEnableSetting(mcpMarketplaceEnabledRaw)
const enableCheckpointsSetting = await migrateEnableCheckpointsSetting(enableCheckpointsSettingRaw)
const mcpResponsesCollapsed = mcpResponsesCollapsedRaw ?? false
@@ -471,6 +370,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
},
isNewUser: isNewUser ?? true,
lastShownAnnouncementId,
customInstructions,
taskHistory,
autoApprovalSettings: autoApprovalSettings || DEFAULT_AUTO_APPROVAL_SETTINGS, // default value can be 0 or empty string
globalClineRulesToggles: globalClineRulesToggles || {},
@@ -563,68 +463,37 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
cerebrasApiKey,
nebiusApiKey,
favoritedModelIds,
fireworksApiKey,
fireworksModelId,
fireworksModelMaxCompletionTokens,
fireworksModelMaxTokens,
} = apiConfiguration
// Workspace state updates
await updateWorkspaceState(context, "apiProvider", apiProvider)
await updateWorkspaceState(context, "apiModelId", apiModelId)
await updateWorkspaceState(context, "thinkingBudgetTokens", thinkingBudgetTokens)
await updateWorkspaceState(context, "reasoningEffort", reasoningEffort)
await updateWorkspaceState(context, "vsCodeLmModelSelector", vsCodeLmModelSelector)
await updateWorkspaceState(context, "awsBedrockCustomSelected", awsBedrockCustomSelected)
await updateWorkspaceState(context, "awsBedrockCustomModelBaseId", awsBedrockCustomModelBaseId)
await updateWorkspaceState(context, "openRouterModelId", openRouterModelId)
await updateWorkspaceState(context, "openRouterModelInfo", openRouterModelInfo)
await updateWorkspaceState(context, "openAiModelId", openAiModelId)
await updateWorkspaceState(context, "openAiModelInfo", openAiModelInfo)
await updateWorkspaceState(context, "ollamaModelId", ollamaModelId)
await updateWorkspaceState(context, "lmStudioModelId", lmStudioModelId)
await updateWorkspaceState(context, "liteLlmModelId", liteLlmModelId)
await updateWorkspaceState(context, "liteLlmModelInfo", liteLlmModelInfo)
await updateWorkspaceState(context, "requestyModelId", requestyModelId)
await updateWorkspaceState(context, "requestyModelInfo", requestyModelInfo)
await updateWorkspaceState(context, "togetherModelId", togetherModelId)
await updateWorkspaceState(context, "fireworksModelId", fireworksModelId)
// Global state updates
await updateGlobalState(context, "apiProvider", apiProvider)
await updateGlobalState(context, "apiModelId", apiModelId)
await storeSecret(context, "apiKey", apiKey)
await storeSecret(context, "openRouterApiKey", openRouterApiKey)
await storeSecret(context, "awsAccessKey", awsAccessKey)
await storeSecret(context, "awsSecretKey", awsSecretKey)
await storeSecret(context, "awsSessionToken", awsSessionToken)
await updateGlobalState(context, "awsRegion", awsRegion)
await updateGlobalState(context, "awsUseCrossRegionInference", awsUseCrossRegionInference)
await updateGlobalState(context, "awsBedrockUsePromptCache", awsBedrockUsePromptCache)
await updateGlobalState(context, "awsBedrockEndpoint", awsBedrockEndpoint)
await updateGlobalState(context, "awsProfile", awsProfile)
await updateGlobalState(context, "awsUseProfile", awsUseProfile)
await updateGlobalState(context, "awsBedrockCustomSelected", awsBedrockCustomSelected)
await updateGlobalState(context, "awsBedrockCustomModelBaseId", awsBedrockCustomModelBaseId)
await updateGlobalState(context, "vertexProjectId", vertexProjectId)
await updateGlobalState(context, "vertexRegion", vertexRegion)
await updateGlobalState(context, "openAiBaseUrl", openAiBaseUrl)
await storeSecret(context, "openAiApiKey", openAiApiKey)
await updateGlobalState(context, "openAiModelId", openAiModelId)
await updateGlobalState(context, "openAiModelInfo", openAiModelInfo)
await updateGlobalState(context, "openAiHeaders", openAiHeaders || {})
await updateGlobalState(context, "ollamaModelId", ollamaModelId)
await updateGlobalState(context, "ollamaBaseUrl", ollamaBaseUrl)
await updateGlobalState(context, "ollamaApiOptionsCtxNum", ollamaApiOptionsCtxNum)
await updateGlobalState(context, "lmStudioModelId", lmStudioModelId)
await updateGlobalState(context, "lmStudioBaseUrl", lmStudioBaseUrl)
await updateGlobalState(context, "anthropicBaseUrl", anthropicBaseUrl)
await updateGlobalState(context, "geminiBaseUrl", geminiBaseUrl)
await updateGlobalState(context, "azureApiVersion", azureApiVersion)
await updateGlobalState(context, "openRouterProviderSorting", openRouterProviderSorting)
await updateGlobalState(context, "liteLlmBaseUrl", liteLlmBaseUrl)
await updateGlobalState(context, "liteLlmUsePromptCache", liteLlmUsePromptCache)
await updateGlobalState(context, "qwenApiLine", qwenApiLine)
await updateGlobalState(context, "asksageApiUrl", asksageApiUrl)
await updateGlobalState(context, "favoritedModelIds", favoritedModelIds)
await updateGlobalState(context, "requestTimeoutMs", apiConfiguration.requestTimeoutMs)
await updateGlobalState(context, "fireworksModelMaxCompletionTokens", fireworksModelMaxCompletionTokens)
await updateGlobalState(context, "fireworksModelMaxTokens", fireworksModelMaxTokens)
// Secret updates
await storeSecret(context, "apiKey", apiKey)
await storeSecret(context, "openRouterApiKey", openRouterApiKey)
await storeSecret(context, "clineApiKey", clineApiKey)
await storeSecret(context, "awsAccessKey", awsAccessKey)
await storeSecret(context, "awsSecretKey", awsSecretKey)
await storeSecret(context, "awsSessionToken", awsSessionToken)
await storeSecret(context, "openAiApiKey", openAiApiKey)
await storeSecret(context, "geminiApiKey", geminiApiKey)
await updateGlobalState(context, "geminiBaseUrl", geminiBaseUrl)
await storeSecret(context, "openAiNativeApiKey", openAiNativeApiKey)
await storeSecret(context, "deepSeekApiKey", deepSeekApiKey)
await storeSecret(context, "requestyApiKey", requestyApiKey)
@@ -633,12 +502,30 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
await storeSecret(context, "doubaoApiKey", doubaoApiKey)
await storeSecret(context, "mistralApiKey", mistralApiKey)
await storeSecret(context, "liteLlmApiKey", liteLlmApiKey)
await storeSecret(context, "fireworksApiKey", fireworksApiKey)
await storeSecret(context, "asksageApiKey", asksageApiKey)
await storeSecret(context, "xaiApiKey", xaiApiKey)
await updateGlobalState(context, "azureApiVersion", azureApiVersion)
await updateGlobalState(context, "openRouterModelId", openRouterModelId)
await updateGlobalState(context, "openRouterModelInfo", openRouterModelInfo)
await updateGlobalState(context, "openRouterProviderSorting", openRouterProviderSorting)
await updateGlobalState(context, "vsCodeLmModelSelector", vsCodeLmModelSelector)
await updateGlobalState(context, "liteLlmBaseUrl", liteLlmBaseUrl)
await updateGlobalState(context, "liteLlmModelId", liteLlmModelId)
await updateGlobalState(context, "liteLlmModelInfo", liteLlmModelInfo)
await updateGlobalState(context, "liteLlmUsePromptCache", liteLlmUsePromptCache)
await updateGlobalState(context, "qwenApiLine", qwenApiLine)
await updateGlobalState(context, "requestyModelId", requestyModelId)
await updateGlobalState(context, "requestyModelInfo", requestyModelInfo)
await updateGlobalState(context, "togetherModelId", togetherModelId)
await storeSecret(context, "asksageApiKey", asksageApiKey)
await updateGlobalState(context, "asksageApiUrl", asksageApiUrl)
await updateGlobalState(context, "thinkingBudgetTokens", thinkingBudgetTokens)
await updateGlobalState(context, "reasoningEffort", reasoningEffort)
await storeSecret(context, "clineApiKey", clineApiKey)
await storeSecret(context, "sambanovaApiKey", sambanovaApiKey)
await storeSecret(context, "cerebrasApiKey", cerebrasApiKey)
await storeSecret(context, "nebiusApiKey", nebiusApiKey)
await updateGlobalState(context, "favoritedModelIds", favoritedModelIds)
await updateGlobalState(context, "requestTimeoutMs", apiConfiguration.requestTimeoutMs)
}
export async function resetExtensionState(context: vscode.ExtensionContext) {
+40 -46
View File
@@ -105,7 +105,7 @@ import {
getLocalCursorRules,
} from "@core/context/instructions/user-instructions/external-rules"
import { refreshWorkflowToggles } from "../context/instructions/user-instructions/workflows"
import { getWorkspaceState } from "@core/storage/state"
import { getGlobalState } from "@core/storage/state"
import { parseSlashCommands } from "@core/slash-commands"
import WorkspaceTracker from "@integrations/workspace/WorkspaceTracker"
import { McpHub } from "@services/mcp/McpHub"
@@ -113,7 +113,6 @@ import { isInTestMode } from "../../services/test/TestMode"
import { processFilesIntoText } from "@integrations/misc/extract-text"
import { featureFlagsService } from "@services/posthog/feature-flags/FeatureFlagsService"
import { StreamingJsonReplacer, ChangeLocation } from "@core/assistant-message/diff-json"
import { isClaude4ModelFamily } from "@/utils/model-utils"
export const USE_EXPERIMENTAL_CLAUDE4_FEATURES = false
@@ -145,6 +144,7 @@ export class Task {
browserSession: BrowserSession
contextManager: ContextManager
private didEditFile: boolean = false
customInstructions?: string
autoApprovalSettings: AutoApprovalSettings
browserSettings: BrowserSettings
chatSettings: ChatSettings
@@ -204,6 +204,7 @@ export class Task {
shellIntegrationTimeout: number,
terminalReuseEnabled: boolean,
enableCheckpointsSetting: boolean,
customInstructions?: string,
task?: string,
images?: string[],
files?: string[],
@@ -226,17 +227,12 @@ export class Task {
this.browserSession = new BrowserSession(context, browserSettings)
this.contextManager = new ContextManager()
this.diffViewProvider = new DiffViewProvider(cwd)
this.customInstructions = customInstructions
this.autoApprovalSettings = autoApprovalSettings
this.browserSettings = browserSettings
this.chatSettings = chatSettings
this.enableCheckpoints = enableCheckpointsSetting
// Set up MCP notification callback for real-time notifications
this.mcpHub.setNotificationCallback(async (serverName: string, level: string, message: string) => {
// Display notification in chat immediately
await this.say("mcp_notification", `[${serverName}] ${message}`)
})
// Initialize taskId first
if (historyItem) {
this.taskId = historyItem.id
@@ -1200,9 +1196,6 @@ export class Task {
this.clineIgnoreController.dispose()
this.fileContextTracker.dispose()
await this.diffViewProvider.revertChanges() // need to await for when we want to make sure directories/files are reverted before re-starting the task from a checkpoint
// Clear the notification callback when task is aborted
this.mcpHub.clearNotificationCallback()
}
// Checkpoints
@@ -1627,6 +1620,12 @@ export class Task {
}
}
private async isClaude4ModelFamily(): Promise<boolean> {
const model = this.api.getModel()
const modelId = model.id
return modelId.includes("sonnet-4") || modelId.includes("opus-4")
}
async *attemptApiRequest(previousApiReqIndex: number): ApiStream {
// Wait for MCP servers to be connected before generating system prompt
await pWaitFor(() => this.mcpHub.isConnecting !== true, { timeout: 10_000 }).catch(() => {
@@ -1640,9 +1639,10 @@ export class Task {
const supportsBrowserUse = modelSupportsBrowserUse && !disableBrowserTool // only enable browser use if the model supports it and the user hasn't disabled it
const isClaude4Model = isClaude4ModelFamily(this.api)
let systemPrompt = await SYSTEM_PROMPT(cwd, supportsBrowserUse, this.mcpHub, this.browserSettings, isClaude4Model)
const isClaude4ModelFamily = await this.isClaude4ModelFamily()
let systemPrompt = await SYSTEM_PROMPT(cwd, supportsBrowserUse, this.mcpHub, this.browserSettings, isClaude4ModelFamily)
let settingsCustomInstructions = this.customInstructions?.trim()
await this.migratePreferredLanguageToolSetting()
const preferredLanguage = getLanguageKey(this.chatSettings.preferredLanguage as LanguageDisplay)
const preferredLanguageInstructions =
@@ -1670,6 +1670,7 @@ export class Task {
}
if (
settingsCustomInstructions ||
globalClineRulesFileInstructions ||
localClineRulesFileInstructions ||
localCursorRulesFileInstructions ||
@@ -1680,6 +1681,7 @@ export class Task {
) {
// altering the system prompt mid-task will break the prompt cache, but in the grand scheme this will not change often so it's better to not pollute user messages with it the way we have to with <potentially relevant details>
const userInstructions = addUserInstructions(
settingsCustomInstructions,
globalClineRulesFileInstructions,
localClineRulesFileInstructions,
localCursorRulesFileInstructions,
@@ -1688,7 +1690,6 @@ export class Task {
clineIgnoreInstructions,
preferredLanguageInstructions,
)
console.log("[INSTRUCTIONS] User instructions:", userInstructions)
systemPrompt += userInstructions
}
const contextManagementMetadata = await this.contextManager.getNewContextMessagesAndMetadata(
@@ -1920,6 +1921,7 @@ export class Task {
// Get final list of replacements
const allReplacements = this.streamingJsonReplacer.getSuccessfullyParsedItems()
// console.log(`Total replacements applied: ${allReplacements.length}`)
// Cleanup
this.streamingJsonReplacer = undefined
@@ -1949,6 +1951,7 @@ export class Task {
if (this.didCompleteReadingStream) {
this.userMessageContentReady = true
}
// console.log("no more content blocks to stream! this shouldn't happen?")
this.presentAssistantMessageLocked = false
return
//throw new Error("No more content blocks to stream! This shouldn't happen...") // remove and just return after testing
@@ -2084,11 +2087,11 @@ export class Task {
break
}
const pushToolResult = (content: ToolResponse, isClaude4Model: boolean = false) => {
const pushToolResult = (content: ToolResponse, isClaude4ModelFamily: boolean = false) => {
if (typeof content === "string") {
const resultText = content || "(tool did not return anything)"
if (isClaude4Model && USE_EXPERIMENTAL_CLAUDE4_FEATURES) {
if (isClaude4ModelFamily && USE_EXPERIMENTAL_CLAUDE4_FEATURES) {
// Claude 4 family: Use function_results format
this.userMessageContent.push({
type: "text",
@@ -2174,7 +2177,7 @@ export class Task {
}
}
const handleError = async (action: string, error: Error, isClaude4Model: boolean = false) => {
const handleError = async (action: string, error: Error, isClaude4ModelFamily: boolean = false) => {
if (this.abandoned) {
console.log("Ignoring error since task was abandoned (i.e. from task cancellation after resetting)")
return
@@ -2185,7 +2188,7 @@ export class Task {
`Error ${action}:\n${error.message ?? JSON.stringify(serializeError(error), null, 2)}`,
)
pushToolResult(formatResponse.toolError(errorString), isClaude4Model)
pushToolResult(formatResponse.toolError(errorString), isClaude4ModelFamily)
}
// If block is partial, remove partial closing tag so its not presented to user
@@ -2263,9 +2266,9 @@ export class Task {
const currentFullJson = block.params.diff
// Check if we should use streaming (e.g., for specific models)
const isClaude4Model = isClaude4ModelFamily(this.api)
const isClaude4ModelFamily = await this.isClaude4ModelFamily()
// Going through claude family of models
if (isClaude4Model && USE_EXPERIMENTAL_CLAUDE4_FEATURES && currentFullJson) {
if (isClaude4ModelFamily && USE_EXPERIMENTAL_CLAUDE4_FEATURES && currentFullJson) {
console.log("[EDIT] Streaming JSON replacement")
const streamingResult = await this.handleStreamingJsonReplacement(
block,
@@ -2657,7 +2660,7 @@ export class Task {
}
}
case "list_files": {
const isClaude4Model = isClaude4ModelFamily(this.api)
const isClaude4ModelFamily = await this.isClaude4ModelFamily()
const relDirPath: string | undefined = block.params.path
const recursiveRaw: string | undefined = block.params.recursive
const recursive = recursiveRaw?.toLowerCase() === "true"
@@ -2683,7 +2686,10 @@ export class Task {
} else {
if (!relDirPath) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("list_files", "path"), isClaude4Model)
pushToolResult(
await this.sayAndCreateMissingParamError("list_files", "path"),
isClaude4ModelFamily,
)
await this.saveCheckpoint()
break
}
@@ -2734,12 +2740,12 @@ export class Task {
true,
)
}
pushToolResult(result, isClaude4Model)
pushToolResult(result, isClaude4ModelFamily)
await this.saveCheckpoint()
break
}
} catch (error) {
await handleError("listing files", error, isClaude4Model)
await handleError("listing files", error, isClaude4ModelFamily)
await this.saveCheckpoint()
break
}
@@ -2827,7 +2833,7 @@ export class Task {
}
}
case "search_files": {
const isClaude4Model = isClaude4ModelFamily(this.api)
const isClaude4ModelFamily = await this.isClaude4ModelFamily()
const relDirPath: string | undefined = block.params.path
const regex: string | undefined = block.params.regex
const filePattern: string | undefined = block.params.file_pattern
@@ -2857,7 +2863,7 @@ export class Task {
this.consecutiveMistakeCount++
pushToolResult(
await this.sayAndCreateMissingParamError("search_files", "path"),
isClaude4Model,
isClaude4ModelFamily,
)
await this.saveCheckpoint()
break
@@ -2866,7 +2872,7 @@ export class Task {
this.consecutiveMistakeCount++
pushToolResult(
await this.sayAndCreateMissingParamError("search_files", "regex"),
isClaude4Model,
isClaude4ModelFamily,
)
await this.saveCheckpoint()
break
@@ -2917,12 +2923,12 @@ export class Task {
true,
)
}
pushToolResult(results, isClaude4Model)
pushToolResult(results, isClaude4ModelFamily)
await this.saveCheckpoint()
break
}
} catch (error) {
await handleError("searching files", error, isClaude4Model)
await handleError("searching files", error, isClaude4ModelFamily)
await this.saveCheckpoint()
break
}
@@ -3317,21 +3323,8 @@ export class Task {
// now execute the tool
await this.say("mcp_server_request_started") // same as browser_action_result
// Check for any pending notifications before the tool call
const notificationsBefore = this.mcpHub.getPendingNotifications()
for (const notification of notificationsBefore) {
await this.say("mcp_notification", `[${notification.serverName}] ${notification.message}`)
}
const toolResult = await this.mcpHub.callTool(server_name, tool_name, parsedArguments)
// Check for any pending notifications after the tool call
const notificationsAfter = this.mcpHub.getPendingNotifications()
for (const notification of notificationsAfter) {
await this.say("mcp_notification", `[${notification.serverName}] ${notification.message}`)
}
// TODO: add progress indicator
const toolResultImages =
@@ -3720,7 +3713,7 @@ export class Task {
const clineVersion =
vscode.extensions.getExtension("saoudrizwan.claude-dev")?.packageJSON.version || "Unknown"
const systemInfo = `VSCode: ${vscode.version}, Node.js: ${process.version}, Architecture: ${os.arch()}`
const providerAndModel = `${(await getWorkspaceState(this.getContext(), "apiProvider")) as string} / ${this.api.getModel().id}`
const providerAndModel = `${(await getGlobalState(this.getContext(), "apiProvider")) as string} / ${this.api.getModel().id}`
// Ask user for confirmation
const bugReportData = JSON.stringify({
@@ -4234,7 +4227,7 @@ export class Task {
}
// Used to know what models were used in the task if user wants to export metadata for error reporting purposes
const currentProviderId = (await getWorkspaceState(this.getContext(), "apiProvider")) as string
const currentProviderId = (await getGlobalState(this.getContext(), "apiProvider")) as string
if (currentProviderId && this.api.getModel().id) {
try {
await this.modelContextTracker.recordModelUsage(currentProviderId, this.api.getModel().id, this.chatSettings.mode)
@@ -4510,8 +4503,9 @@ export class Task {
assistantMessage += chunk.text
// parse raw assistant message into content blocks
const prevLength = this.assistantMessageContent.length
const isClaude4Model = isClaude4ModelFamily(this.api)
if (isClaude4Model && USE_EXPERIMENTAL_CLAUDE4_FEATURES) {
const isClaude4ModelFamily = await this.isClaude4ModelFamily()
if (isClaude4ModelFamily && USE_EXPERIMENTAL_CLAUDE4_FEATURES) {
this.assistantMessageContent = parseAssistantMessageV3(assistantMessage)
} else {
this.assistantMessageContent = parseAssistantMessageV2(assistantMessage)
-23
View File
@@ -9,7 +9,6 @@ import { readFile } from "fs/promises"
import path from "node:path"
import { WebviewProviderType } from "@/shared/webview/types"
import { sendThemeEvent } from "@core/controller/ui/subscribeToTheme"
import { v4 as uuidv4 } from "uuid"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -20,11 +19,9 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
public static readonly sideBarId = "claude-dev.SidebarProvider" // used in package.json as the view's id. This value cannot be changed due to how vscode caches views based on their id, and updating the id would break existing instances of the extension.
public static readonly tabPanelId = "claude-dev.TabPanelProvider"
private static activeInstances: Set<WebviewProvider> = new Set()
private static clientIdMap = new Map<WebviewProvider, string>()
public view?: vscode.WebviewView | vscode.WebviewPanel
private disposables: vscode.Disposable[] = []
controller: Controller
private clientId: string
constructor(
readonly context: vscode.ExtensionContext,
@@ -32,21 +29,9 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
private readonly providerType: WebviewProviderType = WebviewProviderType.TAB, // Default to tab provider
) {
WebviewProvider.activeInstances.add(this)
this.clientId = uuidv4()
WebviewProvider.clientIdMap.set(this, this.clientId)
this.controller = new Controller(context, outputChannel, (message) => this.view?.webview.postMessage(message))
}
// Add a method to get the client ID
public getClientId(): string {
return this.clientId
}
// Add a static method to get the client ID for a specific instance
public static getClientIdForInstance(instance: WebviewProvider): string | undefined {
return WebviewProvider.clientIdMap.get(instance)
}
async dispose() {
if (this.view && "dispose" in this.view) {
this.view.dispose()
@@ -59,8 +44,6 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
}
await this.controller.dispose()
WebviewProvider.activeInstances.delete(this)
// Remove from client ID map
WebviewProvider.clientIdMap.delete(this)
}
public static getVisibleInstance(): WebviewProvider | undefined {
@@ -262,9 +245,6 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
<script type="text/javascript" nonce="${nonce}">
// Inject the provider type
window.WEBVIEW_PROVIDER_TYPE = ${JSON.stringify(this.providerType)};
// Inject the client ID
window.clineClientId = "${this.clientId}";
</script>
<script type="module" nonce="${nonce}" src="${scriptUri}"></script>
</body>
@@ -378,9 +358,6 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
<script type="text/javascript" nonce="${nonce}">
// Inject the provider type
window.WEBVIEW_PROVIDER_TYPE = ${JSON.stringify(this.providerType)};
// Inject the client ID
window.clineClientId = "${this.clientId}";
</script>
${reactRefresh}
<script type="module" src="${scriptUri}"></script>
+7
View File
@@ -18,6 +18,13 @@ The Cline extension exposes an API that can be used by other extensions. To use
if (cline) {
// Now you can use the API
// Set custom instructions
await cline.setCustomInstructions("Talk like a pirate")
// Get custom instructions
const instructions = await cline.getCustomInstructions()
console.log("Current custom instructions:", instructions)
// Start a new task with an initial message
await cline.startNewTask("Hello, Cline! Let's make a new project...")
+64
View File
@@ -65,6 +65,59 @@ describe("ClineAPI Core Functionality", () => {
sandbox.restore()
})
describe("setCustomInstructions", () => {
it("should update custom instructions in controller", async () => {
const testInstructions = "Test custom instructions"
await api.setCustomInstructions(testInstructions)
// Verify controller method was called
sinon.assert.calledOnce(mockController.updateCustomInstructions)
sinon.assert.calledWith(mockController.updateCustomInstructions, testInstructions)
// Verify output channel was updated
sinon.assert.calledWith(mockOutputChannel.appendLine, "Custom instructions set")
})
it("should handle empty instructions", async () => {
await api.setCustomInstructions("")
sinon.assert.calledWith(mockController.updateCustomInstructions, "")
sinon.assert.calledWith(mockOutputChannel.appendLine, "Custom instructions set")
})
it("should handle very long instructions", async () => {
const longInstructions = "a".repeat(10000)
await api.setCustomInstructions(longInstructions)
sinon.assert.calledWith(mockController.updateCustomInstructions, longInstructions)
})
})
describe("getCustomInstructions", () => {
it("should retrieve custom instructions from state", async () => {
const testInstructions = "Retrieved instructions"
// The real implementation uses getGlobalState from the state module
getGlobalStateStub.resolves(testInstructions)
const result = await api.getCustomInstructions()
result!.should.equal(testInstructions)
sinon.assert.calledWith(getGlobalStateStub, mockController.context, "customInstructions")
})
it("should return undefined when no instructions set", async () => {
// The real implementation uses getGlobalState from the state module
getGlobalStateStub.resolves(undefined)
const result = await api.getCustomInstructions()
should.not.exist(result)
sinon.assert.calledWith(getGlobalStateStub, mockController.context, "customInstructions")
})
})
describe("startNewTask", () => {
it("should clear existing task and start new one with description", async () => {
const taskDescription = "Create a test function"
@@ -207,6 +260,17 @@ describe("ClineAPI Core Functionality", () => {
})
describe("Error Handling", () => {
it("should handle errors in setCustomInstructions", async () => {
mockController.updateCustomInstructions.rejects(new Error("Update failed"))
try {
await api.setCustomInstructions("test")
should.fail("", "", "Should have thrown an error", "")
} catch (error: any) {
error.message.should.equal("Update failed")
}
})
it("should handle errors in task initialization", async () => {
mockController.initTask.rejects(new Error("Init failed"))
+12
View File
@@ -1,4 +1,16 @@
export interface ClineAPI {
/**
* Sets the custom instructions in the global storage.
* @param value The custom instructions to be saved.
*/
setCustomInstructions(value: string): Promise<void>
/**
* Retrieves the custom instructions from the global storage.
* @returns The saved custom instructions, or undefined if not set.
*/
getCustomInstructions(): Promise<string | undefined>
/**
* Starts a new task with an optional initial message and images.
* @param task Optional initial task message.
+9
View File
@@ -7,6 +7,15 @@ import { WebviewProviderType as WebviewProviderTypeEnum } from "@shared/proto/ui
export function createClineAPI(outputChannel: vscode.OutputChannel, sidebarController: Controller): ClineAPI {
const api: ClineAPI = {
setCustomInstructions: async (value: string) => {
await sidebarController.updateCustomInstructions(value)
outputChannel.appendLine("Custom instructions set")
},
getCustomInstructions: async () => {
return (await getGlobalState(sidebarController.context, "customInstructions")) as string | undefined
},
startNewTask: async (task?: string, images?: string[]) => {
outputChannel.appendLine("Starting new task")
await sidebarController.clearTask()
+4 -11
View File
@@ -22,9 +22,7 @@ import { WebviewProviderType as WebviewProviderTypeEnum } from "@shared/proto/ui
import { WebviewProviderType } from "./shared/webview/types"
import { sendHistoryButtonClickedEvent } from "./core/controller/ui/subscribeToHistoryButtonClicked"
import { sendAccountButtonClickedEvent } from "./core/controller/ui/subscribeToAccountButtonClicked"
import { migratePlanActGlobalToWorkspaceStorage, migrateCustomInstructionsToGlobalRules } from "./core/storage/state"
import { sendFocusChatInputEvent } from "./core/controller/ui/subscribeToFocusChatInput"
/*
Built using https://github.com/microsoft/vscode-webview-ui-toolkit
@@ -46,12 +44,6 @@ export async function activate(context: vscode.ExtensionContext) {
Logger.initialize(outputChannel)
Logger.log("Cline extension activated")
// Migrate global storage values to workspace storage (one-time cleanup)
await migratePlanActGlobalToWorkspaceStorage(context)
// Migrate custom instructions to global Cline rules (one-time cleanup)
await migrateCustomInstructionsToGlobalRules(context)
// Version checking for autoupdate notification
const currentVersion = context.extension.packageJSON.version
const previousVersion = context.globalState.get<string>("clineVersion")
@@ -593,9 +585,10 @@ export async function activate(context: vscode.ExtensionContext) {
// At this point, activeWebviewProvider should be the one we want to send the message to.
// It could still be undefined if opening a new tab failed or timed out.
if (activeWebviewProvider) {
// Use the gRPC streaming method instead of postMessageToWebview
const clientId = activeWebviewProvider.getClientId()
sendFocusChatInputEvent(clientId)
activeWebviewProvider.controller.postMessageToWebview({
type: "action",
action: "focusChatInput",
})
} else {
console.error("FocusChatInput: Could not find or activate a Cline webview to focus.")
vscode.window.showErrorMessage(
-23
View File
@@ -1,23 +0,0 @@
import { StringRequest } from "@/shared/proto/common"
import { Uri } from "@/shared/proto/host/uri"
import { FileChangeEvent_ChangeType, SubscribeToFileRequest } from "@/shared/proto/host/watch"
const UriServiceClient = {
parse: function (_: StringRequest): Uri {
throw Error("Unimplemented")
},
}
const WatchServiceClient = {
subscribeToFile: function (
_r: SubscribeToFileRequest,
_h: {
onResponse?: (response: { type: FileChangeEvent_ChangeType }) => void | Promise<void>
onError?: (error: any) => void
onComplete?: () => void
},
) {
throw Error("Unimplemented")
},
}
export { UriServiceClient, WatchServiceClient }
-8
View File
@@ -1,8 +0,0 @@
import * as VscodeClient from "./vscode/client/host-grpc-client"
import * as ExternalClient from "./external/client/host-bridge-client"
const isHostBridgeExternal = process.env.HOST_BRIDGE_ADDRESS !== undefined && process.env.HOST_BRIDGE_ADDRESS !== "vscode"
const Client = isHostBridgeExternal ? ExternalClient : VscodeClient
export const UriServiceClient = Client.UriServiceClient
export const WatchServiceClient = Client.WatchServiceClient
+1 -126
View File
@@ -19,7 +19,7 @@ import * as fs from "fs/promises"
import * as path from "path"
import * as vscode from "vscode"
import { z } from "zod"
import { WatchServiceClient } from "@hosts/host-bridge-client"
import { WatchServiceClient } from "../../standalone/services/host-grpc-client"
import { FileChangeEvent_ChangeType, SubscribeToFileRequest } from "../../shared/proto/host/watch"
import { Metadata } from "../../shared/proto/common"
import {
@@ -54,17 +54,6 @@ export class McpHub {
connections: McpConnection[] = []
isConnecting: boolean = false
// Store notifications for display in chat
private pendingNotifications: Array<{
serverName: string
level: string
message: string
timestamp: number
}> = []
// Callback for sending notifications to active task
private notificationCallback?: (serverName: string, level: string, message: string) => void
constructor(
getMcpServersPath: () => Promise<string>,
getSettingsDirectoryPath: () => Promise<string>,
@@ -332,88 +321,6 @@ export class McpHub {
connection.server.status = "connected"
connection.server.error = ""
// Register notification handler for real-time messages
console.log(`[MCP Debug] Setting up notification handlers for server: ${name}`)
console.log(`[MCP Debug] Client instance:`, connection.client)
console.log(`[MCP Debug] Transport type:`, config.type)
// Try to set notification handler using the client's method
try {
// Import the notification schema from MCP SDK
const { z } = await import("zod")
// Define the notification schema for notifications/message
const NotificationMessageSchema = z.object({
method: z.literal("notifications/message"),
params: z
.object({
level: z.enum(["debug", "info", "warning", "error"]).optional(),
logger: z.string().optional(),
data: z.string().optional(),
message: z.string().optional(),
})
.optional(),
})
// Set the notification handler
connection.client.setNotificationHandler(NotificationMessageSchema as any, async (notification: any) => {
console.log(`[MCP Notification] ${name}:`, JSON.stringify(notification, null, 2))
const params = notification.params || {}
const level = params.level || "info"
const data = params.data || params.message || ""
const logger = params.logger || ""
console.log(`[MCP Message Notification] ${name}: level=${level}, data=${data}, logger=${logger}`)
// Format the message
const message = logger ? `[${logger}] ${data}` : data
// Send notification directly to active task if callback is set
if (this.notificationCallback) {
console.log(`[MCP Debug] Sending notification to active task: ${message}`)
this.notificationCallback(name, level, message)
} else {
// Fallback: store for later retrieval
console.log(`[MCP Debug] No active task, storing notification: ${message}`)
this.pendingNotifications.push({
serverName: name,
level,
message,
timestamp: Date.now(),
})
}
// Forward to webview if available
if (this.postMessageToWebview) {
await this.postMessageToWebview({
type: "mcpNotification",
serverName: name,
notification: {
level,
data,
logger,
timestamp: Date.now(),
},
} as any)
}
})
console.log(`[MCP Debug] Successfully set notifications/message handler for ${name}`)
// Also set a fallback handler for any other notification types
connection.client.fallbackNotificationHandler = async (notification: any) => {
console.log(`[MCP Fallback Notification] ${name}:`, JSON.stringify(notification, null, 2))
// Show in VS Code for visibility
vscode.window.showInformationMessage(
`MCP ${name}: ${notification.method || "unknown"} - ${JSON.stringify(notification.params || {})}`,
)
}
console.log(`[MCP Debug] Successfully set fallback notification handler for ${name}`)
} catch (error) {
console.error(`[MCP Debug] Error setting notification handlers for ${name}:`, error)
}
// Initial fetch of tools and resources
connection.server.tools = await this.fetchToolsList(name)
connection.server.resources = await this.fetchResourcesList(name)
@@ -1042,38 +949,6 @@ export class McpHub {
}
}
/**
* Get and clear pending notifications
* @returns Array of pending notifications
*/
getPendingNotifications(): Array<{
serverName: string
level: string
message: string
timestamp: number
}> {
const notifications = [...this.pendingNotifications]
this.pendingNotifications = []
return notifications
}
/**
* Set the notification callback for real-time notifications
* @param callback Function to call when notifications arrive
*/
setNotificationCallback(callback: (serverName: string, level: string, message: string) => void): void {
this.notificationCallback = callback
console.log("[MCP Debug] Notification callback set")
}
/**
* Clear the notification callback
*/
clearNotificationCallback(): void {
this.notificationCallback = undefined
console.log("[MCP Debug] Notification callback cleared")
}
async dispose(): Promise<void> {
this.removeAllFileWatchers()
for (const connection of this.connections) {
+2 -8
View File
@@ -13,13 +13,7 @@ import {
getFileChanges,
calculateToolSuccessRate,
} from "./GitHelper"
import {
updateGlobalState,
getAllExtensionState,
updateApiConfiguration,
storeSecret,
updateWorkspaceState,
} from "@core/storage/state"
import { updateGlobalState, getAllExtensionState, updateApiConfiguration, storeSecret } from "@core/storage/state"
import { ClineAsk, ExtensionMessage } from "@shared/ExtensionMessage"
import { ApiProvider } from "@shared/api"
import { HistoryItem } from "@shared/HistoryItem"
@@ -278,7 +272,7 @@ export function createTestServer(webviewProvider?: WebviewProvider): http.Server
await updateApiConfiguration(visibleWebview.controller.context, updatedConfig)
// Update global state to use cline provider
await updateWorkspaceState(visibleWebview.controller.context, "apiProvider", "cline" as ApiProvider)
await updateGlobalState(visibleWebview.controller.context, "apiProvider", "cline" as ApiProvider)
// Post state to webview to reflect changes
await visibleWebview.controller.postStateToWebview()
+2 -2
View File
@@ -25,7 +25,7 @@ export interface ExtensionMessage {
| "userCreditsPayments"
| "grpc_response" // New type for gRPC responses
text?: string
action?: "didBecomeVisible" | "accountLogoutClicked"
action?: "didBecomeVisible" | "accountLogoutClicked" | "focusChatInput"
state?: ExtensionState
images?: string[]
files?: string[]
@@ -81,6 +81,7 @@ export interface ExtensionState {
checkpointTrackerErrorMessage?: string
clineMessages: ClineMessage[]
currentTaskItem?: HistoryItem
customInstructions?: string
mcpMarketplaceEnabled?: boolean
planActSeparateModelsSetting: boolean
enableCheckpointsSetting?: boolean
@@ -162,7 +163,6 @@ export type ClineSay =
| "browser_action_result"
| "mcp_server_request_started"
| "mcp_server_response"
| "mcp_notification"
| "use_mcp_server"
| "diff_error"
| "deleted_api_reqs"
+2
View File
@@ -8,6 +8,7 @@ import { McpViewTab } from "./mcp"
export interface WebviewMessage {
type:
| "apiConfiguration"
| "requestVsCodeLmModels"
| "authStateChanged"
| "fetchMcpMarketplace"
@@ -45,6 +46,7 @@ export interface WebviewMessage {
mcpMarketplaceEnabled?: boolean
mcpResponsesCollapsed?: boolean
telemetrySetting?: TelemetrySetting
customInstructionsSetting?: string
mentionsRequestId?: string
query?: string
// For toggleFavoriteModel
+9 -3
View File
@@ -598,6 +598,9 @@ export const vertexModels = {
cacheReadsPrice: 0.625,
},
],
thinkingConfig: {
maxBudget: 32768,
},
},
"gemini-2.5-flash-preview-04-17": {
maxTokens: 65536,
@@ -766,6 +769,9 @@ export const geminiModels = {
cacheReadsPrice: 0.625,
},
],
thinkingConfig: {
maxBudget: 32768,
},
},
"gemini-2.5-flash-preview-05-20": {
maxTokens: 65536,
@@ -917,9 +923,9 @@ export const openAiNativeModels = {
contextWindow: 200_000,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 2.0,
outputPrice: 8.0,
cacheReadsPrice: 0.5,
inputPrice: 10.0,
outputPrice: 40.0,
cacheReadsPrice: 2.5,
},
"o4-mini": {
maxTokens: 100_000,
@@ -89,7 +89,6 @@ function convertClineSayToProtoEnum(say: AppClineSay | undefined): ClineSay | un
browser_action_result: ClineSay.BROWSER_ACTION_RESULT,
mcp_server_request_started: ClineSay.MCP_SERVER_REQUEST_STARTED,
mcp_server_response: ClineSay.MCP_SERVER_RESPONSE,
mcp_notification: ClineSay.MCP_NOTIFICATION,
use_mcp_server: ClineSay.USE_MCP_SERVER_SAY,
diff_error: ClineSay.DIFF_ERROR,
deleted_api_reqs: ClineSay.DELETED_API_REQS,
@@ -133,7 +132,6 @@ function convertProtoEnumToClineSay(say: ClineSay): AppClineSay | undefined {
[ClineSay.BROWSER_ACTION_RESULT]: "browser_action_result",
[ClineSay.MCP_SERVER_REQUEST_STARTED]: "mcp_server_request_started",
[ClineSay.MCP_SERVER_RESPONSE]: "mcp_server_response",
[ClineSay.MCP_NOTIFICATION]: "mcp_notification",
[ClineSay.USE_MCP_SERVER_SAY]: "use_mcp_server",
[ClineSay.DIFF_ERROR]: "diff_error",
[ClineSay.DELETED_API_REQS]: "deleted_api_reqs",
@@ -1,442 +0,0 @@
import {
ApiConfiguration,
ApiProvider,
BedrockModelId,
ModelInfo,
OpenAiCompatibleModelInfo as AppOpenAiCompatibleModelInfo,
LiteLLMModelInfo as AppLiteLLMModelInfo,
} from "../../api"
import {
ModelsApiConfiguration as ProtoApiConfiguration,
ApiProvider as ProtoApiProvider,
LiteLLMModelInfo,
OpenAiCompatibleModelInfo,
OpenRouterModelInfo,
ThinkingConfig,
} from "../../proto/models"
// Convert application ThinkingConfig to proto ThinkingConfig
function convertThinkingConfigToProto(config: ModelInfo["thinkingConfig"]): ThinkingConfig | undefined {
if (!config) {
return undefined
}
return {
maxBudget: config.maxBudget,
outputPrice: config.outputPrice,
outputPriceTiers: config.outputPriceTiers || [], // Provide empty array if undefined
}
}
// Convert proto ThinkingConfig to application ThinkingConfig
function convertProtoToThinkingConfig(config: ThinkingConfig | undefined): ModelInfo["thinkingConfig"] | undefined {
if (!config) {
return undefined
}
return {
maxBudget: config.maxBudget,
outputPrice: config.outputPrice,
outputPriceTiers: config.outputPriceTiers.length > 0 ? config.outputPriceTiers : undefined,
}
}
// Convert application ModelInfo to proto OpenRouterModelInfo
function convertModelInfoToProtoOpenRouter(info: ModelInfo | undefined): OpenRouterModelInfo | undefined {
if (!info) {
return undefined
}
return {
maxTokens: info.maxTokens,
contextWindow: info.contextWindow,
supportsImages: info.supportsImages,
supportsPromptCache: info.supportsPromptCache ?? false,
inputPrice: info.inputPrice,
outputPrice: info.outputPrice,
cacheWritesPrice: info.cacheWritesPrice,
cacheReadsPrice: info.cacheReadsPrice,
description: info.description,
thinkingConfig: convertThinkingConfigToProto(info.thinkingConfig),
supportsGlobalEndpoint: info.supportsGlobalEndpoint,
tiers: info.tiers || [],
}
}
// Convert proto OpenRouterModelInfo to application ModelInfo
function convertProtoToModelInfo(info: OpenRouterModelInfo | undefined): ModelInfo | undefined {
if (!info) {
return undefined
}
return {
maxTokens: info.maxTokens,
contextWindow: info.contextWindow,
supportsImages: info.supportsImages,
supportsPromptCache: info.supportsPromptCache,
inputPrice: info.inputPrice,
outputPrice: info.outputPrice,
cacheWritesPrice: info.cacheWritesPrice,
cacheReadsPrice: info.cacheReadsPrice,
description: info.description,
thinkingConfig: convertProtoToThinkingConfig(info.thinkingConfig),
supportsGlobalEndpoint: info.supportsGlobalEndpoint,
tiers: info.tiers.length > 0 ? info.tiers : undefined,
}
}
// Convert application LiteLLMModelInfo to proto LiteLLMModelInfo
function convertLiteLLMModelInfoToProto(info: AppLiteLLMModelInfo | undefined): LiteLLMModelInfo | undefined {
if (!info) {
return undefined
}
return {
maxTokens: info.maxTokens,
contextWindow: info.contextWindow,
supportsImages: info.supportsImages,
supportsPromptCache: info.supportsPromptCache ?? false,
inputPrice: info.inputPrice,
outputPrice: info.outputPrice,
thinkingConfig: convertThinkingConfigToProto(info.thinkingConfig),
supportsGlobalEndpoint: info.supportsGlobalEndpoint,
cacheWritesPrice: info.cacheWritesPrice,
cacheReadsPrice: info.cacheReadsPrice,
description: info.description,
tiers: info.tiers || [],
temperature: info.temperature,
}
}
// Convert proto LiteLLMModelInfo to application LiteLLMModelInfo
function convertProtoToLiteLLMModelInfo(info: LiteLLMModelInfo | undefined): AppLiteLLMModelInfo | undefined {
if (!info) {
return undefined
}
return {
maxTokens: info.maxTokens,
contextWindow: info.contextWindow,
supportsImages: info.supportsImages,
supportsPromptCache: info.supportsPromptCache,
inputPrice: info.inputPrice,
outputPrice: info.outputPrice,
thinkingConfig: convertProtoToThinkingConfig(info.thinkingConfig),
supportsGlobalEndpoint: info.supportsGlobalEndpoint,
cacheWritesPrice: info.cacheWritesPrice,
cacheReadsPrice: info.cacheReadsPrice,
description: info.description,
tiers: info.tiers.length > 0 ? info.tiers : undefined,
temperature: info.temperature,
}
}
// Convert application OpenAiCompatibleModelInfo to proto OpenAiCompatibleModelInfo
function convertOpenAiCompatibleModelInfoToProto(
info: AppOpenAiCompatibleModelInfo | undefined,
): OpenAiCompatibleModelInfo | undefined {
if (!info) {
return undefined
}
return {
maxTokens: info.maxTokens,
contextWindow: info.contextWindow,
supportsImages: info.supportsImages,
supportsPromptCache: info.supportsPromptCache ?? false,
inputPrice: info.inputPrice,
outputPrice: info.outputPrice,
thinkingConfig: convertThinkingConfigToProto(info.thinkingConfig),
supportsGlobalEndpoint: info.supportsGlobalEndpoint,
cacheWritesPrice: info.cacheWritesPrice,
cacheReadsPrice: info.cacheReadsPrice,
description: info.description,
tiers: info.tiers || [],
temperature: info.temperature,
isR1FormatRequired: info.isR1FormatRequired,
}
}
// Convert proto OpenAiCompatibleModelInfo to application OpenAiCompatibleModelInfo
function convertProtoToOpenAiCompatibleModelInfo(
info: OpenAiCompatibleModelInfo | undefined,
): AppOpenAiCompatibleModelInfo | undefined {
if (!info) {
return undefined
}
return {
maxTokens: info.maxTokens,
contextWindow: info.contextWindow,
supportsImages: info.supportsImages,
supportsPromptCache: info.supportsPromptCache,
inputPrice: info.inputPrice,
outputPrice: info.outputPrice,
thinkingConfig: convertProtoToThinkingConfig(info.thinkingConfig),
supportsGlobalEndpoint: info.supportsGlobalEndpoint,
cacheWritesPrice: info.cacheWritesPrice,
cacheReadsPrice: info.cacheReadsPrice,
description: info.description,
tiers: info.tiers.length > 0 ? info.tiers : undefined,
temperature: info.temperature,
isR1FormatRequired: info.isR1FormatRequired,
}
}
// Convert application ApiProvider to proto ApiProvider
function convertApiProviderToProto(provider: string | undefined): ProtoApiProvider {
switch (provider) {
case "anthropic":
return ProtoApiProvider.ANTHROPIC
case "openrouter":
return ProtoApiProvider.OPENROUTER
case "bedrock":
return ProtoApiProvider.BEDROCK
case "vertex":
return ProtoApiProvider.VERTEX
case "openai":
return ProtoApiProvider.OPENAI
case "ollama":
return ProtoApiProvider.OLLAMA
case "lmstudio":
return ProtoApiProvider.LMSTUDIO
case "gemini":
return ProtoApiProvider.GEMINI
case "openai-native":
return ProtoApiProvider.OPENAI_NATIVE
case "requesty":
return ProtoApiProvider.REQUESTY
case "together":
return ProtoApiProvider.TOGETHER
case "deepseek":
return ProtoApiProvider.DEEPSEEK
case "qwen":
return ProtoApiProvider.QWEN
case "doubao":
return ProtoApiProvider.DOUBAO
case "mistral":
return ProtoApiProvider.MISTRAL
case "vscode-lm":
return ProtoApiProvider.VSCODE_LM
case "cline":
return ProtoApiProvider.CLINE
case "litellm":
return ProtoApiProvider.LITELLM
case "nebius":
return ProtoApiProvider.NEBIUS
case "fireworks":
return ProtoApiProvider.FIREWORKS
case "asksage":
return ProtoApiProvider.ASKSAGE
case "xai":
return ProtoApiProvider.XAI
case "sambanova":
return ProtoApiProvider.SAMBANOVA
case "cerebras":
return ProtoApiProvider.CEREBRAS
default:
return ProtoApiProvider.ANTHROPIC
}
}
// Convert proto ApiProvider to application ApiProvider
function convertProtoToApiProvider(provider: ProtoApiProvider): ApiProvider {
switch (provider) {
case ProtoApiProvider.ANTHROPIC:
return "anthropic"
case ProtoApiProvider.OPENROUTER:
return "openrouter"
case ProtoApiProvider.BEDROCK:
return "bedrock"
case ProtoApiProvider.VERTEX:
return "vertex"
case ProtoApiProvider.OPENAI:
return "openai"
case ProtoApiProvider.OLLAMA:
return "ollama"
case ProtoApiProvider.LMSTUDIO:
return "lmstudio"
case ProtoApiProvider.GEMINI:
return "gemini"
case ProtoApiProvider.OPENAI_NATIVE:
return "openai-native"
case ProtoApiProvider.REQUESTY:
return "requesty"
case ProtoApiProvider.TOGETHER:
return "together"
case ProtoApiProvider.DEEPSEEK:
return "deepseek"
case ProtoApiProvider.QWEN:
return "qwen"
case ProtoApiProvider.DOUBAO:
return "doubao"
case ProtoApiProvider.MISTRAL:
return "mistral"
case ProtoApiProvider.VSCODE_LM:
return "vscode-lm"
case ProtoApiProvider.CLINE:
return "cline"
case ProtoApiProvider.LITELLM:
return "litellm"
case ProtoApiProvider.NEBIUS:
return "nebius"
case ProtoApiProvider.FIREWORKS:
return "fireworks"
case ProtoApiProvider.ASKSAGE:
return "asksage"
case ProtoApiProvider.XAI:
return "xai"
case ProtoApiProvider.SAMBANOVA:
return "sambanova"
case ProtoApiProvider.CEREBRAS:
return "cerebras"
default:
return "anthropic"
}
}
// Converts application ApiConfiguration to proto ApiConfiguration
export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoApiConfiguration {
return {
apiModelId: config.apiModelId,
apiKey: config.apiKey,
clineApiKey: config.clineApiKey,
taskId: config.taskId,
liteLlmBaseUrl: config.liteLlmBaseUrl,
liteLlmModelId: config.liteLlmModelId,
liteLlmApiKey: config.liteLlmApiKey,
liteLlmUsePromptCache: config.liteLlmUsePromptCache,
openAiHeaders: config.openAiHeaders || {},
liteLlmModelInfo: convertLiteLLMModelInfoToProto(config.liteLlmModelInfo),
anthropicBaseUrl: config.anthropicBaseUrl,
openRouterApiKey: config.openRouterApiKey,
openRouterModelId: config.openRouterModelId,
openRouterModelInfo: convertModelInfoToProtoOpenRouter(config.openRouterModelInfo),
openRouterProviderSorting: config.openRouterProviderSorting,
awsAccessKey: config.awsAccessKey,
awsSecretKey: config.awsSecretKey,
awsSessionToken: config.awsSessionToken,
awsRegion: config.awsRegion,
awsUseCrossRegionInference: config.awsUseCrossRegionInference,
awsBedrockUsePromptCache: config.awsBedrockUsePromptCache,
awsUseProfile: config.awsUseProfile,
awsProfile: config.awsProfile,
awsBedrockEndpoint: config.awsBedrockEndpoint,
awsBedrockCustomSelected: config.awsBedrockCustomSelected,
awsBedrockCustomModelBaseId: config.awsBedrockCustomModelBaseId as string | undefined,
vertexProjectId: config.vertexProjectId,
vertexRegion: config.vertexRegion,
openAiBaseUrl: config.openAiBaseUrl,
openAiApiKey: config.openAiApiKey,
openAiModelId: config.openAiModelId,
openAiModelInfo: convertOpenAiCompatibleModelInfoToProto(config.openAiModelInfo),
ollamaModelId: config.ollamaModelId,
ollamaBaseUrl: config.ollamaBaseUrl,
ollamaApiOptionsCtxNum: config.ollamaApiOptionsCtxNum,
lmStudioModelId: config.lmStudioModelId,
lmStudioBaseUrl: config.lmStudioBaseUrl,
geminiApiKey: config.geminiApiKey,
geminiBaseUrl: config.geminiBaseUrl,
openAiNativeApiKey: config.openAiNativeApiKey,
deepSeekApiKey: config.deepSeekApiKey,
requestyApiKey: config.requestyApiKey,
requestyModelId: config.requestyModelId,
requestyModelInfo: convertModelInfoToProtoOpenRouter(config.requestyModelInfo),
togetherApiKey: config.togetherApiKey,
togetherModelId: config.togetherModelId,
fireworksApiKey: config.fireworksApiKey,
fireworksModelId: config.fireworksModelId,
fireworksModelMaxCompletionTokens: config.fireworksModelMaxCompletionTokens,
fireworksModelMaxTokens: config.fireworksModelMaxTokens,
qwenApiKey: config.qwenApiKey,
doubaoApiKey: config.doubaoApiKey,
mistralApiKey: config.mistralApiKey,
azureApiVersion: config.azureApiVersion,
vsCodeLmModelSelector: config.vsCodeLmModelSelector,
qwenApiLine: config.qwenApiLine,
nebiusApiKey: config.nebiusApiKey,
asksageApiUrl: config.asksageApiUrl,
asksageApiKey: config.asksageApiKey,
xaiApiKey: config.xaiApiKey,
thinkingBudgetTokens: config.thinkingBudgetTokens,
reasoningEffort: config.reasoningEffort,
sambanovaApiKey: config.sambanovaApiKey,
cerebrasApiKey: config.cerebrasApiKey,
requestTimeoutMs: config.requestTimeoutMs,
apiProvider: config.apiProvider ? convertApiProviderToProto(config.apiProvider) : undefined,
favoritedModelIds: config.favoritedModelIds || [],
}
}
// Converts proto ApiConfiguration to application ApiConfiguration
export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguration): ApiConfiguration {
return {
apiModelId: protoConfig.apiModelId,
apiKey: protoConfig.apiKey,
clineApiKey: protoConfig.clineApiKey,
taskId: protoConfig.taskId,
liteLlmBaseUrl: protoConfig.liteLlmBaseUrl,
liteLlmModelId: protoConfig.liteLlmModelId,
liteLlmApiKey: protoConfig.liteLlmApiKey,
liteLlmUsePromptCache: protoConfig.liteLlmUsePromptCache,
openAiHeaders: Object.keys(protoConfig.openAiHeaders).length > 0 ? protoConfig.openAiHeaders : undefined,
liteLlmModelInfo: convertProtoToLiteLLMModelInfo(protoConfig.liteLlmModelInfo),
anthropicBaseUrl: protoConfig.anthropicBaseUrl,
openRouterApiKey: protoConfig.openRouterApiKey,
openRouterModelId: protoConfig.openRouterModelId,
openRouterModelInfo: convertProtoToModelInfo(protoConfig.openRouterModelInfo),
openRouterProviderSorting: protoConfig.openRouterProviderSorting,
awsAccessKey: protoConfig.awsAccessKey,
awsSecretKey: protoConfig.awsSecretKey,
awsSessionToken: protoConfig.awsSessionToken,
awsRegion: protoConfig.awsRegion,
awsUseCrossRegionInference: protoConfig.awsUseCrossRegionInference,
awsBedrockUsePromptCache: protoConfig.awsBedrockUsePromptCache,
awsUseProfile: protoConfig.awsUseProfile,
awsProfile: protoConfig.awsProfile,
awsBedrockEndpoint: protoConfig.awsBedrockEndpoint,
awsBedrockCustomSelected: protoConfig.awsBedrockCustomSelected,
awsBedrockCustomModelBaseId: protoConfig.awsBedrockCustomModelBaseId as BedrockModelId | undefined,
vertexProjectId: protoConfig.vertexProjectId,
vertexRegion: protoConfig.vertexRegion,
openAiBaseUrl: protoConfig.openAiBaseUrl,
openAiApiKey: protoConfig.openAiApiKey,
openAiModelId: protoConfig.openAiModelId,
openAiModelInfo: convertProtoToOpenAiCompatibleModelInfo(protoConfig.openAiModelInfo),
ollamaModelId: protoConfig.ollamaModelId,
ollamaBaseUrl: protoConfig.ollamaBaseUrl,
ollamaApiOptionsCtxNum: protoConfig.ollamaApiOptionsCtxNum,
lmStudioModelId: protoConfig.lmStudioModelId,
lmStudioBaseUrl: protoConfig.lmStudioBaseUrl,
geminiApiKey: protoConfig.geminiApiKey,
geminiBaseUrl: protoConfig.geminiBaseUrl,
openAiNativeApiKey: protoConfig.openAiNativeApiKey,
deepSeekApiKey: protoConfig.deepSeekApiKey,
requestyApiKey: protoConfig.requestyApiKey,
requestyModelId: protoConfig.requestyModelId,
requestyModelInfo: convertProtoToModelInfo(protoConfig.requestyModelInfo),
togetherApiKey: protoConfig.togetherApiKey,
togetherModelId: protoConfig.togetherModelId,
fireworksApiKey: protoConfig.fireworksApiKey,
fireworksModelId: protoConfig.fireworksModelId,
fireworksModelMaxCompletionTokens: protoConfig.fireworksModelMaxCompletionTokens,
fireworksModelMaxTokens: protoConfig.fireworksModelMaxTokens,
qwenApiKey: protoConfig.qwenApiKey,
doubaoApiKey: protoConfig.doubaoApiKey,
mistralApiKey: protoConfig.mistralApiKey,
azureApiVersion: protoConfig.azureApiVersion,
vsCodeLmModelSelector: protoConfig.vsCodeLmModelSelector,
qwenApiLine: protoConfig.qwenApiLine,
nebiusApiKey: protoConfig.nebiusApiKey,
asksageApiUrl: protoConfig.asksageApiUrl,
asksageApiKey: protoConfig.asksageApiKey,
xaiApiKey: protoConfig.xaiApiKey,
thinkingBudgetTokens: protoConfig.thinkingBudgetTokens,
reasoningEffort: protoConfig.reasoningEffort,
sambanovaApiKey: protoConfig.sambanovaApiKey,
cerebrasApiKey: protoConfig.cerebrasApiKey,
requestTimeoutMs: protoConfig.requestTimeoutMs,
apiProvider: protoConfig.apiProvider !== undefined ? convertProtoToApiProvider(protoConfig.apiProvider) : undefined,
favoritedModelIds: protoConfig.favoritedModelIds.length > 0 ? protoConfig.favoritedModelIds : undefined,
}
}
@@ -1,4 +1,4 @@
import { LanguageModelChatSelector } from "../../proto/models"
import { VsCodeLmModel } from "../../proto/models"
/**
* Represents a VS Code language model in the native VS Code format
@@ -13,7 +13,7 @@ export interface VsCodeNativeModel {
/**
* Converts VS Code native model format to protobuf format
*/
export function convertVsCodeNativeModelsToProtoModels(models: VsCodeNativeModel[]): LanguageModelChatSelector[] {
export function convertVsCodeNativeModelsToProtoModels(models: VsCodeNativeModel[]): VsCodeLmModel[] {
return (models || []).map((model) => ({
vendor: model.vendor || "",
family: model.family || "",
@@ -1,4 +1,4 @@
import { ApiConfiguration, ApiProvider, BedrockModelId } from "@shared/api"
import { ApiConfiguration } from "@shared/api"
import { ChatSettings } from "@shared/ChatSettings"
import {
ApiConfiguration as ProtoApiConfiguration,
@@ -122,7 +122,7 @@ export function convertProtoApiConfigurationToApiConfiguration(protoConfig: Prot
// eslint-disable-next-line eslint-rules/no-protobuf-object-literals
const config: ApiConfiguration = {
// Core API fields
apiProvider: protoConfig.apiProvider as ApiProvider,
apiProvider: protoConfig.apiProvider as any,
apiModelId: protoConfig.apiModelId,
apiKey: protoConfig.apiKey,
@@ -158,7 +158,7 @@ export function convertProtoApiConfigurationToApiConfiguration(protoConfig: Prot
// AWS Bedrock fields
awsBedrockCustomSelected: protoConfig.awsBedrockCustomSelected,
awsBedrockCustomModelBaseId: protoConfig.awsBedrockCustomModelBaseId as BedrockModelId | undefined,
awsBedrockCustomModelBaseId: protoConfig.awsBedrockCustomModelBaseId as any,
awsAccessKey: protoConfig.awsAccessKey,
awsSecretKey: protoConfig.awsSecretKey,
awsSessionToken: protoConfig.awsSessionToken,
@@ -1,5 +1,5 @@
import { v4 as uuidv4 } from "uuid"
import { GrpcHandler, StreamingCallbacks } from "../host-grpc-handler"
import { GrpcHandler, StreamingCallbacks } from "../../../hosts/vscode/host-grpc-handler"
// Generic type for any protobuf service definition
export type ProtoService = {
-7
View File
@@ -1,7 +0,0 @@
import { ApiHandler } from "@api/index"
export function isClaude4ModelFamily(api: ApiHandler): boolean {
const model = api.getModel()
const modelId = model.id
return modelId.includes("sonnet-4") || modelId.includes("opus-4")
}
-1
View File
@@ -24,7 +24,6 @@
"@/*": ["src/*"],
"@api/*": ["src/api/*"],
"@core/*": ["src/core/*"],
"@hosts/*": ["src/hosts/*"],
"@integrations/*": ["src/integrations/*"],
"@services/*": ["src/services/*"],
"@shared/*": ["src/shared/*"],
@@ -1054,36 +1054,6 @@ export const ChatRowContent = ({
return null // we should never see this message type
case "mcp_server_response":
return <McpResponseDisplay responseText={message.text || ""} />
case "mcp_notification":
return (
<div
style={{
display: "flex",
alignItems: "flex-start",
gap: "8px",
padding: "8px 12px",
backgroundColor: "var(--vscode-textBlockQuote-background)",
borderRadius: "4px",
fontSize: "13px",
color: "var(--vscode-foreground)",
opacity: 0.9,
marginBottom: "8px",
}}>
<i
className="codicon codicon-bell"
style={{
marginTop: "2px",
fontSize: "14px",
color: "var(--vscode-notificationsInfoIcon-foreground)",
flexShrink: 0,
}}
/>
<div style={{ flex: 1, wordBreak: "break-word" }}>
<span style={{ fontWeight: 500 }}>MCP Notification: </span>
<span className="ph-no-capture">{message.text}</span>
</div>
</div>
)
case "text":
return (
<WithCopyButton ref={contentRef} onMouseUp={handleMouseUp} textToCopy={message.text}>
@@ -6,7 +6,7 @@ import Thumbnails from "@/components/common/Thumbnails"
import Tooltip from "@/components/common/Tooltip"
import ApiOptions, { normalizeApiConfiguration } from "@/components/settings/ApiOptions"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { FileServiceClient, StateServiceClient, ModelsServiceClient } from "@/services/grpc-client"
import { FileServiceClient, StateServiceClient } from "@/services/grpc-client"
import {
ContextMenuOptionType,
getContextMenuOptions,
@@ -34,8 +34,6 @@ import { mentionRegex, mentionRegexGlobal } from "@shared/context-mentions"
import { ExtensionMessage } from "@shared/ExtensionMessage"
import { EmptyRequest, StringRequest } from "@shared/proto/common"
import { FileSearchRequest, RelativePathsRequest } from "@shared/proto/file"
import { UpdateApiConfigurationRequest } from "@shared/proto/models"
import { convertApiConfigurationToProto } from "@shared/proto-conversions/models/api-configuration-conversion"
import { PlanActMode, TogglePlanActModeRequest } from "@shared/proto/state"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import React, { forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"
@@ -964,20 +962,12 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
)
// Separate the API config submission logic
const submitApiConfig = useCallback(async () => {
const submitApiConfig = useCallback(() => {
const apiValidationResult = validateApiConfiguration(apiConfiguration)
const modelIdValidationResult = validateModelId(apiConfiguration, openRouterModels)
if (!apiValidationResult && !modelIdValidationResult && apiConfiguration) {
try {
await ModelsServiceClient.updateApiConfigurationProto(
UpdateApiConfigurationRequest.create({
apiConfiguration: convertApiConfigurationToProto(apiConfiguration),
}),
)
} catch (error) {
console.error("Failed to update API configuration:", error)
}
if (!apiValidationResult && !modelIdValidationResult) {
vscode.postMessage({ type: "apiConfiguration", apiConfiguration })
} else {
StateServiceClient.getLatestState(EmptyRequest.create())
.then(() => {
+7 -24
View File
@@ -95,14 +95,7 @@ export const MAX_IMAGES_AND_FILES_PER_MESSAGE = 20
const QUICK_WINS_HISTORY_THRESHOLD = 300
const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryView }: ChatViewProps) => {
const {
version,
clineMessages: messages,
taskHistory,
apiConfiguration,
telemetrySetting,
navigateToChat,
} = useExtensionState()
const { version, clineMessages: messages, taskHistory, apiConfiguration, telemetrySetting } = useExtensionState()
const shouldShowQuickWins = false // !taskHistory || taskHistory.length < QUICK_WINS_HISTORY_THRESHOLD
//const task = messages.length > 0 ? (messages[0].say === "task" ? messages[0] : undefined) : undefined) : undefined
const task = useMemo(() => messages.at(0), [messages]) // leaving this less safe version here since if the first message is not a task, then the extension is in a bad state and needs to be debugged (see Cline.abort)
@@ -706,6 +699,12 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
textAreaRef.current?.focus()
}
break
case "focusChatInput":
textAreaRef.current?.focus()
if (isHidden) {
window.dispatchEvent(new CustomEvent("chatButtonClicked"))
}
break
}
break
}
@@ -716,22 +715,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
useEvent("message", handleMessage)
// Listen for local focusChatInput event
useEffect(() => {
const handleFocusChatInput = () => {
if (isHidden) {
navigateToChat()
}
textAreaRef.current?.focus()
}
window.addEventListener("focusChatInput", handleFocusChatInput)
return () => {
window.removeEventListener("focusChatInput", handleFocusChatInput)
}
}, [isHidden])
// Set up addToInput subscription
useEffect(() => {
const cleanup = UiServiceClient.subscribeToAddToInput(EmptyRequest.create({}), {
@@ -39,19 +39,66 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
}
return (
<div className="flex-shrink-0">
<div style={{ flexShrink: 0 }}>
<style>
{`
.history-preview-item {
background-color: color-mix(in srgb, var(--vscode-toolbar-hoverBackground) 65%, transparent);
border-radius: 4px;
position: relative;
overflow: hidden;
opacity: 0.8;
cursor: pointer;
margin-bottom: 12px;
}
.history-preview-item:hover {
background-color: color-mix(in srgb, var(--vscode-toolbar-hoverBackground) 100%, transparent);
opacity: 1;
pointer-events: auto;
}
.history-header {
cursor: pointer;
user-select: none;
}
.history-header:hover {
opacity: 0.8;
}
`}
</style>
<div
className="flex items-center gap-2 mx-5 my-2 cursor-pointer select-none text-[var(--vscode-descriptionForeground)] hover:opacity-80 transition-all duration-200 rounded-lg px-2 py-1 hover:bg-[var(--vscode-toolbar-hoverBackground)]"
onClick={toggleExpanded}>
className="history-header"
onClick={toggleExpanded}
style={{
color: "var(--vscode-descriptionForeground)",
margin: "10px 20px 10px 20px",
display: "flex",
alignItems: "center",
}}>
<span
className={`codicon codicon-chevron-${isExpanded ? "down" : "right"} scale-90 transition-transform duration-200`}
/>
<span className="codicon codicon-comment-discussion scale-90" />
<span className="font-medium text-xs uppercase tracking-wide">Recent Tasks</span>
className={`codicon codicon-chevron-${isExpanded ? "down" : "right"}`}
style={{
marginRight: "4px",
transform: "scale(0.9)",
}}></span>
<span
className="codicon codicon-comment-discussion"
style={{
marginRight: "4px",
transform: "scale(0.9)",
}}></span>
<span
style={{
fontWeight: 500,
fontSize: "0.85em",
textTransform: "uppercase",
}}>
Recent Tasks
</span>
</div>
{isExpanded && (
<div className="px-5 space-y-3">
<div style={{ padding: "0px 20px 0 20px" }}>
{taskHistory.filter((item) => item.ts && item.task).length > 0 ? (
<>
{taskHistory
@@ -60,58 +107,61 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
.map((item) => (
<div
key={item.id}
className="relative rounded-xl p-3 cursor-pointer overflow-hidden transition-all duration-150 ease-out hover:scale-[1.02] hover:shadow-xl group hover:bg-[color-mix(in_srgb,var(--vscode-toolbar-hoverBackground)_50%,transparent)] hover:border-[color-mix(in_srgb,var(--vscode-panel-border)_80%,transparent)]"
style={{
backgroundColor:
"color-mix(in srgb, var(--vscode-toolbar-hoverBackground) 30%, transparent)",
border: "1px solid color-mix(in srgb, var(--vscode-panel-border) 50%, transparent)",
backdropFilter: "blur(8px)",
}}
className="history-preview-item"
onClick={() => handleHistorySelect(item.id)}>
{/* Subtle gradient overlay for extra depth */}
<div
className="absolute inset-0 transition-all duration-150 rounded-xl opacity-0 group-hover:opacity-100"
style={{
background:
"linear-gradient(135deg, color-mix(in srgb, var(--vscode-button-background) 5%, transparent) 0%, color-mix(in srgb, var(--vscode-focusBorder) 3%, transparent) 100%)",
}}
/>
{item.isFavorited && (
<div
className="absolute top-3 right-3 z-20 drop-shadow-sm"
style={{ color: "var(--vscode-button-background)" }}>
<span className="codicon codicon-star-full" aria-label="Favorited" />
</div>
)}
<div className="relative z-10">
<div className="mb-2">
<span className="text-[var(--vscode-descriptionForeground)] font-medium text-xs uppercase tracking-wider opacity-80">
<div style={{ padding: "12px" }}>
<div style={{ marginBottom: "8px" }}>
<span
style={{
color: "var(--vscode-descriptionForeground)",
fontWeight: 500,
fontSize: "0.85em",
textTransform: "uppercase",
}}>
{formatDate(item.ts)}
</span>
</div>
{item.isFavorited && (
<div
style={{
position: "absolute",
top: "12px",
right: "12px",
color: "var(--vscode-button-background)",
}}>
<span className="codicon codicon-star-full" aria-label="Favorited" />
</div>
)}
<div
id={`history-preview-task-${item.id}`}
className="text-[var(--vscode-descriptionForeground)] mb-2 line-clamp-3 whitespace-pre-wrap break-words"
style={{ fontSize: "var(--vscode-font-size)" }}>
className="history-preview-task"
style={{
fontSize: "var(--vscode-font-size)",
color: "var(--vscode-descriptionForeground)",
marginBottom: "8px",
display: "-webkit-box",
WebkitLineClamp: 3,
WebkitBoxOrient: "vertical",
overflow: "hidden",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
overflowWrap: "anywhere",
}}>
<span className="ph-no-capture">{item.task}</span>
</div>
<div className="text-xs text-[var(--vscode-descriptionForeground)] opacity-75 space-x-1">
<div
style={{
fontSize: "0.85em",
color: "var(--vscode-descriptionForeground)",
}}>
<span>
Tokens: {formatLargeNumber(item.tokensIn || 0)}
{formatLargeNumber(item.tokensOut || 0)}
</span>
{!!item.cacheWrites && (
<>
<span
style={{
color: "color-mix(in srgb, var(--vscode-descriptionForeground) 40%, transparent)",
}}>
</span>
{" • "}
<span>
Cache: +{formatLargeNumber(item.cacheWrites || 0)} {" "}
{formatLargeNumber(item.cacheReads || 0)}
@@ -120,12 +170,7 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
)}
{!!item.totalCost && (
<>
<span
style={{
color: "color-mix(in srgb, var(--vscode-descriptionForeground) 40%, transparent)",
}}>
</span>
{" • "}
<span>API Cost: ${item.totalCost?.toFixed(4)}</span>
</>
)}
@@ -133,27 +178,35 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
</div>
</div>
))}
<div className="flex items-center justify-center pt-2">
<button
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
}}>
<VSCodeButton
appearance="icon"
onClick={() => showHistoryView()}
className="cursor-pointer text-center transition-all duration-150 hover:opacity-80 flex items-center gap-1 bg-transparent border-none outline-none focus:outline-none"
style={{
color: "var(--vscode-descriptionForeground)",
fontSize: "var(--vscode-font-size)",
opacity: 0.9,
}}>
<span className="codicon codicon-history scale-90"></span>
<span className="font-medium">View all history</span>
</button>
<div
style={{
fontSize: "var(--vscode-font-size)",
color: "var(--vscode-descriptionForeground)",
}}>
View all history
</div>
</VSCodeButton>
</div>
</>
) : (
<div
className="text-center text-[var(--vscode-descriptionForeground)] py-4 rounded-xl"
style={{
textAlign: "center",
color: "var(--vscode-descriptionForeground)",
fontSize: "var(--vscode-font-size)",
backgroundColor: "color-mix(in srgb, var(--vscode-toolbar-hoverBackground) 20%, transparent)",
border: "1px solid color-mix(in srgb, var(--vscode-panel-border) 30%, transparent)",
backdropFilter: "blur(8px)",
padding: "10px 0",
}}>
No recent tasks
</div>
@@ -20,7 +20,7 @@ const McpMarketplaceView = () => {
const [isRefreshing, setIsRefreshing] = useState(false)
const [searchQuery, setSearchQuery] = useState("")
const [selectedCategory, setSelectedCategory] = useState<string | null>(null)
const [sortBy, setSortBy] = useState<"newest" | "stars" | "name" | "downloadCount">("newest")
const [sortBy, setSortBy] = useState<"newest" | "stars" | "name" | "downloadCount">("downloadCount")
const items = mcpMarketplaceCatalog?.items || []
@@ -48,8 +48,7 @@ import {
xaiModels,
} from "@shared/api"
import { EmptyRequest, StringRequest } from "@shared/proto/common"
import { OpenAiModelsRequest, UpdateApiConfigurationRequest } from "@shared/proto/models"
import { convertApiConfigurationToProto } from "@shared/proto-conversions/models/api-configuration-conversion"
import { OpenAiModelsRequest } from "@shared/proto/models"
import {
VSCodeButton,
VSCodeCheckbox,
@@ -119,7 +118,14 @@ const OpenRouterBalanceDisplay = ({ apiKey }: { apiKey: string }) => {
const SUPPORTED_THINKING_MODELS: Record<string, string[]> = {
anthropic: ["claude-3-7-sonnet-20250219", "claude-sonnet-4-20250514", "claude-opus-4-20250514"],
vertex: ["claude-3-7-sonnet@20250219", "claude-sonnet-4@20250514", "claude-opus-4@20250514"],
vertex: [
"claude-3-7-sonnet@20250219",
"claude-sonnet-4@20250514",
"claude-opus-4@20250514",
"gemini-2.5-flash-preview-05-20",
"gemini-2.5-flash-preview-04-17",
"gemini-2.5-pro-preview-06-05",
],
qwen: [
"qwen3-235b-a22b",
"qwen3-32b",
@@ -132,6 +138,7 @@ const SUPPORTED_THINKING_MODELS: Record<string, string[]> = {
"qwen-plus-latest",
"qwen-turbo-latest",
],
gemini: ["gemini-2.5-flash-preview-05-20", "gemini-2.5-flash-preview-04-17", "gemini-2.5-pro-preview-06-05"],
}
// This is necessary to ensure dropdown opens downward, important for when this is used in popup
@@ -193,19 +200,12 @@ const ApiOptions = ({
if (saveImmediately && field === "apiProvider") {
// Use apiConfiguration from the full extensionState context to send the most complete data
const currentFullApiConfig = extensionState.apiConfiguration
// Convert to proto format and send via gRPC
const updatedConfig = {
...currentFullApiConfig,
apiProvider: newValue,
}
const protoConfig = convertApiConfigurationToProto(updatedConfig)
ModelsServiceClient.updateApiConfigurationProto(
UpdateApiConfigurationRequest.create({
apiConfiguration: protoConfig,
}),
).catch((error) => {
console.error("Failed to update API configuration:", error)
vscode.postMessage({
type: "apiConfiguration",
apiConfiguration: {
...currentFullApiConfig, // Send the most complete config available
apiProvider: newValue, // Override with the new provider
},
})
}
}
@@ -1057,15 +1057,6 @@ const ApiOptions = ({
</VSCodeLink>
)}
</p>
{/* Add Thinking Budget Slider specifically for gemini-2.5-flash-preview-04-17 */}
{selectedProvider === "gemini" && selectedModelId === "gemini-2.5-flash-preview-04-17" && (
<ThinkingBudgetSlider
apiConfiguration={apiConfiguration}
setApiConfiguration={setApiConfiguration}
maxBudget={selectedModelInfo.thinkingConfig?.maxBudget}
/>
)}
</div>
)}
@@ -115,6 +115,8 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
const {
apiConfiguration,
version,
customInstructions,
setCustomInstructions,
openRouterModels,
telemetrySetting,
setTelemetrySetting,
@@ -138,6 +140,7 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
// Store the original state to detect changes
const originalState = useRef({
apiConfiguration,
customInstructions,
telemetrySetting,
planActSeparateModelsSetting,
enableCheckpointsSetting,
@@ -160,6 +163,10 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
if (!apiValidationResult && !modelIdValidationResult) {
// vscode.postMessage({ type: "apiConfiguration", apiConfiguration })
// vscode.postMessage({
// type: "customInstructions",
// text: customInstructions,
// })
// vscode.postMessage({
// type: "telemetrySetting",
// text: telemetrySetting,
// })
@@ -177,6 +184,7 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
await StateServiceClient.updateSettings(
UpdateSettingsRequest.create({
planActSeparateModelsSetting,
customInstructionsSetting: customInstructions,
telemetrySetting,
enableCheckpointsSetting,
mcpMarketplaceEnabled,
@@ -207,6 +215,7 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
useEffect(() => {
const hasChanges =
JSON.stringify(apiConfiguration) !== JSON.stringify(originalState.current.apiConfiguration) ||
customInstructions !== originalState.current.customInstructions ||
telemetrySetting !== originalState.current.telemetrySetting ||
planActSeparateModelsSetting !== originalState.current.planActSeparateModelsSetting ||
enableCheckpointsSetting !== originalState.current.enableCheckpointsSetting ||
@@ -219,6 +228,7 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
setHasUnsavedChanges(hasChanges)
}, [
apiConfiguration,
customInstructions,
telemetrySetting,
planActSeparateModelsSetting,
enableCheckpointsSetting,
@@ -236,6 +246,7 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
setIsUnsavedChangesDialogOpen(true)
pendingAction.current = () => {
// Reset all tracked state to original values
setCustomInstructions(originalState.current.customInstructions)
setTelemetrySetting(originalState.current.telemetrySetting)
setPlanActSeparateModelsSetting(originalState.current.planActSeparateModelsSetting)
setChatSettings(originalState.current.chatSettings)
@@ -276,6 +287,7 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
}, [
hasUnsavedChanges,
onDone,
setCustomInstructions,
setTelemetrySetting,
setPlanActSeparateModelsSetting,
setChatSettings,
@@ -573,6 +585,24 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
architect a plan for a cheaper coding model to act on.
</p>
</div>
<div className="mb-[5px]">
<VSCodeTextArea
value={customInstructions ?? ""}
className="w-full"
resize="vertical"
rows={4}
placeholder={
'e.g. "Run unit tests at the end", "Use TypeScript with async/await", "Speak in Spanish"'
}
onInput={(e: any) => setCustomInstructions(e.target?.value ?? "")}>
<span className="font-medium">Custom Instructions</span>
</VSCodeTextArea>
<p className="text-xs mt-[5px] text-[var(--vscode-descriptionForeground)]">
These instructions are added to the end of the system prompt sent with every
request.
</p>
</div>
</Section>
</div>
)}
@@ -1,6 +1,8 @@
import React, { useState } from "react"
import { VSCodeTextField, VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { StateServiceClient } from "@/services/grpc-client"
import { Int64, Int64Request } from "@shared/proto/common"
export const TerminalSettingsSection: React.FC = () => {
const { shellIntegrationTimeout, setShellIntegrationTimeout, terminalReuseEnabled, setTerminalReuseEnabled } =
@@ -25,6 +27,18 @@ export const TerminalSettingsSection: React.FC = () => {
// Update local state
setShellIntegrationTimeout(timeout)
// Send to extension using gRPC
StateServiceClient.updateTerminalConnectionTimeout({
value: timeout,
} as Int64Request)
.then((response: Int64) => {
setShellIntegrationTimeout(response.value)
setInputValue((response.value / 1000).toString())
})
.catch((error) => {
console.error("Failed to update terminal connection timeout:", error)
})
}
const handleInputBlur = () => {
@@ -5,10 +5,8 @@ import { validateApiConfiguration } from "@/utils/validate"
import { vscode } from "@/utils/vscode"
import ApiOptions from "@/components/settings/ApiOptions"
import ClineLogoWhite from "@/assets/ClineLogoWhite"
import { AccountServiceClient, ModelsServiceClient } from "@/services/grpc-client"
import { AccountServiceClient } from "@/services/grpc-client"
import { EmptyRequest } from "@shared/proto/common"
import { UpdateApiConfigurationRequest } from "@shared/proto/models"
import { convertApiConfigurationToProto } from "@shared/proto-conversions/models/api-configuration-conversion"
const WelcomeView = memo(() => {
const { apiConfiguration } = useExtensionState()
@@ -23,18 +21,8 @@ const WelcomeView = memo(() => {
)
}
const handleSubmit = async () => {
if (apiConfiguration) {
try {
await ModelsServiceClient.updateApiConfigurationProto(
UpdateApiConfigurationRequest.create({
apiConfiguration: convertApiConfigurationToProto(apiConfiguration),
}),
)
} catch (error) {
console.error("Failed to update API configuration:", error)
}
}
const handleSubmit = () => {
vscode.postMessage({ type: "apiConfiguration", apiConfiguration })
}
useEffect(() => {
@@ -7,11 +7,10 @@ import {
FileServiceClient,
McpServiceClient,
} from "../services/grpc-client"
import { EmptyRequest, StringRequest } from "@shared/proto/common"
import { EmptyRequest } from "@shared/proto/common"
import { UpdateSettingsRequest } from "@shared/proto/state"
import { WebviewProviderType as WebviewProviderTypeEnum, WebviewProviderTypeRequest } from "@shared/proto/ui"
import { convertProtoToClineMessage } from "@shared/proto-conversions/cline-message"
import { convertProtoMcpServersToMcpServers } from "@shared/proto-conversions/mcp/mcp-server-conversion"
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "@shared/AutoApprovalSettings"
import { DEFAULT_BROWSER_SETTINGS } from "@shared/BrowserSettings"
import { ChatSettings, DEFAULT_CHAT_SETTINGS } from "@shared/ChatSettings"
@@ -27,7 +26,9 @@ import {
requestyDefaultModelInfo,
} from "../../../src/shared/api"
import { McpMarketplaceCatalog, McpServer, McpViewTab } from "../../../src/shared/mcp"
import { convertProtoMcpServersToMcpServers } from "@shared/proto-conversions/mcp/mcp-server-conversion"
import { convertTextMateToHljs } from "../utils/textMateToHljs"
import { vscode } from "../utils/vscode"
import { OpenRouterCompatibleModelInfo } from "@shared/proto/models"
interface ExtensionStateContextType extends ExtensionState {
@@ -52,6 +53,7 @@ interface ExtensionStateContextType extends ExtensionState {
// Setters
setApiConfiguration: (config: ApiConfiguration) => void
setCustomInstructions: (value?: string) => void
setTelemetrySetting: (value: TelemetrySetting) => void
setShowAnnouncement: (value: boolean) => void
setShouldShowAnnouncement: (value: boolean) => void
@@ -229,9 +231,6 @@ export const ExtensionStateContextProvider: React.FC<{
// References to store subscription cancellation functions
const stateSubscriptionRef = useRef<(() => void) | null>(null)
// Reference for focusChatInput subscription
const focusChatInputUnsubscribeRef = useRef<(() => void) | null>(null)
const mcpButtonUnsubscribeRef = useRef<(() => void) | null>(null)
const historyButtonClickedSubscriptionRef = useRef<(() => void) | null>(null)
const chatButtonUnsubscribeRef = useRef<(() => void) | null>(null)
@@ -553,24 +552,6 @@ export const ExtensionStateContextProvider: React.FC<{
onComplete: () => {},
})
// Subscribe to focus chat input events
const clientId = (window as any).clineClientId
if (clientId) {
const request = StringRequest.create({ value: clientId })
focusChatInputUnsubscribeRef.current = UiServiceClient.subscribeToFocusChatInput(request, {
onResponse: () => {
// Dispatch a local DOM event within this webview only
window.dispatchEvent(new CustomEvent("focusChatInput"))
},
onError: (error: Error) => {
console.error("Error in focusChatInput subscription:", error)
},
onComplete: () => {},
})
} else {
console.error("Client ID not found in window object")
}
// Clean up subscriptions when component unmounts
return () => {
if (stateSubscriptionRef.current) {
@@ -621,10 +602,7 @@ export const ExtensionStateContextProvider: React.FC<{
relinquishControlUnsubscribeRef.current()
relinquishControlUnsubscribeRef.current = null
}
if (focusChatInputUnsubscribeRef.current) {
focusChatInputUnsubscribeRef.current()
focusChatInputUnsubscribeRef.current = null
}
if (mcpServersSubscriptionRef.current) {
mcpServersSubscriptionRef.current()
mcpServersSubscriptionRef.current = null
@@ -687,6 +665,11 @@ export const ExtensionStateContextProvider: React.FC<{
...prevState,
apiConfiguration: value,
})),
setCustomInstructions: (value) =>
setState((prevState) => ({
...prevState,
customInstructions: value,
})),
setTelemetrySetting: (value) =>
setState((prevState) => ({
...prevState,
@@ -754,6 +737,7 @@ export const ExtensionStateContextProvider: React.FC<{
apiConfiguration: state.apiConfiguration
? convertApiConfigurationToProtoApiConfiguration(state.apiConfiguration)
: undefined,
customInstructionsSetting: state.customInstructions,
telemetrySetting: state.telemetrySetting,
planActSeparateModelsSetting: state.planActSeparateModelsSetting,
enableCheckpointsSetting: state.enableCheckpointsSetting,