Compare commits

..

2 Commits

Author SHA1 Message Date
Elephant Lumps bc686deb1f merge conflicts 2025-06-03 23:53:18 -07:00
Elephant Lumps c13b1a9c4c migrate accountButtonClicked 2025-06-03 12:37:30 -07:00
63 changed files with 374 additions and 2814 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Ollama: Use a filterable dropdown instead of radio selection
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Prioritize active files in file context menu
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
adding support for streamable mcp server
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Migrate chatButtonClicked to Protobus
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Telemetry fix
+1 -1
View File
@@ -20,7 +20,7 @@
"no-throw-literal": "warn",
"semi": "off",
"react-hooks/exhaustive-deps": "off",
"eslint-rules/no-protobuf-object-literals": "error",
"eslint-rules/no-protobuf-object-literals": "warn",
"eslint-rules/no-grpc-client-object-literals": "error"
},
"ignorePatterns": ["out", "dist", "**/*.d.ts"]
+1 -10
View File
@@ -22,18 +22,9 @@ coverage
*evals.env
# Generated proto files
src/shared/proto/*.ts
src/core/controller/*/methods.ts
src/core/controller/*/index.ts
src/core/controller/grpc-service-config.ts
# Shared
src/shared/proto/*.ts
src/shared/proto/host/*.ts
# Webview
webview-ui/src/services/grpc-client.ts
# Standalone
src/standalone/server-setup.ts
src/standalone/services/host-grpc-client.ts
# Host bridge
hosts/vscode/*/methods.ts
hosts/vscode/*/index.ts
hosts/vscode/host-grpc-service-config.ts
-14
View File
@@ -1,19 +1,5 @@
# Changelog
## [3.17.11]
- Add support for Gemini 2.5 Pro Preview 06-05 model to Vertex AI and Google Gemini providers
## [3.17.10]
- Add support for Qwen 3 series models with thinking mode options (Thanks @Jonny-china!)
- Add new AskSage models: Claude 4 Sonnet, Claude 4 Opus, GPT 4.1, Gemini 2.5 Pro (Thanks @swhite24!)
- Add VSCode walkthrough to help new users get started with Cline
- Add support for streamable MCP servers
- Improve Ollama model selection with filterable dropdown instead of radio buttons (Thanks @paulgear!)
- Add setting to disable aggressive terminal reuse to help users experiencing task lockout issues
- Fix settings dialog applying changes even when cancel button is clicked
## [3.17.9]
- Aligning Cline to work with Claude 4 model family (Experimental)
-197
View File
@@ -1,197 +0,0 @@
import { v4 as uuidv4 } from "uuid"
import { hostServiceHandlers } from "./host-grpc-service-config"
import { GrpcRequestRegistry } from "../../src/core/controller/grpc-request-registry"
/**
* Type definition for a streaming response handler
*/
export type StreamingResponseHandler = (response: any, isLast?: boolean, sequenceNumber?: number) => Promise<void>
// Registry to track active gRPC requests and their cleanup functions
const requestRegistry = new GrpcRequestRegistry()
/**
* Callback interface for streaming requests
*/
export interface StreamingCallbacks<T = any> {
onResponse: (response: T) => void
onError?: (error: Error) => void
onComplete?: () => void
}
/**
* Handles gRPC requests from the webview
*/
export class GrpcHandler {
constructor() {}
/**
* Handle a gRPC request from the webview
* @param service The service name
* @param method The method name
* @param message The request message
* @param requestId The request ID for response correlation
* @param streamingCallbacks Optional callbacks for streaming responses
* @returns For unary requests: the response message or error. For streaming requests: a cancel function.
*/
async handleRequest<T = any>(
service: string,
method: string,
message: any,
requestId: string,
streamingCallbacks?: StreamingCallbacks<T>,
): Promise<
| {
message?: any
error?: string
request_id: string
}
| (() => void)
> {
// If streaming callbacks are provided, handle as a streaming request
if (streamingCallbacks) {
let completionCalled = false
// Create a response handler that will call the client's callbacks
const responseHandler: StreamingResponseHandler = async (response, isLast = false, sequenceNumber) => {
try {
// Call the client's onResponse callback with the response
streamingCallbacks.onResponse(response)
// If this is the last response, call the onComplete callback
if (isLast && streamingCallbacks.onComplete && !completionCalled) {
completionCalled = true
streamingCallbacks.onComplete()
}
} catch (error) {
// If there's an error in the callback, call the onError callback
if (streamingCallbacks.onError) {
streamingCallbacks.onError(error instanceof Error ? error : new Error(String(error)))
}
}
}
// Register the response handler with the registry
requestRegistry.registerRequest(
requestId,
() => {
console.log(`[DEBUG] Cleaning up streaming request: ${requestId}`)
if (streamingCallbacks.onComplete && !completionCalled) {
completionCalled = true
streamingCallbacks.onComplete()
}
},
{ type: "streaming_request", service, method },
responseHandler,
)
// Call the streaming handler directly
console.log(`[DEBUG] Streaming gRPC host call to ${service}.${method} req:${requestId}`)
try {
await this.handleStreamingRequest(service, method, message, requestId)
} catch (error) {
if (streamingCallbacks.onError) {
streamingCallbacks.onError(error instanceof Error ? error : new Error(String(error)))
}
}
// Return a function to cancel the stream
return () => {
console.log(`[DEBUG] Cancelling streaming request: ${requestId}`)
this.cancelRequest(requestId)
}
}
// Handle as a unary request
try {
// Get the service handler from the config
const serviceConfig = hostServiceHandlers[service]
if (!serviceConfig) {
throw new Error(`Unknown service: ${service}`)
}
// Handle unary request
return {
message: await serviceConfig.requestHandler(method, message),
request_id: requestId,
}
} catch (error) {
return {
error: error instanceof Error ? error.message : String(error),
request_id: requestId,
}
}
}
/**
* Cancel a gRPC request
* @param requestId The request ID to cancel
* @returns True if the request was found and cancelled, false otherwise
*/
public async cancelRequest(requestId: string): Promise<boolean> {
const cancelled = requestRegistry.cancelRequest(requestId)
if (cancelled) {
// Get the registered response handler from the registry
const requestInfo = requestRegistry.getRequestInfo(requestId)
if (requestInfo && requestInfo.responseStream) {
try {
// Send cancellation confirmation using the registered response handler
await requestInfo.responseStream(
{ cancelled: true },
true, // Mark as last message
)
} catch (e) {
console.error(`Error sending cancellation response for ${requestId}:`, e)
}
}
} else {
console.log(`[DEBUG] Request not found for cancellation: ${requestId}`)
}
return cancelled
}
/**
* Handle a streaming gRPC request
* @param service The service name
* @param method The method name
* @param message The request message
* @param requestId The request ID for response correlation
*/
private async handleStreamingRequest(service: string, method: string, message: any, requestId: string): Promise<void> {
// Get the service handler from the config
const serviceConfig = hostServiceHandlers[service]
if (!serviceConfig) {
throw new Error(`Unknown service: ${service}`)
}
// Check if the service supports streaming
if (!serviceConfig.streamingHandler) {
throw new Error(`Service ${service} does not support streaming`)
}
// Get the registered response handler from the registry
const requestInfo = requestRegistry.getRequestInfo(requestId)
if (!requestInfo || !requestInfo.responseStream) {
throw new Error(`No response handler registered for request: ${requestId}`)
}
// Use the registered response handler
const responseStream = requestInfo.responseStream
// Handle streaming request and pass the requestId to all streaming handlers
await serviceConfig.streamingHandler(method, message, responseStream, requestId)
// Don't send a final message here - the stream should stay open for future updates
// The stream will be closed when the client disconnects or when the service explicitly ends it
}
}
/**
* Get the request registry instance
* This allows other parts of the code to access the registry
*/
export function getRequestRegistry(): GrpcRequestRegistry {
return requestRegistry
}
-138
View File
@@ -1,138 +0,0 @@
import { StreamingResponseHandler } from "./host-grpc-handler"
/**
* Generic type for service method handlers
*/
export type ServiceMethodHandler = (message: any) => Promise<any>
/**
* Type for streaming method handlers
*/
export type StreamingMethodHandler = (message: any, responseStream: StreamingResponseHandler, requestId?: string) => Promise<void>
/**
* Method metadata including streaming information
*/
export interface MethodMetadata {
isStreaming: boolean
}
/**
* Generic service registry for gRPC services
*/
export class ServiceRegistry {
private serviceName: string
private methodRegistry: Record<string, ServiceMethodHandler> = {}
private streamingMethodRegistry: Record<string, StreamingMethodHandler> = {}
private methodMetadata: Record<string, MethodMetadata> = {}
/**
* Create a new service registry
* @param serviceName The name of the service (used for logging)
*/
constructor(serviceName: string) {
this.serviceName = serviceName
}
/**
* Register a method handler
* @param methodName The name of the method to register
* @param handler The handler function for the method
* @param metadata Optional metadata about the method
*/
registerMethod(methodName: string, handler: ServiceMethodHandler | StreamingMethodHandler, metadata?: MethodMetadata): void {
const isStreaming = metadata?.isStreaming || false
if (isStreaming) {
this.streamingMethodRegistry[methodName] = handler as StreamingMethodHandler
} else {
this.methodRegistry[methodName] = handler as ServiceMethodHandler
}
this.methodMetadata[methodName] = { isStreaming, ...metadata }
console.log(`Registered ${this.serviceName} method: ${methodName}${isStreaming ? " (streaming)" : ""}`)
}
/**
* Check if a method is a streaming method
* @param method The method name
* @returns True if the method is a streaming method
*/
isStreamingMethod(method: string): boolean {
return this.methodMetadata[method]?.isStreaming || false
}
/**
* Get a streaming method handler
* @param method The method name
* @returns The streaming method handler or undefined if not found
*/
getStreamingHandler(method: string): StreamingMethodHandler | undefined {
return this.streamingMethodRegistry[method]
}
/**
* Handle a service request
* @param method The method name
* @param message The request message
* @returns The response message
*/
async handleRequest(method: string, message: any): Promise<any> {
const handler = this.methodRegistry[method]
if (!handler) {
if (this.isStreamingMethod(method)) {
throw new Error(`Method ${method} is a streaming method and should be handled with handleStreamingRequest`)
}
throw new Error(`Unknown ${this.serviceName} method: ${method}`)
}
return handler(message)
}
/**
* Handle a streaming service request
* @param method The method name
* @param message The request message
* @param responseStream The streaming response handler
* @param requestId The request ID for correlation and cleanup
*/
async handleStreamingRequest(
method: string,
message: any,
responseStream: StreamingResponseHandler,
requestId?: string,
): Promise<void> {
const handler = this.streamingMethodRegistry[method]
if (!handler) {
if (this.methodRegistry[method]) {
throw new Error(`Method ${method} is not a streaming method and should be handled with handleRequest`)
}
throw new Error(`Unknown ${this.serviceName} streaming method: ${method}`)
}
await handler(message, responseStream, requestId)
}
}
/**
* Create a service registry factory function
* @param serviceName The name of the service
* @returns An object with register and handle functions
*/
export function createServiceRegistry(serviceName: string) {
const registry = new ServiceRegistry(serviceName)
return {
registerMethod: (methodName: string, handler: ServiceMethodHandler | StreamingMethodHandler, metadata?: MethodMetadata) =>
registry.registerMethod(methodName, handler, metadata),
handleRequest: (method: string, message: any) => registry.handleRequest(method, message),
handleStreamingRequest: (method: string, message: any, responseStream: StreamingResponseHandler, requestId?: string) =>
registry.handleStreamingRequest(method, message, responseStream, requestId),
isStreamingMethod: (method: string) => registry.isStreamingMethod(method),
}
}
-20
View File
@@ -1,20 +0,0 @@
import * as vscode from "vscode"
import { Uri } from "../../../src/shared/proto/host/uri"
import { StringRequest } from "../../../src/shared/proto/common"
/**
* Creates a file URI from a file path
* @param request The request containing the file path
* @returns A URI object representing the file
*/
export async function file(request: StringRequest): Promise<Uri> {
const uri = vscode.Uri.file(request.value)
return Uri.create({
scheme: uri.scheme,
authority: uri.authority,
path: uri.path,
query: uri.query,
fragment: uri.fragment,
fsPath: uri.fsPath,
})
}
-28
View File
@@ -1,28 +0,0 @@
import * as vscode from "vscode"
import { JoinPathRequest, Uri } from "../../../src/shared/proto/host/uri"
/**
* Joins a URI with additional path segments
* @param request The request containing the base URI and path segments
* @returns A new URI with the path segments joined
*/
export async function joinPath(request: JoinPathRequest): Promise<Uri> {
// Convert proto Uri to vscode.Uri
if (!request.base) {
throw new Error("Base URI is required")
}
const baseUri = vscode.Uri.parse(`${request.base.scheme}://${request.base.authority}${request.base.path}`)
// Join paths
const result = vscode.Uri.joinPath(baseUri, ...request.pathSegments)
// Convert back to proto Uri
return Uri.create({
scheme: result.scheme,
authority: result.authority,
path: result.path,
query: result.query,
fragment: result.fragment,
fsPath: result.fsPath,
})
}
-20
View File
@@ -1,20 +0,0 @@
import * as vscode from "vscode"
import { Uri } from "../../../src/shared/proto/host/uri"
import { StringRequest } from "../../../src/shared/proto/common"
/**
* Parses a string URI into a Uri object
* @param request The request containing the URI string
* @returns A URI object representing the parsed URI
*/
export async function parse(request: StringRequest): Promise<Uri> {
const uri = vscode.Uri.parse(request.value)
return Uri.create({
scheme: uri.scheme,
authority: uri.authority,
path: uri.path,
query: uri.query,
fragment: uri.fragment,
fsPath: uri.fsPath,
})
}
-225
View File
@@ -1,225 +0,0 @@
import * as fs from "fs/promises"
import * as fsSync from "fs"
import { SubscribeToFileRequest, FileChangeEvent, FileChangeEvent_ChangeType } from "../../../src/shared/proto/host/watch"
import { StreamingResponseHandler, getRequestRegistry } from "../host-grpc-handler"
// Debounce configuration
const DEBOUNCE_DELAY = 100 // ms
// Keep track of active file watchers
const fileWatchers = new Map<
string,
{
watcher: fsSync.FSWatcher
subscribers: Set<StreamingResponseHandler>
lastEventTime: Map<FileChangeEvent_ChangeType, number> // Track last event time by event type
}
>()
/**
* Subscribe to file changes
* @param request The request containing the file path
* @param responseStream The streaming response handler
* @param requestId The ID of the request (passed by the gRPC handler)
*/
export async function subscribeToFile(
request: SubscribeToFileRequest,
responseStream: StreamingResponseHandler,
requestId?: string,
): Promise<void> {
const filePath = request.path
console.log(`[DEBUG] Setting up file subscription for ${filePath}`)
try {
// We don't send an initial event to avoid triggering handlers immediately
console.log(`[DEBUG] Now watching file: ${filePath}`)
// Set up or reuse file watcher
if (!fileWatchers.has(filePath)) {
// Create a new watcher for this file using Node.js fs.watch API
// This is more reliable than the VSCode FileSystemWatcher for detecting file saves
const watcher = fsSync.watch(filePath, { persistent: true }, async (eventType, filename) => {
if (eventType === "change") {
try {
const content = await fs.readFile(filePath, "utf8")
console.log(`[DEBUG] File changed: ${filePath}`)
// Get the watcher info
const watcherInfo = fileWatchers.get(filePath)
if (watcherInfo) {
// Check if this event should be debounced
const eventType = FileChangeEvent_ChangeType.CHANGED
const now = Date.now()
const lastTime = watcherInfo.lastEventTime.get(eventType) || 0
if (now - lastTime < DEBOUNCE_DELAY) {
console.log(
`[DEBUG] Debouncing change event for ${filePath} (${now - lastTime}ms since last event)`,
)
return // Skip this event due to debounce
}
// Update the last event time
watcherInfo.lastEventTime.set(eventType, now)
// Notify all subscribers
for (const subscriber of watcherInfo.subscribers) {
try {
await subscriber({
path: filePath,
type: eventType,
content,
})
} catch (error) {
console.error(`Error sending file change event: ${error}`)
watcherInfo.subscribers.delete(subscriber)
}
}
}
} catch (error) {
console.error(`Error reading changed file: ${error}`)
}
} else if (eventType === "rename") {
// In Node.js fs.watch, 'rename' can mean either creation or deletion
// We need to check if the file exists to determine which it is
try {
await fs.access(filePath)
// File exists, so it was created or renamed
const content = await fs.readFile(filePath, "utf8")
console.log(`[DEBUG] File created/renamed: ${filePath}`)
// Get the watcher info
const watcherInfo = fileWatchers.get(filePath)
if (watcherInfo) {
// Check if this event should be debounced
const eventType = FileChangeEvent_ChangeType.CREATED
const now = Date.now()
const lastTime = watcherInfo.lastEventTime.get(eventType) || 0
if (now - lastTime < DEBOUNCE_DELAY) {
console.log(
`[DEBUG] Debouncing creation event for ${filePath} (${now - lastTime}ms since last event)`,
)
return // Skip this event due to debounce
}
// Update the last event time
watcherInfo.lastEventTime.set(eventType, now)
// Notify all subscribers
for (const subscriber of watcherInfo.subscribers) {
try {
await subscriber({
path: filePath,
type: eventType,
content,
})
} catch (error) {
console.error(`Error sending file creation event: ${error}`)
watcherInfo.subscribers.delete(subscriber)
}
}
}
} catch (error) {
// File doesn't exist, so it was deleted
console.log(`[DEBUG] File deleted: ${filePath}`)
// Get the watcher info
const watcherInfo = fileWatchers.get(filePath)
if (watcherInfo) {
// Check if this event should be debounced
const eventType = FileChangeEvent_ChangeType.DELETED
const now = Date.now()
const lastTime = watcherInfo.lastEventTime.get(eventType) || 0
if (now - lastTime < DEBOUNCE_DELAY) {
console.log(
`[DEBUG] Debouncing deletion event for ${filePath} (${now - lastTime}ms since last event)`,
)
return // Skip this event due to debounce
}
// Update the last event time
watcherInfo.lastEventTime.set(eventType, now)
// Notify all subscribers
for (const subscriber of watcherInfo.subscribers) {
try {
await subscriber({
path: filePath,
type: eventType,
content: "",
})
} catch (error) {
console.error(`Error sending file deletion event: ${error}`)
watcherInfo.subscribers.delete(subscriber)
}
}
// Clean up the watcher
cleanupWatcher(filePath)
}
}
}
})
// Set up the watcher info
const watcherInfo = {
watcher,
subscribers: new Set<StreamingResponseHandler>(),
lastEventTime: new Map<FileChangeEvent_ChangeType, number>(),
}
fileWatchers.set(filePath, watcherInfo)
}
// Add this subscriber to the watcher
const watcherInfo = fileWatchers.get(filePath)!
watcherInfo.subscribers.add(responseStream)
// Register cleanup when the connection is closed
const cleanup = () => {
console.log(`[DEBUG] Cleaning up file subscription for ${filePath}`)
const watcherInfo = fileWatchers.get(filePath)
if (watcherInfo) {
watcherInfo.subscribers.delete(responseStream)
// If no subscribers left, clean up the watcher
if (watcherInfo.subscribers.size === 0) {
cleanupWatcher(filePath)
}
}
}
// Register the cleanup function with the request registry
if (requestId) {
getRequestRegistry().registerRequest(
requestId,
cleanup,
{ type: "file_subscription", path: filePath },
responseStream,
)
}
} catch (error) {
console.error(`Error setting up file subscription: ${error}`)
// Send an error response
await responseStream({
path: filePath,
type: FileChangeEvent_ChangeType.DELETED,
content: `Error: ${error instanceof Error ? error.message : String(error)}`,
})
}
}
/**
* Clean up a file watcher
* @param filePath The path of the file to clean up
*/
function cleanupWatcher(filePath: string): void {
const watcherInfo = fileWatchers.get(filePath)
if (watcherInfo) {
watcherInfo.watcher.close()
fileWatchers.delete(filePath)
console.log(`[DEBUG] Removed file watcher for ${filePath}`)
}
}
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "claude-dev",
"version": "3.17.11",
"version": "3.17.9",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.17.11",
"version": "3.17.9",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.12.4",
+1 -58
View File
@@ -2,7 +2,7 @@
"name": "claude-dev",
"displayName": "Cline",
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
"version": "3.17.11",
"version": "3.17.9",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -46,58 +46,6 @@
],
"main": "./dist/extension.js",
"contributes": {
"walkthroughs": [
{
"id": "ClineWalkthrough",
"title": "Meet Cline, your new coding partner",
"description": "Cline codes like a developer because it thinks like one. Here are 5 ways to put it to work:",
"steps": [
{
"id": "welcome",
"title": "Start with a Goal, Not Just a Prompt",
"description": "Tell Cline what you want to achieve. It plans, asks, and then codes, like a true partner.",
"media": {
"markdown": "walkthrough/step1.md"
}
},
{
"id": "learn",
"title": "Let Cline Learn Your Codebase",
"description": "Point Cline to your project. It builds understanding to make smart, context-aware changes.",
"media": {
"markdown": "walkthrough/step2.md"
}
},
{
"id": "advanced-features",
"title": "Always Use the Best AI Models",
"description": "Cline empowers you with State-of-the-Art AI, connecting to top models (Anthropic, Gemini, OpenAI & more).",
"media": {
"markdown": "walkthrough/step3.md"
}
},
{
"id": "mcp",
"title": "Extend with Powerful Tools (MCP)",
"description": "Connect to databases, APIs, or discover new capabilities in the MCP Marketplace.",
"media": {
"markdown": "walkthrough/step4.md"
}
},
{
"id": "getting-started",
"title": "You're Always in Control",
"description": "Review Cline's plans and diffs. Approve changes before they happen. No surprises.",
"media": {
"markdown": "walkthrough/step5.md"
},
"content": {
"path": "walkthrough/step5.md"
}
}
]
}
],
"viewsContainers": {
"activitybar": [
{
@@ -195,11 +143,6 @@
"command": "cline.improveCode",
"title": "Improve with Cline",
"category": "Cline"
},
{
"command": "cline.openWalkthrough",
"title": "Open Walkthrough",
"category": "Cline"
}
],
"keybindings": [
+36 -253
View File
@@ -12,6 +12,38 @@ import { createRequire } from "module"
const require = createRequire(import.meta.url)
const protoc = path.join(require.resolve("grpc-tools"), "../bin/protoc")
// Check for Apple Silicon compatibility
function checkAppleSiliconCompatibility() {
// Only run check on macOS
if (process.platform !== "darwin") {
return
}
// Check if running on Apple Silicon
const cpuArchitecture = os.arch()
if (cpuArchitecture === "arm64") {
try {
// Check if Rosetta is installed
const rosettaCheck = execSync('/usr/bin/pgrep oahd || echo "NOT_INSTALLED"').toString().trim()
if (rosettaCheck === "NOT_INSTALLED") {
console.log(chalk.yellow("Detected Apple Silicon (ARM64) architecture."))
console.log(
chalk.red("Rosetta 2 is NOT installed. The npm version of protoc is not compatible with Apple Silicon."),
)
console.log(chalk.cyan("Please install Rosetta 2 using the following command:"))
console.log(chalk.cyan(" softwareupdate --install-rosetta --agree-to-license"))
console.log(chalk.red("Aborting build process."))
process.exit(1)
} else {
console.log(chalk.green("Rosetta 2 is installed. Continuing with build."))
}
} catch (error) {
console.log(chalk.yellow("Could not determine Rosetta installation status. Proceeding anyway."))
}
}
}
const __filename = fileURLToPath(import.meta.url)
const SCRIPT_DIR = path.dirname(__filename)
const ROOT_DIR = path.resolve(SCRIPT_DIR, "..")
@@ -40,15 +72,6 @@ const serviceNameMap = {
}
const serviceDirs = Object.keys(serviceNameMap).map((serviceKey) => path.join(ROOT_DIR, "src", "core", "controller", serviceKey))
// List of host gRPC services (IDE API bridge)
// These services are implemented in the IDE extension and called by the standalone Cline Core
const hostServiceNameMap = {
uri: "host.UriService",
watch: "host.WatchService",
// Add new host services here
}
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..."))
@@ -58,7 +81,7 @@ async function main() {
// Define output directories
const TS_OUT_DIR = path.join(ROOT_DIR, "src", "shared", "proto")
// Create output directories if they don't exist
// Create output directory if it doesn't exist
await fs.mkdir(TS_OUT_DIR, { recursive: true })
// Clean up existing generated files
@@ -73,7 +96,7 @@ async function main() {
// Process all proto files
console.log(chalk.cyan("Processing proto files from"), SCRIPT_DIR)
const protoFiles = await globby("**/*.proto", { cwd: SCRIPT_DIR, realpath: true })
const protoFiles = await globby("*.proto", { cwd: SCRIPT_DIR, realpath: true })
// Build the protoc command with proper path handling for cross-platform
const tsProtocCommand = [
@@ -81,8 +104,6 @@ async function main() {
`--proto_path="${SCRIPT_DIR}"`,
`--plugin=protoc-gen-ts_proto="${tsProtoPlugin}"`,
`--ts_proto_out="${TS_OUT_DIR}"`,
"--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(" ")
@@ -96,6 +117,7 @@ async function main() {
const descriptorOutDir = path.join(ROOT_DIR, "dist-standalone", "proto")
await fs.mkdir(descriptorOutDir, { recursive: true })
const descriptorFile = path.join(descriptorOutDir, "descriptor_set.pb")
const descriptorProtocCommand = [
protoc,
@@ -116,11 +138,8 @@ async function main() {
console.log(chalk.green(`TypeScript files generated in: ${TS_OUT_DIR}`))
await generateMethodRegistrations()
await generateHostMethodRegistrations()
await generateServiceConfig()
await generateHostServiceConfig()
await generateGrpcClientConfig()
await generateHostGrpcClientConfig()
}
/**
@@ -263,7 +282,7 @@ async function generateMethodRegistrations() {
// Import all method implementations
import { registerMethod } from "./index"\n`
// Import implementations directly
// Add imports for all implementation files
for (const file of implementationFiles) {
const baseName = path.basename(file, ".ts")
methodsContent += `import { ${baseName} } from "./${baseName}"\n`
@@ -437,242 +456,6 @@ service ${serviceClassName} {
}
}
/**
* Generate method registration files for host services
*/
async function generateHostMethodRegistrations() {
console.log(chalk.cyan("Generating host method registration files..."))
// Parse proto files for streaming methods
const hostProtoFiles = await globby("*.proto", { cwd: path.join(SCRIPT_DIR, "host") })
const streamingMethodsMap = await parseProtoForStreamingMethods(hostProtoFiles, path.join(SCRIPT_DIR, "host"))
for (const serviceDir of hostServiceDirs) {
try {
await fs.access(serviceDir)
} catch (error) {
console.log(chalk.cyan(`Creating directory ${serviceDir} for new host service`))
await fs.mkdir(serviceDir, { recursive: true })
}
const serviceName = path.basename(serviceDir)
const registryFile = path.join(serviceDir, "methods.ts")
const indexFile = path.join(serviceDir, "index.ts")
const fullServiceName = hostServiceNameMap[serviceName]
const streamingMethods = streamingMethodsMap.get(fullServiceName) || []
console.log(chalk.cyan(`Generating method registrations for host ${serviceName}...`))
// Get all TypeScript files in the service directory
const files = await globby("*.ts", { cwd: serviceDir })
// Filter out index.ts and methods.ts
const implementationFiles = files.filter((file) => file !== "index.ts" && file !== "methods.ts")
// Create the methods.ts file with header
let methodsContent = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Import all method implementations
import { registerMethod } from "./index"\n`
// Import implementations directly
for (const file of implementationFiles) {
const baseName = path.basename(file, ".ts")
methodsContent += `import { ${baseName} } from "./${baseName}"\n`
}
// Add streaming methods information
if (streamingMethods.length > 0) {
methodsContent += `\n// Streaming methods for this service
export const streamingMethods = ${JSON.stringify(
streamingMethods.map((m) => m.name),
null,
2,
)}\n`
}
// Add registration function
methodsContent += `\n// Register all ${serviceName} service methods
export function registerAllMethods(): void {
\t// Register each method with the registry\n`
// Add registration statements
for (const file of implementationFiles) {
const baseName = path.basename(file, ".ts")
const isStreaming = streamingMethods.some((m) => m.name === baseName)
if (isStreaming) {
methodsContent += `\tregisterMethod("${baseName}", ${baseName}, { isStreaming: true })\n`
} else {
methodsContent += `\tregisterMethod("${baseName}", ${baseName})\n`
}
}
// Close the function
methodsContent += `}`
// Write the methods.ts file
await fs.writeFile(registryFile, methodsContent)
console.log(chalk.green(`Generated ${registryFile}`))
// Generate index.ts file
const capitalizedServiceName = serviceName.charAt(0).toUpperCase() + serviceName.slice(1)
const indexContent = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../host-grpc-service"
import { StreamingResponseHandler } from "../host-grpc-handler"
import { registerAllMethods } from "./methods"
// Create ${serviceName} service registry
const ${serviceName}Service = createServiceRegistry("${serviceName}")
// Export the method handler types and registration function
export type ${capitalizedServiceName}MethodHandler = ServiceMethodHandler
export type ${capitalizedServiceName}StreamingMethodHandler = StreamingMethodHandler
export const registerMethod = ${serviceName}Service.registerMethod
// Export the request handlers
export const handle${capitalizedServiceName}ServiceRequest = ${serviceName}Service.handleRequest
export const handle${capitalizedServiceName}ServiceStreamingRequest = ${serviceName}Service.handleStreamingRequest
export const isStreamingMethod = ${serviceName}Service.isStreamingMethod
// Register all ${serviceName} methods
registerAllMethods()`
// Write the index.ts file
await fs.writeFile(indexFile, indexContent)
console.log(chalk.green(`Generated ${indexFile}`))
}
console.log(chalk.green("Host method registration files generated successfully."))
}
/**
* Generate a service configuration file for host services
*/
async function generateHostServiceConfig() {
console.log(chalk.cyan("Generating host service configuration file..."))
const serviceImports = []
const serviceConfigs = []
// Add all services from the hostServiceNameMap
for (const [dirName, fullServiceName] of Object.entries(hostServiceNameMap)) {
const capitalizedName = dirName.charAt(0).toUpperCase() + dirName.slice(1)
serviceImports.push(
`import { handle${capitalizedName}ServiceRequest, handle${capitalizedName}ServiceStreamingRequest } from "./${dirName}/index"`,
)
serviceConfigs.push(`
"${fullServiceName}": {
requestHandler: handle${capitalizedName}ServiceRequest,
streamingHandler: handle${capitalizedName}ServiceStreamingRequest
}`)
}
const content = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { StreamingResponseHandler } from "./host-grpc-handler"
${serviceImports.join("\n")}
/**
* Configuration for a host service handler
*/
export interface HostServiceHandlerConfig {
requestHandler: (method: string, message: any) => Promise<any>;
streamingHandler: (method: string, message: any, responseStream: StreamingResponseHandler, requestId?: string) => Promise<void>;
}
/**
* Map of host service names to their handler configurations
*/
export const hostServiceHandlers: Record<string, HostServiceHandlerConfig> = {${serviceConfigs.join(",")}
};`
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}`))
}
/**
* Generate a gRPC client configuration file for host services
*/
async function generateHostGrpcClientConfig() {
console.log(chalk.cyan("Generating host gRPC client configuration..."))
const serviceImports = []
const serviceClientCreations = []
const serviceExports = []
// Process each service in the hostServiceNameMap
for (const [dirName, fullServiceName] of Object.entries(hostServiceNameMap)) {
const capitalizedName = dirName.charAt(0).toUpperCase() + dirName.slice(1)
// Add import statement
serviceImports.push(`import { ${capitalizedName}ServiceDefinition } from "@shared/proto/host/${dirName}"`)
// Add client creation
serviceClientCreations.push(
`const ${capitalizedName}ServiceClient = createGrpcClient(${capitalizedName}ServiceDefinition)`,
)
// Add to exports
serviceExports.push(`${capitalizedName}ServiceClient`)
}
// Generate the file content
const content = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { createGrpcClient } from "./host-grpc-client-base"
${serviceImports.join("\n")}
${serviceClientCreations.join("\n")}
export {
${serviceExports.join(",\n\t")}
}`
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}`))
}
// Check for Apple Silicon compatibility
function checkAppleSiliconCompatibility() {
// Only run check on macOS
if (process.platform !== "darwin") {
return
}
// Check if running on Apple Silicon
const cpuArchitecture = os.arch()
if (cpuArchitecture === "arm64") {
try {
// Check if Rosetta is installed
const rosettaCheck = execSync('/usr/bin/pgrep oahd || echo "NOT_INSTALLED"').toString().trim()
if (rosettaCheck === "NOT_INSTALLED") {
console.log(chalk.yellow("Detected Apple Silicon (ARM64) architecture."))
console.log(
chalk.red("Rosetta 2 is NOT installed. The npm version of protoc is not compatible with Apple Silicon."),
)
console.log(chalk.cyan("Please install Rosetta 2 using the following command:"))
console.log(chalk.cyan(" softwareupdate --install-rosetta --agree-to-license"))
console.log(chalk.red("Aborting build process."))
process.exit(1)
}
} catch (error) {
console.log(chalk.yellow("Could not determine Rosetta installation status. Proceeding anyway."))
}
}
}
// Run the main function
main().catch((error) => {
console.error(chalk.red("Error:"), error)
-36
View File
@@ -1,36 +0,0 @@
syntax = "proto3";
package host;
option java_package = "bot.cline.host.proto";
option java_multiple_files = true;
import "common.proto";
// UriService provides methods for working with URIs in the IDE
service UriService {
// Create a new file URI from a file path
rpc file(cline.StringRequest) returns (Uri);
// Join a URI with additional path segments
rpc joinPath(JoinPathRequest) returns (Uri);
// Parse a string URI into a Uri object
rpc parse(cline.StringRequest) returns (Uri);
}
// Uri represents a URI in the IDE
message Uri {
string scheme = 1;
string authority = 2;
string path = 3;
string query = 4;
string fragment = 5;
string fsPath = 6;
}
// Request for joining path segments to a URI
message JoinPathRequest {
cline.Metadata metadata = 1;
Uri base = 2;
repeated string pathSegments = 3;
}
-32
View File
@@ -1,32 +0,0 @@
syntax = "proto3";
package host;
option java_package = "bot.cline.host.proto";
option java_multiple_files = true;
import "common.proto";
// WatchService provides methods for watching files in the IDE
service WatchService {
// Subscribe to file changes
rpc subscribeToFile(SubscribeToFileRequest) returns (stream FileChangeEvent);
}
// Request to subscribe to file changes
message SubscribeToFileRequest {
cline.Metadata metadata = 1;
string path = 2;
}
// Event representing a file change
message FileChangeEvent {
enum ChangeType {
CREATED = 0;
CHANGED = 1;
DELETED = 2;
}
string path = 1;
ChangeType type = 2;
string content = 3; // Optional content of the file after change
}
-3
View File
@@ -16,9 +16,6 @@ service McpService {
rpc toggleToolAutoApprove(ToggleToolAutoApproveRequest) returns (McpServers);
rpc refreshMcpMarketplace(EmptyRequest) returns (McpMarketplaceCatalog);
rpc openMcpSettings(EmptyRequest) returns (Empty);
// Subscribe to MCP marketplace catalog updates
rpc subscribeToMcpMarketplaceCatalog(EmptyRequest) returns (stream McpMarketplaceCatalog);
}
message ToggleMcpServerRequest {
+8 -35
View File
@@ -20,8 +20,6 @@ service ModelsService {
rpc refreshOpenAiModels(OpenAiModelsRequest) returns (StringArray);
// Refreshes and returns Requesty models
rpc refreshRequestyModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
// Subscribe to OpenRouter models updates
rpc subscribeToOpenRouterModels(EmptyRequest) returns (stream OpenRouterCompatibleModelInfo);
}
// List of VS Code LM models
@@ -37,42 +35,17 @@ message VsCodeLmModel {
string id = 4;
}
// Price tier for tiered pricing models
message PriceTier {
int32 token_limit = 1; // Upper limit (inclusive) of input tokens for this price
double price = 2; // Price per million tokens for this tier
}
// Thinking configuration for models that support thinking/reasoning
message ThinkingConfig {
optional int32 max_budget = 1; // Max allowed thinking budget tokens
optional double output_price = 2; // Output price per million tokens when budget > 0
repeated PriceTier output_price_tiers = 3; // Optional: Tiered output price when budget > 0
}
// Model tier for tiered pricing structures
message ModelTier {
int32 context_window = 1;
optional double input_price = 2;
optional double output_price = 3;
optional double cache_writes_price = 4;
optional double cache_reads_price = 5;
}
// For OpenRouterCompatibleModelInfo structure in OpenRouterModels
message OpenRouterModelInfo {
optional int32 max_tokens = 1;
optional int32 context_window = 2;
optional bool supports_images = 3;
int32 max_tokens = 1;
int32 context_window = 2;
bool supports_images = 3;
bool supports_prompt_cache = 4;
optional double input_price = 5;
optional double output_price = 6;
optional double cache_writes_price = 7;
optional double cache_reads_price = 8;
optional string description = 9;
optional ThinkingConfig thinking_config = 10;
optional bool supports_global_endpoint = 11;
repeated ModelTier tiers = 12;
double input_price = 5;
double output_price = 6;
double cache_writes_price = 7;
double cache_reads_price = 8;
string description = 9;
}
// Shared response message for model information
-209
View File
@@ -18,206 +18,6 @@ message WebviewProviderTypeRequest {
WebviewProviderType providerType = 2;
}
// Enum for ClineMessage type
enum ClineMessageType {
ASK = 0;
SAY = 1;
}
// Enum for ClineAsk types
enum ClineAsk {
FOLLOWUP = 0;
PLAN_MODE_RESPOND = 1;
COMMAND = 2;
COMMAND_OUTPUT = 3;
COMPLETION_RESULT = 4;
TOOL = 5;
API_REQ_FAILED = 6;
RESUME_TASK = 7;
RESUME_COMPLETED_TASK = 8;
MISTAKE_LIMIT_REACHED = 9;
AUTO_APPROVAL_MAX_REQ_REACHED = 10;
BROWSER_ACTION_LAUNCH = 11;
USE_MCP_SERVER = 12;
NEW_TASK = 13;
CONDENSE = 14;
REPORT_BUG = 15;
}
// Enum for ClineSay types
enum ClineSay {
TASK = 0;
ERROR = 1;
API_REQ_STARTED = 2;
API_REQ_FINISHED = 3;
TEXT = 4;
REASONING = 5;
COMPLETION_RESULT_SAY = 6;
USER_FEEDBACK = 7;
USER_FEEDBACK_DIFF = 8;
API_REQ_RETRIED = 9;
COMMAND_SAY = 10;
COMMAND_OUTPUT_SAY = 11;
TOOL_SAY = 12;
SHELL_INTEGRATION_WARNING = 13;
BROWSER_ACTION_LAUNCH_SAY = 14;
BROWSER_ACTION = 15;
BROWSER_ACTION_RESULT = 16;
MCP_SERVER_REQUEST_STARTED = 17;
MCP_SERVER_RESPONSE = 18;
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
enum ClineSayToolType {
EDITED_EXISTING_FILE = 0;
NEW_FILE_CREATED = 1;
READ_FILE = 2;
LIST_FILES_TOP_LEVEL = 3;
LIST_FILES_RECURSIVE = 4;
LIST_CODE_DEFINITION_NAMES = 5;
SEARCH_FILES = 6;
WEB_FETCH = 7;
}
// Enum for browser actions
enum BrowserAction {
LAUNCH = 0;
CLICK = 1;
TYPE = 2;
SCROLL_DOWN = 3;
SCROLL_UP = 4;
CLOSE = 5;
}
// Enum for MCP server request types
enum McpServerRequestType {
USE_MCP_TOOL = 0;
ACCESS_MCP_RESOURCE = 1;
}
// Enum for API request cancel reasons
enum ClineApiReqCancelReason {
STREAMING_FAILED = 0;
USER_CANCELLED = 1;
RETRIES_EXHAUSTED = 2;
}
// Message for conversation history deleted range
message ConversationHistoryDeletedRange {
int32 start_index = 1;
int32 end_index = 2;
}
// Message for ClineSayTool
message ClineSayTool {
ClineSayToolType tool = 1;
string path = 2;
string diff = 3;
string content = 4;
string regex = 5;
string file_pattern = 6;
bool operation_is_located_in_workspace = 7;
}
// Message for ClineSayBrowserAction
message ClineSayBrowserAction {
BrowserAction action = 1;
string coordinate = 2;
string text = 3;
}
// Message for BrowserActionResult
message BrowserActionResult {
string screenshot = 1;
string logs = 2;
string current_url = 3;
string current_mouse_position = 4;
}
// Message for ClineAskUseMcpServer
message ClineAskUseMcpServer {
string server_name = 1;
McpServerRequestType type = 2;
string tool_name = 3;
string arguments = 4;
string uri = 5;
}
// Message for ClinePlanModeResponse
message ClinePlanModeResponse {
string response = 1;
repeated string options = 2;
string selected = 3;
}
// Message for ClineAskQuestion
message ClineAskQuestion {
string question = 1;
repeated string options = 2;
string selected = 3;
}
// Message for ClineAskNewTask
message ClineAskNewTask {
string context = 1;
}
// Message for API request retry status
message ApiReqRetryStatus {
int32 attempt = 1;
int32 max_attempts = 2;
int32 delay_sec = 3;
string error_snippet = 4;
}
// Message for ClineApiReqInfo
message ClineApiReqInfo {
string request = 1;
int32 tokens_in = 2;
int32 tokens_out = 3;
int32 cache_writes = 4;
int32 cache_reads = 5;
double cost = 6;
ClineApiReqCancelReason cancel_reason = 7;
string streaming_failed_message = 8;
ApiReqRetryStatus retry_status = 9;
}
// Main ClineMessage type
message ClineMessage {
int64 ts = 1;
ClineMessageType type = 2;
ClineAsk ask = 3;
ClineSay say = 4;
string text = 5;
string reasoning = 6;
repeated string images = 7;
repeated string files = 8;
bool partial = 9;
string last_checkpoint_hash = 10;
bool is_checkpoint_checked_out = 11;
bool is_operation_outside_workspace = 12;
int32 conversation_history_index = 13;
ConversationHistoryDeletedRange conversation_history_deleted_range = 14;
// Additional fields for specific ask/say types
ClineSayTool say_tool = 15;
ClineSayBrowserAction say_browser_action = 16;
BrowserActionResult browser_action_result = 17;
ClineAskUseMcpServer ask_use_mcp_server = 18;
ClinePlanModeResponse plan_mode_response = 19;
ClineAskQuestion ask_question = 20;
ClineAskNewTask ask_new_task = 21;
ClineApiReqInfo api_req_info = 22;
}
// UiService provides methods for managing UI interactions
service UiService {
// Scrolls to a specific settings section in the settings view
@@ -240,13 +40,4 @@ service UiService {
// Subscribe to account button click events
rpc subscribeToAccountButtonClicked(EmptyRequest) returns (stream Empty);
// Subscribe to settings button clicked events
rpc subscribeToSettingsButtonClicked(WebviewProviderTypeRequest) returns (stream Empty);
// Subscribe to partial message updates (streaming Cline messages as they're built)
rpc subscribeToPartialMessage(EmptyRequest) returns (stream ClineMessage);
// Subscribe to theme change events
rpc subscribeToTheme(EmptyRequest) returns (stream String);
}
+3 -5
View File
@@ -32,15 +32,13 @@ function generateHandlersAndExports() {
handlerSetup.push(` server.addService(proto.cline.${name}.service, {`)
for (const [rpcName, rpc] of Object.entries(def.service)) {
imports.push(`import { ${rpcName} } from "../core/controller/${dir}/${rpcName}"`)
const requestType = "proto.cline." + rpc.requestType.type.name
if (rpc.requestStream) {
throw new Error("Request streaming is not supported")
}
if (rpc.responseStream) {
handlerSetup.push(` ${rpcName}: wrapStreamingResponse<${requestType},void>(${rpcName}, controller),`)
handlerSetup.push(` ${rpcName}: wrapStreamingResponse(${rpcName}, controller),`)
} else {
const responseType = "proto.cline." + rpc.responseType.type.name
handlerSetup.push(` ${rpcName}: wrapper<${requestType},${responseType}>(${rpcName}, controller),`)
handlerSetup.push(` ${rpcName}: wrapper(${rpcName}, controller),`)
}
}
handlerSetup.push(` });`)
@@ -60,11 +58,11 @@ const scriptName = path.basename(fileURLToPath(import.meta.url))
let output = `// GENERATED CODE -- DO NOT EDIT!
// Generated by ${scriptName}
import * as grpc from "@grpc/grpc-js"
import * as proto from "@/shared/proto"
import { Controller } from "../core/controller"
import { GrpcHandlerWrapper, GrpcStreamingResponseHandlerWrapper } from "./grpc-types"
${imports}
export function addServices(
server: grpc.Server,
proto: any,
+2 -19
View File
@@ -53,37 +53,20 @@ export class QwenHandler implements ApiHandler {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const model = this.getModel()
const isDeepseekReasoner = model.id.includes("deepseek-r1")
const isReasoningModelFamily = model.id.includes("qwen3") || ["qwen-plus-latest", "qwen-turbo-latest"].includes(model.id)
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
let temperature: number | undefined = 0
// Configuration for extended thinking
const budgetTokens = this.options.thinkingBudgetTokens || 0
const reasoningOn = budgetTokens !== 0 ? true : false
const thinkingArgs = isReasoningModelFamily
? {
enable_thinking: reasoningOn,
thinking_budget: reasoningOn ? budgetTokens : undefined,
}
: undefined
if (isDeepseekReasoner || (reasoningOn && isReasoningModelFamily)) {
if (isDeepseekReasoner) {
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
temperature = undefined
}
const stream = await this.client.chat.completions.create({
model: model.id,
max_completion_tokens: model.info.maxTokens,
messages: openAiMessages,
stream: true,
stream_options: { include_usage: true },
temperature,
...thinkingArgs,
...(model.id === "deepseek-r1" ? {} : { temperature: 0 }),
})
for await (const chunk of stream) {
+1
View File
@@ -528,6 +528,7 @@ class NewFileContentConstructor {
} else {
let appendToPendingNonStandardLines = canWritependingNonStandardLines
if (appendToPendingNonStandardLines) {
console.log("unstandard line:" + line)
// 处理非标内容
this.pendingNonStandardLines.push(line)
}
+1 -11
View File
@@ -3,8 +3,6 @@ import { RelativePathsRequest, RelativePaths } from "@shared/proto/file"
import { FileMethodHandler } from "./index"
import * as vscode from "vscode"
import * as path from "path"
import { UriServiceClient } from "../../../standalone/services/host-grpc-client"
import { Metadata, StringRequest } from "@shared/proto/common"
/**
* Converts a list of URIs to workspace-relative paths
@@ -19,15 +17,7 @@ export const getRelativePaths: FileMethodHandler = async (
const resolvedPaths = await Promise.all(
request.uris.map(async (uriString) => {
try {
// Use the host URI service client instead of directly using vscode.Uri.parse
const parseResponse = await UriServiceClient.parse(
StringRequest.create({
metadata: Metadata.create({}),
value: uriString,
}),
)
const fileUri = vscode.Uri.parse(`${parseResponse.scheme}://${parseResponse.authority}${parseResponse.path}`)
console.log("[DEBUG] UriServiceClient.parse:", fileUri)
const fileUri = vscode.Uri.parse(uriString, true)
const relativePathToGet = vscode.workspace.asRelativePath(fileUri, false)
// If the path is still absolute, it's outside the workspace
+53 -24
View File
@@ -1,6 +1,7 @@
import { Anthropic } from "@anthropic-ai/sdk"
import axios from "axios"
import { v4 as uuidv4 } from "uuid"
import fs from "fs/promises"
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
import pWaitFor from "p-wait-for"
@@ -12,8 +13,12 @@ import { EmptyRequest } from "@shared/proto/common"
import { buildApiHandler } from "@api/index"
import { cleanupLegacyCheckpoints } from "@integrations/checkpoints/CheckpointMigration"
import { downloadTask } from "@integrations/misc/export-markdown"
import { fetchOpenGraphData } from "@integrations/misc/link-preview"
import { handleFileServiceRequest } from "./file"
import { getTheme } from "@integrations/theme/getTheme"
import WorkspaceTracker from "@integrations/workspace/WorkspaceTracker"
import { ClineAccountService } from "@services/account/ClineAccountService"
import { BrowserSession } from "@services/browser/BrowserSession"
import { McpHub } from "@services/mcp/McpHub"
import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
import { ApiProvider, ModelInfo } from "@shared/api"
@@ -21,31 +26,39 @@ import { ChatContent } from "@shared/ChatContent"
import { ChatSettings } from "@shared/ChatSettings"
import { ExtensionMessage, ExtensionState, Platform } from "@shared/ExtensionMessage"
import { HistoryItem } from "@shared/HistoryItem"
import { McpMarketplaceCatalog } from "@shared/mcp"
import { McpDownloadResponse, McpMarketplaceCatalog, McpServer } from "@shared/mcp"
import { TelemetrySetting } from "@shared/TelemetrySetting"
import { WebviewMessage } from "@shared/WebviewMessage"
import { fileExistsAtPath } from "@utils/fs"
import { getWorkingState } from "@utils/git"
import { extractCommitMessage } from "@integrations/git/commit-message-generator"
import { ensureMcpServersDirectoryExists, ensureSettingsDirectoryExists, GlobalFileNames } from "../storage/disk"
import { getTotalTasksSize } from "@utils/storage"
import {
ensureMcpServersDirectoryExists,
ensureSettingsDirectoryExists,
GlobalFileNames,
ensureWorkflowsDirectoryExists,
} from "../storage/disk"
import {
getAllExtensionState,
getGlobalState,
getSecret,
getWorkspaceState,
resetExtensionState,
storeSecret,
updateApiConfiguration,
updateGlobalState,
updateWorkspaceState,
} from "../storage/state"
import { Task } from "../task"
import { Task, cwd } from "../task"
import { ClineRulesToggles } from "@shared/cline-rules"
import { sendStateUpdate } from "./state/subscribeToState"
import { sendAddToInputEvent } from "./ui/subscribeToAddToInput"
import { sendAuthCallbackEvent } from "./account/subscribeToAuthCallback"
import { sendMcpMarketplaceCatalogEvent } from "./mcp/subscribeToMcpMarketplaceCatalog"
import { sendOpenRouterModelsEvent } from "./models/subscribeToOpenRouterModels"
import { OpenRouterCompatibleModelInfo } from "@/shared/proto/models"
import { sendChatButtonClickedEvent } from "./ui/subscribeToChatButtonClicked"
import { refreshClineRulesToggles } from "@core/context/instructions/user-instructions/cline-rules"
import { refreshExternalRulesToggles } from "@core/context/instructions/user-instructions/external-rules"
import { refreshWorkflowToggles } from "@core/context/instructions/user-instructions/workflows"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -138,7 +151,6 @@ export class Controller {
browserSettings,
chatSettings,
shellIntegrationTimeout,
terminalReuseEnabled,
enableCheckpointsSetting,
isNewUser,
taskHistory,
@@ -173,7 +185,6 @@ export class Controller {
browserSettings,
chatSettings,
shellIntegrationTimeout,
terminalReuseEnabled ?? true,
enableCheckpointsSetting ?? true,
customInstructions,
task,
@@ -210,10 +221,19 @@ export class Controller {
case "webviewDidLaunch":
this.postStateToWebview()
this.workspaceTracker?.populateFilePaths() // don't await
getTheme().then((theme) =>
this.postMessageToWebview({
type: "theme",
text: JSON.stringify(theme),
}),
)
// post last cached models in case the call to endpoint fails
this.readOpenRouterModels().then((openRouterModels) => {
if (openRouterModels) {
sendOpenRouterModelsEvent(OpenRouterCompatibleModelInfo.create({ models: openRouterModels }))
this.postMessageToWebview({
type: "openRouterModels",
openRouterModels,
})
}
})
// gui relies on model info to be up-to-date to provide the most accurate pricing, so we need to fetch the latest details on launch.
@@ -223,7 +243,10 @@ export class Controller {
getGlobalState(this.context, "mcpMarketplaceCatalog").then((mcpMarketplaceCatalog) => {
if (mcpMarketplaceCatalog) {
sendMcpMarketplaceCatalogEvent(mcpMarketplaceCatalog as McpMarketplaceCatalog)
this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
mcpMarketplaceCatalog: mcpMarketplaceCatalog as McpMarketplaceCatalog,
})
}
})
this.silentlyRefreshMcpMarketplace()
@@ -378,15 +401,6 @@ export class Controller {
}
}
// terminal settings
if (typeof message.shellIntegrationTimeout === "number") {
await updateGlobalState(this.context, "shellIntegrationTimeout", message.shellIntegrationTimeout)
}
if (typeof message.terminalReuseEnabled === "boolean") {
await updateGlobalState(this.context, "terminalReuseEnabled", message.terminalReuseEnabled)
}
// after settings are updated, post state to webview
await this.postStateToWebview()
@@ -734,6 +748,10 @@ export class Controller {
console.error("Failed to fetch MCP marketplace:", error)
if (!silent) {
const errorMessage = error instanceof Error ? error.message : "Failed to fetch MCP marketplace"
await this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
error: errorMessage,
})
vscode.window.showErrorMessage(errorMessage)
}
return undefined
@@ -779,7 +797,10 @@ export class Controller {
try {
const catalog = await this.fetchMcpMarketplaceFromApi(true)
if (catalog) {
await sendMcpMarketplaceCatalogEvent(catalog)
await this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
mcpMarketplaceCatalog: catalog,
})
}
} catch (error) {
console.error("Failed to silently refresh MCP marketplace:", error)
@@ -807,17 +828,27 @@ export class Controller {
| McpMarketplaceCatalog
| undefined
if (!forceRefresh && cachedCatalog?.items) {
await sendMcpMarketplaceCatalogEvent(cachedCatalog)
await this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
mcpMarketplaceCatalog: cachedCatalog,
})
return
}
const catalog = await this.fetchMcpMarketplaceFromApi(false)
if (catalog) {
await sendMcpMarketplaceCatalogEvent(catalog)
await this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
mcpMarketplaceCatalog: catalog,
})
}
} catch (error) {
console.error("Failed to handle cached MCP marketplace:", error)
const errorMessage = error instanceof Error ? error.message : "Failed to handle cached MCP marketplace"
await this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
error: errorMessage,
})
vscode.window.showErrorMessage(errorMessage)
}
}
@@ -1140,7 +1171,6 @@ export class Controller {
globalClineRulesToggles,
globalWorkflowToggles,
shellIntegrationTimeout,
terminalReuseEnabled,
isNewUser,
} = await getAllExtensionState(this.context)
@@ -1185,7 +1215,6 @@ export class Controller {
localWorkflowToggles: localWorkflowToggles || {},
globalWorkflowToggles: globalWorkflowToggles || {},
shellIntegrationTimeout,
terminalReuseEnabled,
isNewUser,
}
}
@@ -1,55 +0,0 @@
import { Controller } from "../index"
import { EmptyRequest } from "@shared/proto/common"
import { McpMarketplaceCatalog } from "@shared/proto/mcp"
import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler"
// Keep track of active subscriptions
const activeMcpMarketplaceSubscriptions = new Set<StreamingResponseHandler>()
/**
* Subscribe to MCP marketplace catalog updates
* @param controller The controller instance
* @param request The empty request
* @param responseStream The streaming response handler
* @param requestId The ID of the request (passed by the gRPC handler)
*/
export async function subscribeToMcpMarketplaceCatalog(
controller: Controller,
request: EmptyRequest,
responseStream: StreamingResponseHandler,
requestId?: string,
): Promise<void> {
// Add this subscription to the active subscriptions
activeMcpMarketplaceSubscriptions.add(responseStream)
// Register cleanup when the connection is closed
const cleanup = () => {
activeMcpMarketplaceSubscriptions.delete(responseStream)
}
// Register the cleanup function with the request registry if we have a requestId
if (requestId) {
getRequestRegistry().registerRequest(requestId, cleanup, { type: "mcp_marketplace_subscription" }, responseStream)
}
}
/**
* Send an MCP marketplace catalog event to all active subscribers
*/
export async function sendMcpMarketplaceCatalogEvent(catalog: McpMarketplaceCatalog): Promise<void> {
// Send the event to all active subscribers
const promises = Array.from(activeMcpMarketplaceSubscriptions).map(async (responseStream) => {
try {
await responseStream(
catalog,
false, // Not the last message
)
} catch (error) {
console.error("Error sending MCP marketplace catalog event:", error)
// Remove the subscription if there was an error
activeMcpMarketplaceSubscriptions.delete(responseStream)
}
})
await Promise.all(promises)
}
@@ -19,7 +19,7 @@ export async function refreshOpenRouterModels(
): Promise<OpenRouterCompatibleModelInfo> {
const openRouterModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.openRouterModels)
let models: Record<string, OpenRouterModelInfo> = {}
let models: Record<string, Partial<OpenRouterModelInfo>> = {}
try {
const response = await axios.get("https://openrouter.ai/api/v1/models")
@@ -32,20 +32,15 @@ export async function refreshOpenRouterModels(
return undefined
}
for (const rawModel of rawModels) {
const modelInfo = OpenRouterModelInfo.create({
maxTokens: rawModel.top_provider?.max_completion_tokens ?? 0,
contextWindow: rawModel.context_length ?? 0,
supportsImages: rawModel.architecture?.modality?.includes("image") ?? false,
const modelInfo: Partial<OpenRouterModelInfo> = {
maxTokens: rawModel.top_provider?.max_completion_tokens,
contextWindow: rawModel.context_length,
supportsImages: rawModel.architecture?.modality?.includes("image"),
supportsPromptCache: false,
inputPrice: parsePrice(rawModel.pricing?.prompt) ?? 0,
outputPrice: parsePrice(rawModel.pricing?.completion) ?? 0,
cacheWritesPrice: 0,
cacheReadsPrice: 0,
description: rawModel.description ?? "",
thinkingConfig: rawModel.thinking_config ?? undefined,
supportsGlobalEndpoint: rawModel.supports_global_endpoint ?? undefined,
tiers: rawModel.tiers ?? [],
})
inputPrice: parsePrice(rawModel.pricing?.prompt),
outputPrice: parsePrice(rawModel.pricing?.completion),
description: rawModel.description,
}
switch (rawModel.id) {
case "anthropic/claude-sonnet-4":
@@ -134,13 +129,30 @@ export async function refreshOpenRouterModels(
}
}
return OpenRouterCompatibleModelInfo.create({ models })
// Convert the Record<string, Partial<OpenRouterModelInfo>> to Record<string, OpenRouterModelInfo>
// by filling in any missing required fields with defaults
const typedModels: Record<string, OpenRouterModelInfo> = {}
for (const [key, model] of Object.entries(models)) {
typedModels[key] = {
maxTokens: model.maxTokens ?? 0,
contextWindow: model.contextWindow ?? 0,
supportsImages: model.supportsImages ?? false,
supportsPromptCache: model.supportsPromptCache ?? false,
inputPrice: model.inputPrice ?? 0,
outputPrice: model.outputPrice ?? 0,
cacheWritesPrice: model.cacheWritesPrice ?? 0,
cacheReadsPrice: model.cacheReadsPrice ?? 0,
description: model.description ?? "",
}
}
return OpenRouterCompatibleModelInfo.create({ models: typedModels })
}
/**
* Reads cached OpenRouter models from disk
*/
async function readOpenRouterModels(controller: Controller): Promise<Record<string, OpenRouterModelInfo> | undefined> {
async function readOpenRouterModels(controller: Controller): Promise<Record<string, Partial<OpenRouterModelInfo>> | undefined> {
const openRouterModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.openRouterModels)
const fileExists = await fileExistsAtPath(openRouterModelsFilePath)
if (fileExists) {
@@ -1,60 +0,0 @@
import { Controller } from "../index"
import { EmptyRequest } from "@shared/proto/common"
import { OpenRouterCompatibleModelInfo } from "@shared/proto/models"
import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler"
// Keep track of active OpenRouter models subscriptions
const activeOpenRouterModelsSubscriptions = new Set<StreamingResponseHandler>()
/**
* Subscribe to OpenRouter models events
* @param controller The controller instance
* @param request The empty request
* @param responseStream The streaming response handler
* @param requestId The ID of the request (passed by the gRPC handler)
*/
export async function subscribeToOpenRouterModels(
controller: Controller,
request: EmptyRequest,
responseStream: StreamingResponseHandler,
requestId?: string,
): Promise<void> {
console.log("[DEBUG] set up OpenRouter models subscription")
// Add this subscription to the active subscriptions
activeOpenRouterModelsSubscriptions.add(responseStream)
// Register cleanup when the connection is closed
const cleanup = () => {
activeOpenRouterModelsSubscriptions.delete(responseStream)
console.log("[DEBUG] Cleaned up OpenRouter models subscription")
}
// Register the cleanup function with the request registry if we have a requestId
if (requestId) {
getRequestRegistry().registerRequest(requestId, cleanup, { type: "openRouterModels_subscription" }, responseStream)
}
}
/**
* Send an OpenRouter models event to all active subscribers
* @param models The OpenRouter models to send
*/
export async function sendOpenRouterModelsEvent(models: OpenRouterCompatibleModelInfo): Promise<void> {
// Send the event to all active subscribers
const promises = Array.from(activeOpenRouterModelsSubscriptions).map(async (responseStream) => {
try {
await responseStream(
models,
false, // Not the last message
)
console.log("[DEBUG] sending OpenRouter models event")
} catch (error) {
console.error("Error sending OpenRouter models event:", error)
// Remove the subscription if there was an error
activeOpenRouterModelsSubscriptions.delete(responseStream)
}
})
await Promise.all(promises)
}
-1
View File
@@ -1,7 +1,6 @@
import { Controller } from ".."
import { Empty } from "../../../shared/proto/common"
import { NewTaskRequest } from "../../../shared/proto/task"
import { handleFileServiceRequest } from "../file"
/**
* Creates a new task with the given text and optional images
@@ -14,7 +14,7 @@ const activeSubscriptions = new Map<string, StreamingResponseHandler>()
*/
export async function subscribeToAccountButtonClicked(
controller: Controller,
_request: EmptyRequest,
request: EmptyRequest,
responseStream: StreamingResponseHandler,
requestId?: string,
): Promise<void> {
@@ -46,7 +46,7 @@ export async function sendAccountButtonClickedEvent(controllerId: string): Promi
}
try {
const event: Empty = Empty.create({})
const event: Empty = {}
await responseStream(event, false)
} catch (error) {
console.error(`Error sending account button clicked event to controller ${controllerId}:`, error)
@@ -15,7 +15,7 @@ const activeChatButtonClickedSubscriptions = new Map<string, StreamingResponseHa
*/
export async function subscribeToChatButtonClicked(
controller: Controller,
_request: EmptyRequest,
request: EmptyRequest,
responseStream: StreamingResponseHandler,
requestId?: string,
): Promise<void> {
@@ -50,7 +50,7 @@ export async function sendChatButtonClickedEvent(controllerId: string): Promise<
}
try {
const event: Empty = Empty.create({})
const event: Empty = {}
await responseStream(
event,
false, // Not the last message
@@ -14,7 +14,7 @@ const activeHistoryButtonClickedSubscriptions = new Map<StreamingResponseHandler
* @param requestId The ID of the request (passed by the gRPC handler)
*/
export async function subscribeToHistoryButtonClicked(
_controller: Controller,
controller: Controller,
request: WebviewProviderTypeRequest,
responseStream: StreamingResponseHandler,
requestId?: string,
@@ -50,7 +50,7 @@ export async function sendHistoryButtonClickedEvent(webviewType?: WebviewProvide
}
try {
const event: Empty = Empty.create({})
const event: Empty = {}
await responseStream(
event,
false, // Not the last message
@@ -14,7 +14,7 @@ const mcpButtonClickedSubscriptions = new Map<StreamingResponseHandler, WebviewP
* @param requestId The ID of the request (passed by the gRPC handler)
*/
export async function subscribeToMcpButtonClicked(
_controller: Controller,
controller: Controller,
request: WebviewProviderTypeRequest,
responseStream: StreamingResponseHandler,
requestId?: string,
@@ -41,7 +41,7 @@ export async function subscribeToMcpButtonClicked(
* @param webviewType The type of webview that triggered the event (SIDEBAR or TAB)
*/
export async function sendMcpButtonClickedEvent(webviewType?: WebviewProviderType): Promise<void> {
const event: Empty = Empty.create({})
const event: Empty = {}
// Process all subscriptions, filtering based on the source
const promises = Array.from(mcpButtonClickedSubscriptions.entries()).map(async ([responseStream, providerType]) => {
@@ -1,56 +0,0 @@
import { Controller } from "../index"
import { EmptyRequest } from "@shared/proto/common"
import { ClineMessage } from "@shared/proto/ui"
import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler"
// Keep track of active partial message subscriptions
const activePartialMessageSubscriptions = new Set<StreamingResponseHandler>()
/**
* Subscribe to partial message events
* @param controller The controller instance
* @param request The empty request
* @param responseStream The streaming response handler
* @param requestId The ID of the request (passed by the gRPC handler)
*/
export async function subscribeToPartialMessage(
controller: Controller,
request: EmptyRequest,
responseStream: StreamingResponseHandler,
requestId?: string,
): Promise<void> {
// Add this subscription to the active subscriptions
activePartialMessageSubscriptions.add(responseStream)
// Register cleanup when the connection is closed
const cleanup = () => {
activePartialMessageSubscriptions.delete(responseStream)
}
// Register the cleanup function with the request registry if we have a requestId
if (requestId) {
getRequestRegistry().registerRequest(requestId, cleanup, { type: "partial_message_subscription" }, responseStream)
}
}
/**
* Send a partial message event to all active subscribers
* @param partialMessage The ClineMessage to send
*/
export async function sendPartialMessageEvent(partialMessage: ClineMessage): Promise<void> {
// Send the event to all active subscribers
const promises = Array.from(activePartialMessageSubscriptions).map(async (responseStream) => {
try {
await responseStream(
partialMessage,
false, // Not the last message
)
} catch (error) {
console.error("Error sending partial message event:", error)
// Remove the subscription if there was an error
activePartialMessageSubscriptions.delete(responseStream)
}
})
await Promise.all(promises)
}
@@ -1,61 +0,0 @@
import { Empty } from "@shared/proto/common"
import { WebviewProviderType, WebviewProviderTypeRequest } from "@shared/proto/ui"
import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler"
import type { Controller } from "../index"
// Track subscriptions with their provider type
const subscriptions = new Map<StreamingResponseHandler, WebviewProviderType>()
/**
* Subscribe to settings button clicked events
* @param controller The controller instance
* @param request The request with provider type
* @param responseStream The streaming response handler
* @param requestId The ID of the request (passed by the gRPC handler)
*/
export async function subscribeToSettingsButtonClicked(
controller: Controller,
request: WebviewProviderTypeRequest,
responseStream: StreamingResponseHandler,
requestId?: string,
): Promise<void> {
const providerType = request.providerType
console.log(`[DEBUG] set up settings button subscription for ${WebviewProviderType[providerType]} webview`)
// Store the subscription with its provider type
subscriptions.set(responseStream, providerType)
// Register cleanup when the connection is closed
const cleanup = () => {
subscriptions.delete(responseStream)
}
// Register the cleanup function with the request registry if we have a requestId
if (requestId) {
getRequestRegistry().registerRequest(requestId, cleanup, { type: "settings_button_clicked_subscription" }, responseStream)
}
}
/**
* Send a settings button clicked event to active subscribers of matching provider type
* @param webviewType The type of webview that triggered the event
*/
export async function sendSettingsButtonClickedEvent(webviewType?: WebviewProviderType): Promise<void> {
// Process all subscriptions, filtering based on the source
const promises = Array.from(subscriptions.entries()).map(async ([responseStream, providerType]) => {
// If webviewType is provided, only send to subscribers of the same type
if (webviewType !== undefined && webviewType !== providerType) {
return // Skip subscribers of different types
}
try {
const event = Empty.create({})
await responseStream(event, false) // Not the last message
} catch (error) {
console.error(`Error sending settings button clicked event to ${WebviewProviderType[providerType]}:`, error)
subscriptions.delete(responseStream)
}
})
await Promise.all(promises)
}
@@ -1,76 +0,0 @@
import { Controller } from "../index"
import { EmptyRequest, String } from "@shared/proto/common"
import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler"
import { getTheme } from "@integrations/theme/getTheme"
// Keep track of active theme subscriptions
const activeThemeSubscriptions = new Set<StreamingResponseHandler>()
/**
* Subscribe to theme change events
* @param controller The controller instance
* @param request The empty request
* @param responseStream The streaming response handler
* @param requestId The ID of the request (passed by the gRPC handler)
*/
export async function subscribeToTheme(
controller: Controller,
request: EmptyRequest,
responseStream: StreamingResponseHandler,
requestId?: string,
): Promise<void> {
// Add this subscription to the active subscriptions
activeThemeSubscriptions.add(responseStream)
// Register cleanup when the connection is closed
const cleanup = () => {
activeThemeSubscriptions.delete(responseStream)
}
// Register the cleanup function with the request registry if we have a requestId
if (requestId) {
getRequestRegistry().registerRequest(requestId, cleanup, { type: "theme_subscription" }, responseStream)
}
// Send the current theme immediately upon subscription
const theme = await getTheme()
if (theme) {
try {
const themeEvent = String.create({
value: JSON.stringify(theme),
})
await responseStream(
themeEvent,
false, // Not the last message
)
} catch (error) {
console.error("Error sending initial theme:", error)
activeThemeSubscriptions.delete(responseStream)
}
}
}
/**
* Send a theme event to all active subscribers
* @param themeJson The JSON-stringified theme data
*/
export async function sendThemeEvent(themeJson: string): Promise<void> {
// Send the event to all active subscribers
const promises = Array.from(activeThemeSubscriptions).map(async (responseStream) => {
try {
const event = String.create({
value: themeJson,
})
await responseStream(
event,
false, // Not the last message
)
} catch (error) {
console.error("Error sending theme event:", error)
// Remove the subscription if there was an error
activeThemeSubscriptions.delete(responseStream)
}
})
await Promise.all(promises)
}
-1
View File
@@ -91,7 +91,6 @@ export type GlobalStateKey =
| "favoritedModelIds"
| "requestTimeoutMs"
| "shellIntegrationTimeout"
| "terminalReuseEnabled"
| "isNewUser"
export type LocalStateKey = "localClineRulesToggles"
+1 -3
View File
@@ -165,7 +165,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
enableCheckpointsSettingRaw,
mcpMarketplaceEnabledRaw,
globalWorkflowToggles,
terminalReuseEnabled,
] = await Promise.all([
getGlobalState(context, "isNewUser") as Promise<boolean | undefined>,
getGlobalState(context, "apiProvider") as Promise<ApiProvider | undefined>,
@@ -256,7 +255,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
getGlobalState(context, "enableCheckpointsSetting") as Promise<boolean | undefined>,
getGlobalState(context, "mcpMarketplaceEnabled") as Promise<boolean | undefined>,
getGlobalState(context, "globalWorkflowToggles") as Promise<ClineRulesToggles | undefined>,
getGlobalState(context, "terminalReuseEnabled") as Promise<boolean | undefined>,
fetch,
])
let apiProvider: ApiProvider
@@ -391,7 +390,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
planActSeparateModelsSetting,
enableCheckpointsSetting: enableCheckpointsSetting,
shellIntegrationTimeout: shellIntegrationTimeout || 4000,
terminalReuseEnabled: terminalReuseEnabled ?? true,
globalWorkflowToggles: globalWorkflowToggles || {},
}
}
+10 -12
View File
@@ -72,8 +72,6 @@ import { ClineIgnoreController } from "@core/ignore/ClineIgnoreController"
import { parseMentions } from "@core/mentions"
import { formatResponse } from "@core/prompts/responses"
import { addUserInstructions, SYSTEM_PROMPT } from "@core/prompts/system"
import { sendPartialMessageEvent } from "@core/controller/ui/subscribeToPartialMessage"
import { convertClineMessageToProto } from "@shared/proto-conversions/cline-message"
import { getContextWindowInfo } from "@core/context/context-management/context-window-utils"
import { FileContextTracker } from "@core/context/context-tracking/FileContextTracker"
import { ModelContextTracker } from "@core/context/context-tracking/ModelContextTracker"
@@ -201,7 +199,6 @@ export class Task {
browserSettings: BrowserSettings,
chatSettings: ChatSettings,
shellIntegrationTimeout: number,
terminalReuseEnabled: boolean,
enableCheckpointsSetting: boolean,
customInstructions?: string,
task?: string,
@@ -221,7 +218,6 @@ export class Task {
// Initialization moved to startTask/resumeTaskFromHistory
this.terminalManager = new TerminalManager()
this.terminalManager.setShellIntegrationTimeout(shellIntegrationTimeout)
this.terminalManager.setTerminalReuseEnabled(terminalReuseEnabled ?? true)
this.urlContentFetcher = new UrlContentFetcher(context)
this.browserSession = new BrowserSession(context, browserSettings)
this.contextManager = new ContextManager()
@@ -745,8 +741,10 @@ export class Task {
// todo be more efficient about saving and posting only new data or one whole message at a time so ignore partial for saves, and only post parts of partial message instead of whole array in new listener
// await this.saveClineMessagesAndUpdateHistory()
// await this.postStateToWebview()
const protoMessage = convertClineMessageToProto(lastMessage)
await sendPartialMessageEvent(protoMessage)
await this.postMessageToWebview({
type: "partialMessage",
partialMessage: lastMessage,
})
throw new Error("Current ask promise was ignored 1")
} else {
// this is a new partial message, so add it with partial state
@@ -787,8 +785,10 @@ export class Task {
lastMessage.partial = false
await this.saveClineMessagesAndUpdateHistory()
// await this.postStateToWebview()
const protoMessage = convertClineMessageToProto(lastMessage)
await sendPartialMessageEvent(protoMessage)
await this.postMessageToWebview({
type: "partialMessage",
partialMessage: lastMessage,
})
} else {
// this is a new partial=false message, so add it like normal
this.askResponse = undefined
@@ -864,8 +864,7 @@ export class Task {
lastMessage.images = images
lastMessage.files = files
lastMessage.partial = partial
const protoMessage = convertClineMessageToProto(lastMessage)
await sendPartialMessageEvent(protoMessage)
await this.postMessageToWebview({ type: "partialMessage", partialMessage: lastMessage })
} else {
// this is a new partial message, so add it with partial state
const sayTs = Date.now()
@@ -895,8 +894,7 @@ export class Task {
// instead of streaming partialMessage events, we do a save and post like normal to persist to disk
await this.saveClineMessagesAndUpdateHistory()
// await this.postStateToWebview()
const protoMessage = convertClineMessageToProto(lastMessage)
await sendPartialMessageEvent(protoMessage) // more performant than an entire postStateToWebview
await this.postMessageToWebview({ type: "partialMessage", partialMessage: lastMessage }) // more performant than an entire postStateToWebview
} else {
// this is a new partial=false message, so add it like normal
const sayTs = Date.now()
+5 -6
View File
@@ -8,7 +8,6 @@ import { findLast } from "@shared/array"
import { readFile } from "fs/promises"
import path from "node:path"
import { WebviewProviderType } from "@/shared/webview/types"
import { sendThemeEvent } from "@core/controller/ui/subscribeToTheme"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -140,11 +139,11 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
vscode.workspace.onDidChangeConfiguration(
async (e) => {
if (e && e.affectsConfiguration("workbench.colorTheme")) {
// Send theme update via gRPC subscription
const theme = await getTheme()
if (theme) {
await sendThemeEvent(JSON.stringify(theme))
}
// Sends latest theme name to webview
await this.controller.postMessageToWebview({
type: "theme",
text: JSON.stringify(await getTheme()),
})
}
if (e && e.affectsConfiguration("cline.mcpMarketplace.enabled")) {
// Update state when marketplace tab setting changes
+14 -5
View File
@@ -16,7 +16,6 @@ import { sendChatButtonClickedEvent } from "./core/controller/ui/subscribeToChat
import { ErrorService } from "./services/error/ErrorService"
import { initializeTestMode, cleanupTestMode } from "./services/test/TestMode"
import { telemetryService } from "./services/posthog/telemetry/TelemetryService"
import { sendSettingsButtonClickedEvent } from "./core/controller/ui/subscribeToSettingsButtonClicked"
import { v4 as uuidv4 } from "uuid"
import { WebviewProviderType as WebviewProviderTypeEnum } from "@shared/proto/ui"
import { WebviewProviderType } from "./shared/webview/types"
@@ -170,10 +169,20 @@ export async function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand("cline.settingsButtonClicked", (webview: any) => {
const isSidebar = !webview
const webviewType = isSidebar ? WebviewProviderTypeEnum.SIDEBAR : WebviewProviderTypeEnum.TAB
sendSettingsButtonClickedEvent(webviewType)
WebviewProvider.getAllInstances().forEach((instance) => {
const openSettings = async (instance?: WebviewProvider) => {
instance?.controller.postMessageToWebview({
type: "action",
action: "settingsButtonClicked",
})
}
const isSidebar = !webview
if (isSidebar) {
openSettings(WebviewProvider.getSidebarInstance())
} else {
WebviewProvider.getTabInstances().forEach(openSettings)
}
})
}),
)
+30 -37
View File
@@ -94,7 +94,6 @@ export class TerminalManager {
private processes: Map<number, TerminalProcess> = new Map()
private disposables: vscode.Disposable[] = []
private shellIntegrationTimeout: number = 4000
private terminalReuseEnabled: boolean = true
constructor() {
let disposable: vscode.Disposable | undefined
@@ -235,44 +234,42 @@ export class TerminalManager {
return matchingTerminal
}
// If no non-busy terminal in the current working dir exists and terminal reuse is enabled, try to find any non-busy terminal regardless of CWD
if (this.terminalReuseEnabled) {
const availableTerminal = terminals.find((t) => !t.busy)
if (availableTerminal) {
// Set up promise and tracking for CWD change
const cwdPromise = new Promise<void>((resolve, reject) => {
availableTerminal.pendingCwdChange = cwd
availableTerminal.cwdResolved = { resolve, reject }
})
// If no matching terminal exists, try to find any non-busy terminal
const availableTerminal = terminals.find((t) => !t.busy)
if (availableTerminal) {
// Set up promise and tracking for CWD change
const cwdPromise = new Promise<void>((resolve, reject) => {
availableTerminal.pendingCwdChange = cwd
availableTerminal.cwdResolved = { resolve, reject }
})
// Navigate back to the desired directory
await this.runCommand(availableTerminal, `cd "${cwd}"`)
// Navigate back to the desired directory
await this.runCommand(availableTerminal, `cd "${cwd}"`)
// Either resolve immediately if CWD already updated or wait for event/timeout
if (this.isCwdMatchingExpected(availableTerminal)) {
if (availableTerminal.cwdResolved) {
availableTerminal.cwdResolved.resolve()
}
// Either resolve immediately if CWD already updated or wait for event/timeout
if (this.isCwdMatchingExpected(availableTerminal)) {
if (availableTerminal.cwdResolved) {
availableTerminal.cwdResolved.resolve()
}
availableTerminal.pendingCwdChange = undefined
availableTerminal.cwdResolved = undefined
} else {
try {
// Wait with a timeout for state change event to resolve
await Promise.race([
cwdPromise,
new Promise<void>((_, reject) =>
setTimeout(() => reject(new Error(`CWD timeout: Failed to update to ${cwd}`)), 1000),
),
])
} catch (err) {
// Clear pending state on timeout
availableTerminal.pendingCwdChange = undefined
availableTerminal.cwdResolved = undefined
} else {
try {
// Wait with a timeout for state change event to resolve
await Promise.race([
cwdPromise,
new Promise<void>((_, reject) =>
setTimeout(() => reject(new Error(`CWD timeout: Failed to update to ${cwd}`)), 1000),
),
])
} catch (err) {
// Clear pending state on timeout
availableTerminal.pendingCwdChange = undefined
availableTerminal.cwdResolved = undefined
}
}
this.terminalIds.add(availableTerminal.id)
return availableTerminal
}
this.terminalIds.add(availableTerminal.id)
return availableTerminal
}
// If all terminals are busy, create a new one
@@ -314,8 +311,4 @@ export class TerminalManager {
setShellIntegrationTimeout(timeout: number): void {
this.shellIntegrationTimeout = timeout
}
setTerminalReuseEnabled(enabled: boolean): void {
this.terminalReuseEnabled = enabled
}
}
+1 -12
View File
@@ -10,14 +10,6 @@ class WorkspaceTracker {
private disposables: vscode.Disposable[] = []
private filePaths: Set<string> = new Set()
private get activeFiles() {
return new Set(
vscode.window.tabGroups.activeTabGroup.tabs
.filter((tab) => tab.input instanceof vscode.TabInputText)
.map((tab) => (tab.input as vscode.TabInputText).uri.fsPath),
)
}
constructor(private readonly postMessageToWebview: (message: ExtensionMessage) => Promise<void>) {
this.postMessageToWebview = postMessageToWebview
this.registerListeners()
@@ -44,9 +36,6 @@ class WorkspaceTracker {
// Listen for file renaming
this.disposables.push(vscode.workspace.onDidRenameFiles(this.onFilesRenamed.bind(this)))
// Listen for tab groups changes
this.disposables.push(vscode.window.tabGroups.onDidChangeTabs(this.workspaceDidUpdate.bind(this)))
/*
An event that is emitted when a workspace folder is added or removed.
**Note:** this event will not fire if the first workspace folder is added, removed or changed,
@@ -97,7 +86,7 @@ class WorkspaceTracker {
}
this.postMessageToWebview({
type: "workspaceUpdated",
filePaths: Array.from(new Set([...this.activeFiles, ...this.filePaths])).map((file) => {
filePaths: Array.from(this.filePaths).map((file) => {
const relativePath = path.relative(cwd, file).toPosix()
return file.endsWith("/") ? relativePath + "/" : relativePath
}),
+13 -39
View File
@@ -17,9 +17,6 @@ import * as fs from "fs/promises"
import * as path from "path"
import * as vscode from "vscode"
import { z } from "zod"
import { WatchServiceClient } from "../../standalone/services/host-grpc-client"
import { FileChangeEvent_ChangeType, SubscribeToFileRequest } from "../../shared/proto/host/watch"
import { Metadata } from "../../shared/proto/common"
import {
DEFAULT_MCP_TIMEOUT_SECONDS,
McpMode,
@@ -121,45 +118,22 @@ export class McpHub {
private async watchMcpSettingsFile(): Promise<void> {
const settingsPath = await this.getMcpSettingsFilePath()
// Subscribe to file changes using the gRPC WatchService
console.log("[DEBUG] subscribing to mcp file changes")
const cancelSubscription = WatchServiceClient.subscribeToFile(
SubscribeToFileRequest.create({
metadata: Metadata.create({}),
path: settingsPath,
}),
{
onResponse: async (response) => {
console.log(
`[DEBUG] MCP settings ${response.type === FileChangeEvent_ChangeType.CHANGED ? "changed" : "event"}`,
)
// Only process the file if it was changed (not created or deleted)
if (response.type === FileChangeEvent_ChangeType.CHANGED) {
const settings = await this.readAndValidateMcpSettingsFile()
if (settings) {
try {
vscode.window.showInformationMessage("Updating MCP servers...")
await this.updateServerConnections(settings.mcpServers)
vscode.window.showInformationMessage("MCP servers updated")
} catch (error) {
console.error("Failed to process MCP settings change:", error)
}
this.disposables.push(
vscode.workspace.onDidSaveTextDocument(async (document) => {
if (arePathsEqual(document.uri.fsPath, settingsPath)) {
const settings = await this.readAndValidateMcpSettingsFile()
if (settings) {
try {
vscode.window.showInformationMessage("Updating MCP servers...")
await this.updateServerConnections(settings.mcpServers)
vscode.window.showInformationMessage("MCP servers updated")
} catch (error) {
console.error("Failed to process MCP settings change:", error)
}
}
},
onError: (error) => {
console.error("Error watching MCP settings file:", error)
},
onComplete: () => {
console.log("[DEBUG] MCP settings file watch completed")
},
},
}
}),
)
// Add the cancellation function to disposables
this.disposables.push({ dispose: cancelSubscription })
}
private async initializeMcpServers(): Promise<void> {
+32 -38
View File
@@ -37,28 +37,25 @@ function createToolCallTracker(webviewProvider: WebviewProvider): {
// Intercept messages to track tool usage
const originalPostMessageToWebview = webviewProvider.controller.postMessageToWebview
webviewProvider.controller.postMessageToWebview = async (message: ExtensionMessage) => {
// NOTE: Tool tracking via partialMessage has been migrated to gRPC streaming
// This interceptor is kept for potential future use with other message types
// Track tool calls
if (message.type === "partialMessage" && message.partialMessage?.say === "tool") {
const toolName = (message.partialMessage.text as any)?.tool
if (toolName) {
tracker.toolCalls[toolName] = (tracker.toolCalls[toolName] || 0) + 1
}
}
// Track tool calls - commented out as partialMessage is now handled via gRPC
// if (message.type === "partialMessage" && message.partialMessage?.say === "tool") {
// const toolName = (message.partialMessage.text as any)?.tool
// if (toolName) {
// tracker.toolCalls[toolName] = (tracker.toolCalls[toolName] || 0) + 1
// }
// }
// Track tool failures - commented out as partialMessage is now handled via gRPC
// if (message.type === "partialMessage" && message.partialMessage?.say === "error") {
// const errorText = message.partialMessage.text
// if (errorText && errorText.includes("Error executing tool")) {
// const match = errorText.match(/Error executing tool: (\w+)/)
// if (match && match[1]) {
// const toolName = match[1]
// tracker.toolFailures[toolName] = (tracker.toolFailures[toolName] || 0) + 1
// }
// }
// }
// Track tool failures
if (message.type === "partialMessage" && message.partialMessage?.say === "error") {
const errorText = message.partialMessage.text
if (errorText && errorText.includes("Error executing tool")) {
const match = errorText.match(/Error executing tool: (\w+)/)
if (match && match[1]) {
const toolName = match[1]
tracker.toolFailures[toolName] = (tracker.toolFailures[toolName] || 0) + 1
}
}
}
return originalPostMessageToWebview.call(webviewProvider.controller, message)
}
@@ -507,25 +504,22 @@ export function createMessageCatcher(webviewProvider: WebviewProvider): vscode.D
// Intercept outgoing messages from extension to webview
webviewProvider.controller.postMessageToWebview = async (message: ExtensionMessage) => {
// NOTE: Completion and ask message detection has been migrated to gRPC streaming
// This interceptor is kept for potential future use with other message types
// Check for completion_result message
if (message.type === "partialMessage" && message.partialMessage?.say === "completion_result") {
// Complete the current task
completeTask()
}
// Check for completion_result message - commented out as partialMessage is now handled via gRPC
// if (message.type === "partialMessage" && message.partialMessage?.say === "completion_result") {
// // Complete the current task
// completeTask()
// }
// Check for ask messages that require user intervention
if (message.type === "partialMessage" && message.partialMessage?.type === "ask" && !message.partialMessage.partial) {
const askType = message.partialMessage.ask as ClineAsk
const askText = message.partialMessage.text
// Check for ask messages that require user intervention - commented out as partialMessage is now handled via gRPC
// if (message.type === "partialMessage" && message.partialMessage?.type === "ask" && !message.partialMessage.partial) {
// const askType = message.partialMessage.ask as ClineAsk
// const askText = message.partialMessage.text
// // Automatically respond to different types of asks
// setTimeout(async () => {
// await autoRespondToAsk(webviewProvider, askType, askText)
// }, 100) // Small delay to ensure the message is processed first
// }
// Automatically respond to different types of asks
setTimeout(async () => {
await autoRespondToAsk(webviewProvider, askType, askText)
}, 100) // Small delay to ensure the message is processed first
}
return originalPostMessageToWebview.call(webviewProvider.controller, message)
}
+7 -2
View File
@@ -19,11 +19,15 @@ export interface ExtensionMessage {
| "selectedImages"
| "ollamaModels"
| "lmStudioModels"
| "theme"
| "workspaceUpdated"
| "partialMessage"
| "openRouterModels"
| "openAiModels"
| "requestyModels"
| "mcpServers"
| "relinquishControl"
| "mcpMarketplaceCatalog"
| "mcpDownloadDetails"
| "commitSearchResults"
| "openGraphData"
@@ -34,7 +38,7 @@ export interface ExtensionMessage {
| "fileSearchResults"
| "grpc_response" // New type for gRPC responses
text?: string
action?: "didBecomeVisible" | "accountLogoutClicked" | "focusChatInput"
action?: "settingsButtonClicked" | "didBecomeVisible" | "accountLogoutClicked" | "focusChatInput"
state?: ExtensionState
images?: string[]
files?: string[]
@@ -42,6 +46,8 @@ export interface ExtensionMessage {
lmStudioModels?: string[]
vsCodeLmModels?: { vendor?: string; family?: string; version?: string; id?: string }[]
filePaths?: string[]
partialMessage?: ClineMessage
openRouterModels?: Record<string, ModelInfo>
openAiModels?: string[]
requestyModels?: Record<string, ModelInfo>
mcpServers?: McpServer[]
@@ -108,7 +114,6 @@ export interface ExtensionState {
taskHistory: HistoryItem[]
telemetrySetting: TelemetrySetting
shellIntegrationTimeout: number
terminalReuseEnabled?: boolean
uriScheme?: string
userInfo?: {
displayName: string | null
-1
View File
@@ -80,7 +80,6 @@ export interface WebviewMessage {
offset?: number
shellIntegrationTimeout?: number
terminalReuseEnabled?: boolean
}
export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse"
+14 -334
View File
@@ -575,30 +575,6 @@ export const vertexModels = {
},
],
},
"gemini-2.5-pro-preview-06-05": {
maxTokens: 65536,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: true,
supportsGlobalEndpoint: true,
inputPrice: 2.5,
outputPrice: 15,
cacheReadsPrice: 0.31,
tiers: [
{
contextWindow: 200000,
inputPrice: 1.25,
outputPrice: 10,
cacheReadsPrice: 0.31,
},
{
contextWindow: Infinity,
inputPrice: 2.5,
outputPrice: 15,
cacheReadsPrice: 0.625,
},
],
},
"gemini-2.5-flash-preview-04-17": {
maxTokens: 65536,
contextWindow: 1_048_576,
@@ -743,30 +719,6 @@ export const geminiModels = {
},
],
},
"gemini-2.5-pro-preview-06-05": {
maxTokens: 65536,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: true,
supportsGlobalEndpoint: true,
inputPrice: 2.5,
outputPrice: 15,
cacheReadsPrice: 0.31,
tiers: [
{
contextWindow: 200000,
inputPrice: 1.25,
outputPrice: 10,
cacheReadsPrice: 0.31,
},
{
contextWindow: Infinity,
inputPrice: 2.5,
outputPrice: 15,
cacheReadsPrice: 0.625,
},
],
},
"gemini-2.5-flash-preview-05-20": {
maxTokens: 65536,
contextWindow: 1_048_576,
@@ -1069,118 +1021,6 @@ export type InternationalQwenModelId = keyof typeof internationalQwenModels
export const internationalQwenDefaultModelId: InternationalQwenModelId = "qwen-coder-plus-latest"
export const mainlandQwenDefaultModelId: MainlandQwenModelId = "qwen-coder-plus-latest"
export const internationalQwenModels = {
"qwen3-235b-a22b": {
maxTokens: 16_384,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 2,
outputPrice: 8,
cacheWritesPrice: 2,
cacheReadsPrice: 8,
thinkingConfig: {
maxBudget: 38_912,
outputPrice: 20,
},
},
"qwen3-32b": {
maxTokens: 16_384,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 2,
outputPrice: 8,
cacheWritesPrice: 2,
cacheReadsPrice: 8,
thinkingConfig: {
maxBudget: 38_912,
outputPrice: 20,
},
},
"qwen3-30b-a3b": {
maxTokens: 16_384,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.75,
outputPrice: 3,
cacheWritesPrice: 0.75,
cacheReadsPrice: 3,
thinkingConfig: {
maxBudget: 38_912,
outputPrice: 7.5,
},
},
"qwen3-14b": {
maxTokens: 8_192,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 1,
outputPrice: 4,
cacheWritesPrice: 1,
cacheReadsPrice: 4,
thinkingConfig: {
maxBudget: 38_912,
outputPrice: 10,
},
},
"qwen3-8b": {
maxTokens: 8_192,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.5,
outputPrice: 2,
cacheWritesPrice: 0.5,
cacheReadsPrice: 2,
thinkingConfig: {
maxBudget: 38_912,
outputPrice: 5,
},
},
"qwen3-4b": {
maxTokens: 8_192,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.3,
outputPrice: 1.2,
cacheWritesPrice: 0.3,
cacheReadsPrice: 1.2,
thinkingConfig: {
maxBudget: 38_912,
outputPrice: 3,
},
},
"qwen3-1.7b": {
maxTokens: 8_192,
contextWindow: 32_768,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.3,
outputPrice: 1.2,
cacheWritesPrice: 0.3,
cacheReadsPrice: 1.2,
thinkingConfig: {
maxBudget: 30_720,
outputPrice: 3,
},
},
"qwen3-0.6b": {
maxTokens: 8_192,
contextWindow: 32_768,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.3,
outputPrice: 1.2,
cacheWritesPrice: 0.3,
cacheReadsPrice: 1.2,
thinkingConfig: {
maxBudget: 30_720,
outputPrice: 3,
},
},
"qwen2.5-coder-32b-instruct": {
maxTokens: 8_192,
contextWindow: 131_072,
@@ -1252,32 +1092,24 @@ export const internationalQwenModels = {
cacheReadsPrice: 7,
},
"qwen-plus-latest": {
maxTokens: 16_384,
maxTokens: 129_024,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.8,
outputPrice: 2,
cacheWritesPrice: 0.8,
cacheReadsPrice: 2,
thinkingConfig: {
maxBudget: 38_912,
outputPrice: 16,
},
cacheReadsPrice: 0.2,
},
"qwen-turbo-latest": {
maxTokens: 16_384,
maxTokens: 1_000_000,
contextWindow: 1_000_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.3,
outputPrice: 0.6,
cacheWritesPrice: 0.3,
cacheReadsPrice: 0.6,
thinkingConfig: {
maxBudget: 38_912,
outputPrice: 6,
},
inputPrice: 0.8,
outputPrice: 2,
cacheWritesPrice: 0.8,
cacheReadsPrice: 2,
},
"qwen-max-latest": {
maxTokens: 30_720,
@@ -1392,118 +1224,6 @@ export const internationalQwenModels = {
} as const satisfies Record<string, ModelInfo>
export const mainlandQwenModels = {
"qwen3-235b-a22b": {
maxTokens: 16_384,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 2,
outputPrice: 8,
cacheWritesPrice: 2,
cacheReadsPrice: 8,
thinkingConfig: {
maxBudget: 38_912,
outputPrice: 20,
},
},
"qwen3-32b": {
maxTokens: 16_384,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 2,
outputPrice: 8,
cacheWritesPrice: 2,
cacheReadsPrice: 8,
thinkingConfig: {
maxBudget: 38_912,
outputPrice: 20,
},
},
"qwen3-30b-a3b": {
maxTokens: 16_384,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.75,
outputPrice: 3,
cacheWritesPrice: 0.75,
cacheReadsPrice: 3,
thinkingConfig: {
maxBudget: 38_912,
outputPrice: 7.5,
},
},
"qwen3-14b": {
maxTokens: 8_192,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 1,
outputPrice: 4,
cacheWritesPrice: 1,
cacheReadsPrice: 4,
thinkingConfig: {
maxBudget: 38_912,
outputPrice: 10,
},
},
"qwen3-8b": {
maxTokens: 8_192,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.5,
outputPrice: 2,
cacheWritesPrice: 0.5,
cacheReadsPrice: 2,
thinkingConfig: {
maxBudget: 38_912,
outputPrice: 5,
},
},
"qwen3-4b": {
maxTokens: 8_192,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.3,
outputPrice: 1.2,
cacheWritesPrice: 0.3,
cacheReadsPrice: 1.2,
thinkingConfig: {
maxBudget: 38_912,
outputPrice: 3,
},
},
"qwen3-1.7b": {
maxTokens: 8_192,
contextWindow: 32_768,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.3,
outputPrice: 1.2,
cacheWritesPrice: 0.3,
cacheReadsPrice: 1.2,
thinkingConfig: {
maxBudget: 30_720,
outputPrice: 3,
},
},
"qwen3-0.6b": {
maxTokens: 8_192,
contextWindow: 32_768,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.3,
outputPrice: 1.2,
cacheWritesPrice: 0.3,
cacheReadsPrice: 1.2,
thinkingConfig: {
maxBudget: 30_720,
outputPrice: 3,
},
},
"qwen2.5-coder-32b-instruct": {
maxTokens: 8_192,
contextWindow: 131_072,
@@ -1575,32 +1295,24 @@ export const mainlandQwenModels = {
cacheReadsPrice: 7,
},
"qwen-plus-latest": {
maxTokens: 16_384,
maxTokens: 129_024,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.8,
outputPrice: 2,
cacheWritesPrice: 0.8,
cacheReadsPrice: 2,
thinkingConfig: {
maxBudget: 38_912,
outputPrice: 16,
},
cacheReadsPrice: 0.2,
},
"qwen-turbo-latest": {
maxTokens: 16_384,
maxTokens: 1_000_000,
contextWindow: 1_000_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.3,
outputPrice: 0.6,
cacheWritesPrice: 0.3,
cacheReadsPrice: 0.6,
thinkingConfig: {
maxBudget: 38_912,
outputPrice: 6,
},
inputPrice: 0.8,
outputPrice: 2,
cacheWritesPrice: 0.8,
cacheReadsPrice: 2,
},
"qwen-max-latest": {
maxTokens: 30_720,
@@ -1927,14 +1639,6 @@ export const askSageModels = {
inputPrice: 0,
outputPrice: 0,
},
"gpt-4.1": {
maxTokens: 32_768,
contextWindow: 1_047_576,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"claude-35-sonnet": {
maxTokens: 8192,
contextWindow: 200_000,
@@ -1959,30 +1663,6 @@ export const askSageModels = {
inputPrice: 0,
outputPrice: 0,
},
"claude-4-sonnet": {
maxTokens: 8192,
contextWindow: 200_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"claude-4-opus": {
maxTokens: 8192,
contextWindow: 200_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"google-gemini-2.5-pro": {
maxTokens: 65536,
contextWindow: 1_048_576,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
}
// Nebius AI Studio
@@ -1,253 +0,0 @@
import { ClineMessage as AppClineMessage, ClineAsk as AppClineAsk, ClineSay as AppClineSay } from "@shared/ExtensionMessage"
import { ClineMessage as ProtoClineMessage, ClineMessageType, ClineAsk, ClineSay } from "@shared/proto/ui"
// Helper function to convert ClineAsk string to enum
function convertClineAskToProtoEnum(ask: AppClineAsk | undefined): ClineAsk | undefined {
if (!ask) {
return undefined
}
const mapping: Record<AppClineAsk, ClineAsk> = {
followup: ClineAsk.FOLLOWUP,
plan_mode_respond: ClineAsk.PLAN_MODE_RESPOND,
command: ClineAsk.COMMAND,
command_output: ClineAsk.COMMAND_OUTPUT,
completion_result: ClineAsk.COMPLETION_RESULT,
tool: ClineAsk.TOOL,
api_req_failed: ClineAsk.API_REQ_FAILED,
resume_task: ClineAsk.RESUME_TASK,
resume_completed_task: ClineAsk.RESUME_COMPLETED_TASK,
mistake_limit_reached: ClineAsk.MISTAKE_LIMIT_REACHED,
auto_approval_max_req_reached: ClineAsk.AUTO_APPROVAL_MAX_REQ_REACHED,
browser_action_launch: ClineAsk.BROWSER_ACTION_LAUNCH,
use_mcp_server: ClineAsk.USE_MCP_SERVER,
new_task: ClineAsk.NEW_TASK,
condense: ClineAsk.CONDENSE,
report_bug: ClineAsk.REPORT_BUG,
}
const result = mapping[ask]
if (result === undefined) {
console.warn(`Unknown ClineAsk value: ${ask}`)
}
return result
}
// Helper function to convert ClineAsk enum to string
function convertProtoEnumToClineAsk(ask: ClineAsk): AppClineAsk | undefined {
if (ask === ClineAsk.UNRECOGNIZED) {
console.warn("Received UNRECOGNIZED ClineAsk enum value")
return undefined
}
const mapping: Record<Exclude<ClineAsk, ClineAsk.UNRECOGNIZED>, AppClineAsk> = {
[ClineAsk.FOLLOWUP]: "followup",
[ClineAsk.PLAN_MODE_RESPOND]: "plan_mode_respond",
[ClineAsk.COMMAND]: "command",
[ClineAsk.COMMAND_OUTPUT]: "command_output",
[ClineAsk.COMPLETION_RESULT]: "completion_result",
[ClineAsk.TOOL]: "tool",
[ClineAsk.API_REQ_FAILED]: "api_req_failed",
[ClineAsk.RESUME_TASK]: "resume_task",
[ClineAsk.RESUME_COMPLETED_TASK]: "resume_completed_task",
[ClineAsk.MISTAKE_LIMIT_REACHED]: "mistake_limit_reached",
[ClineAsk.AUTO_APPROVAL_MAX_REQ_REACHED]: "auto_approval_max_req_reached",
[ClineAsk.BROWSER_ACTION_LAUNCH]: "browser_action_launch",
[ClineAsk.USE_MCP_SERVER]: "use_mcp_server",
[ClineAsk.NEW_TASK]: "new_task",
[ClineAsk.CONDENSE]: "condense",
[ClineAsk.REPORT_BUG]: "report_bug",
}
return mapping[ask]
}
// Helper function to convert ClineSay string to enum
function convertClineSayToProtoEnum(say: AppClineSay | undefined): ClineSay | undefined {
if (!say) {
return undefined
}
const mapping: Record<AppClineSay, ClineSay> = {
task: ClineSay.TASK,
error: ClineSay.ERROR,
api_req_started: ClineSay.API_REQ_STARTED,
api_req_finished: ClineSay.API_REQ_FINISHED,
text: ClineSay.TEXT,
reasoning: ClineSay.REASONING,
completion_result: ClineSay.COMPLETION_RESULT_SAY,
user_feedback: ClineSay.USER_FEEDBACK,
user_feedback_diff: ClineSay.USER_FEEDBACK_DIFF,
api_req_retried: ClineSay.API_REQ_RETRIED,
command: ClineSay.COMMAND_SAY,
command_output: ClineSay.COMMAND_OUTPUT_SAY,
tool: ClineSay.TOOL_SAY,
shell_integration_warning: ClineSay.SHELL_INTEGRATION_WARNING,
browser_action_launch: ClineSay.BROWSER_ACTION_LAUNCH_SAY,
browser_action: ClineSay.BROWSER_ACTION,
browser_action_result: ClineSay.BROWSER_ACTION_RESULT,
mcp_server_request_started: ClineSay.MCP_SERVER_REQUEST_STARTED,
mcp_server_response: ClineSay.MCP_SERVER_RESPONSE,
use_mcp_server: ClineSay.USE_MCP_SERVER_SAY,
diff_error: ClineSay.DIFF_ERROR,
deleted_api_reqs: ClineSay.DELETED_API_REQS,
clineignore_error: ClineSay.CLINEIGNORE_ERROR,
checkpoint_created: ClineSay.CHECKPOINT_CREATED,
load_mcp_documentation: ClineSay.LOAD_MCP_DOCUMENTATION,
info: ClineSay.INFO,
}
const result = mapping[say]
if (result === undefined) {
console.warn(`Unknown ClineSay value: ${say}`)
}
return result
}
// Helper function to convert ClineSay enum to string
function convertProtoEnumToClineSay(say: ClineSay): AppClineSay | undefined {
if (say === ClineSay.UNRECOGNIZED) {
console.warn("Received UNRECOGNIZED ClineSay enum value")
return undefined
}
const mapping: Record<Exclude<ClineSay, ClineSay.UNRECOGNIZED>, AppClineSay> = {
[ClineSay.TASK]: "task",
[ClineSay.ERROR]: "error",
[ClineSay.API_REQ_STARTED]: "api_req_started",
[ClineSay.API_REQ_FINISHED]: "api_req_finished",
[ClineSay.TEXT]: "text",
[ClineSay.REASONING]: "reasoning",
[ClineSay.COMPLETION_RESULT_SAY]: "completion_result",
[ClineSay.USER_FEEDBACK]: "user_feedback",
[ClineSay.USER_FEEDBACK_DIFF]: "user_feedback_diff",
[ClineSay.API_REQ_RETRIED]: "api_req_retried",
[ClineSay.COMMAND_SAY]: "command",
[ClineSay.COMMAND_OUTPUT_SAY]: "command_output",
[ClineSay.TOOL_SAY]: "tool",
[ClineSay.SHELL_INTEGRATION_WARNING]: "shell_integration_warning",
[ClineSay.BROWSER_ACTION_LAUNCH_SAY]: "browser_action_launch",
[ClineSay.BROWSER_ACTION]: "browser_action",
[ClineSay.BROWSER_ACTION_RESULT]: "browser_action_result",
[ClineSay.MCP_SERVER_REQUEST_STARTED]: "mcp_server_request_started",
[ClineSay.MCP_SERVER_RESPONSE]: "mcp_server_response",
[ClineSay.USE_MCP_SERVER_SAY]: "use_mcp_server",
[ClineSay.DIFF_ERROR]: "diff_error",
[ClineSay.DELETED_API_REQS]: "deleted_api_reqs",
[ClineSay.CLINEIGNORE_ERROR]: "clineignore_error",
[ClineSay.CHECKPOINT_CREATED]: "checkpoint_created",
[ClineSay.LOAD_MCP_DOCUMENTATION]: "load_mcp_documentation",
[ClineSay.INFO]: "info",
}
return mapping[say]
}
/**
* Convert application ClineMessage to proto ClineMessage
*/
export function convertClineMessageToProto(message: AppClineMessage): ProtoClineMessage {
// For sending messages, we need to provide values for required proto fields
const askEnum = message.ask ? convertClineAskToProtoEnum(message.ask) : undefined
const sayEnum = message.say ? convertClineSayToProtoEnum(message.say) : undefined
// Determine appropriate enum values based on message type
let finalAskEnum: ClineAsk = ClineAsk.FOLLOWUP // Proto default
let finalSayEnum: ClineSay = ClineSay.TEXT // Proto default
if (message.type === "ask") {
finalAskEnum = askEnum ?? ClineAsk.FOLLOWUP // Use FOLLOWUP as default for ask messages
} else if (message.type === "say") {
finalSayEnum = sayEnum ?? ClineSay.TEXT // Use TEXT as default for say messages
}
const protoMessage: ProtoClineMessage = {
ts: message.ts,
type: message.type === "ask" ? ClineMessageType.ASK : ClineMessageType.SAY,
ask: finalAskEnum,
say: finalSayEnum,
text: message.text ?? "",
reasoning: message.reasoning ?? "",
images: message.images ?? [],
files: message.files ?? [],
partial: message.partial ?? false,
lastCheckpointHash: message.lastCheckpointHash ?? "",
isCheckpointCheckedOut: message.isCheckpointCheckedOut ?? false,
isOperationOutsideWorkspace: message.isOperationOutsideWorkspace ?? false,
conversationHistoryIndex: message.conversationHistoryIndex ?? 0,
conversationHistoryDeletedRange: message.conversationHistoryDeletedRange
? {
startIndex: message.conversationHistoryDeletedRange[0],
endIndex: message.conversationHistoryDeletedRange[1],
}
: undefined,
}
return protoMessage
}
/**
* Convert proto ClineMessage to application ClineMessage
*/
export function convertProtoToClineMessage(protoMessage: ProtoClineMessage): AppClineMessage {
const message: AppClineMessage = {
ts: protoMessage.ts,
type: protoMessage.type === ClineMessageType.ASK ? "ask" : "say",
}
// Convert ask enum to string
if (protoMessage.type === ClineMessageType.ASK) {
const ask = convertProtoEnumToClineAsk(protoMessage.ask)
if (ask !== undefined) {
message.ask = ask
}
}
// Convert say enum to string
if (protoMessage.type === ClineMessageType.SAY) {
const say = convertProtoEnumToClineSay(protoMessage.say)
if (say !== undefined) {
message.say = say
}
}
// Convert other fields - preserve empty strings as they may be intentional
if (protoMessage.text !== "") {
message.text = protoMessage.text
}
if (protoMessage.reasoning !== "") {
message.reasoning = protoMessage.reasoning
}
if (protoMessage.images.length > 0) {
message.images = protoMessage.images
}
if (protoMessage.files.length > 0) {
message.files = protoMessage.files
}
if (protoMessage.partial) {
message.partial = protoMessage.partial
}
if (protoMessage.lastCheckpointHash !== "") {
message.lastCheckpointHash = protoMessage.lastCheckpointHash
}
if (protoMessage.isCheckpointCheckedOut) {
message.isCheckpointCheckedOut = protoMessage.isCheckpointCheckedOut
}
if (protoMessage.isOperationOutsideWorkspace) {
message.isOperationOutsideWorkspace = protoMessage.isOperationOutsideWorkspace
}
if (protoMessage.conversationHistoryIndex !== 0) {
message.conversationHistoryIndex = protoMessage.conversationHistoryIndex
}
// Convert conversationHistoryDeletedRange from object to tuple
if (protoMessage.conversationHistoryDeletedRange) {
message.conversationHistoryDeletedRange = [
protoMessage.conversationHistoryDeletedRange.startIndex,
protoMessage.conversationHistoryDeletedRange.endIndex,
]
}
return message
}
@@ -1,106 +0,0 @@
import { v4 as uuidv4 } from "uuid"
import { GrpcHandler, StreamingCallbacks } from "../../../hosts/vscode/host-grpc-handler"
// Generic type for any protobuf service definition
export type ProtoService = {
name: string
fullName: string
methods: {
[key: string]: {
name: string
requestType: any
responseType: any
requestStream: boolean
responseStream: boolean
options: any
}
}
}
// Define a unified client type that handles both unary and streaming methods
export type GrpcClientType<T extends ProtoService> = {
[K in keyof T["methods"]]: T["methods"][K]["responseStream"] extends true
? (
request: InstanceType<T["methods"][K]["requestType"]>,
options: StreamingCallbacks<InstanceType<T["methods"][K]["responseType"]>>,
) => () => void // Returns a cancel function
: (request: InstanceType<T["methods"][K]["requestType"]>) => Promise<InstanceType<T["methods"][K]["responseType"]>>
}
// Create a client for any protobuf service with inferred types
export function createGrpcClient<T extends ProtoService>(service: T): GrpcClientType<T> {
const client = {} as GrpcClientType<T>
const grpcHandler = new GrpcHandler()
Object.values(service.methods).forEach((method) => {
// Streaming method implementation
if (method.responseStream) {
// Use lowercase method name as the key in the client object
const methodKey = method.name.charAt(0).toLowerCase() + method.name.slice(1)
client[methodKey as keyof GrpcClientType<T>] = ((
request: any,
options: StreamingCallbacks<InstanceType<typeof method.responseType>>,
) => {
// Use handleRequest with streaming callbacks
const requestId = uuidv4()
console.log(`[DEBUG] Streaming gRPC host call to ${service.fullName}.${methodKey} req:${requestId}`)
// We need to await the promise and then return the cancel function
return (async () => {
try {
const result = await grpcHandler.handleRequest<InstanceType<typeof method.responseType>>(
service.fullName,
methodKey,
request,
requestId,
options,
)
// If the result is a function, it's the cancel function
if (typeof result === "function") {
return result
} else {
// This shouldn't happen, but just in case
console.error(`Expected cancel function but got response object for streaming request: ${requestId}`)
return () => {}
}
} catch (error) {
console.error(`Error in streaming request: ${error}`)
if (options.onError) {
options.onError(error instanceof Error ? error : new Error(String(error)))
}
return () => {}
}
})()
}) as any
} else {
// Unary method implementation
const methodKey = method.name.charAt(0).toLowerCase() + method.name.slice(1)
client[methodKey as keyof GrpcClientType<T>] = ((request: any) => {
return new Promise(async (resolve, reject) => {
const requestId = uuidv4()
console.log(`[DEBUG] gRPC host call to ${service.fullName}.${methodKey} req:${requestId}`)
try {
const response = await grpcHandler.handleRequest(service.fullName, methodKey, request, requestId)
console.log(`[DEBUG] gRPC host resp to ${service.fullName}.${methodKey} req:${requestId}`)
// Check if the response is a function (streaming) or an object (unary)
if (typeof response === "function") {
// This shouldn't happen for unary requests
throw new Error("Received streaming response for unary request")
} else if (response && response.message) {
resolve(response.message)
} else {
throw new Error("gRPC response didn't have a message")
}
} catch (e) {
console.log(`[DEBUG] gRPC host ERR to ${service.fullName}.${methodKey} req:${requestId} err:${e}`)
reject(e)
}
})
}) as any
}
})
return client
}
-7
View File
@@ -1,7 +0,0 @@
# Beyond Autocomplete: True Agentic Planning
**Cline analyzes your request, explores your code, and presents a clear plan.**
Watch Cline break down complex tasks, ask clarifying questions, and outline its approach. Understand the 'why' before any code is written, ensuring changes align with your architecture and intent.
![Cline planning demonstration](https://storage.googleapis.com/cline_public_images/docs/assets/cline-plan-hifi-1_compress.webp)
-7
View File
@@ -1,7 +0,0 @@
# Deep Codebase Intelligence
**Cline starts with broad context and explores deeply where needed.**
Cline is designed with inherent codebase intelligence. It doesn't operate in a vacuum, but starts with a structural understanding of your project. Before making changes, it performs targeted agentic exploration to gain any additional specific context required, ensuring its actions are always well-informed and aligned with your architecture.
![Cline Deep Codebase Intelligence Demo](https://storage.googleapis.com/cline_public_images/docs/assets/cline-reading-codebase-hifi-2_compress.webp)
-7
View File
@@ -1,7 +0,0 @@
# Always Use the Best Models
**Connect your keys for Anthropic (Claude), Google (Gemini), OpenAI (GPT), and other leading LLMs.**
Cline puts you at the forefront of AI. Bring your own API keys for leading models like Anthropic (Claude), Google (Gemini), and OpenAI (GPT). Always leverage the most powerful State-of-the-Art (SOTA) capabilities, ensuring you control both cost and cutting-edge performance.
![Cline Models Demo](https://storage.googleapis.com/cline_public_images/docs/assets/clines-models-hifi-3_compress.webp)
-7
View File
@@ -1,7 +0,0 @@
# Unlock Specialized Capabilities with MCP
**The Model Context Protocol (MCP) connects Cline to a world of powerful tools.**
Go beyond local code. With the Model Context Protocol (MCP), Cline accesses vital context from external datasources like databases and APIs. It can interact with these platforms and leverage a growing marketplace of specialized, secure tools to tackle complex, real-world development tasks.
![Cline MCP Servers Demo](https://storage.googleapis.com/cline_public_images/docs/assets/clines-mcp-servers-4_compress.webp)
-7
View File
@@ -1,7 +0,0 @@
# No Black Box: Full Visibility & Control
**Cline operates with complete transparency, showing you every file read and every proposed diff.**
Understand exactly what Cline is doing and why—no obfuscation. Review all actions and approve changes before they're made. Cline uses checkpoints, allowing you to easily revert if needed, maintaining full control over your codebase. With BYO-key, you also have clear cost transparency.
![Cline Transparency Demo](https://storage.googleapis.com/cline_public_images/docs/assets/clines-transparency-hifi-5_compress.webp)
@@ -14,7 +14,8 @@ import { vscode } from "@/utils/vscode"
import McpMarketplaceCard from "./McpMarketplaceCard"
import McpSubmitCard from "./McpSubmitCard"
const McpMarketplaceView = () => {
const { mcpServers, mcpMarketplaceCatalog } = useExtensionState()
const { mcpServers } = useExtensionState()
const [items, setItems] = useState<McpMarketplaceItem[]>([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [isRefreshing, setIsRefreshing] = useState(false)
@@ -22,8 +23,6 @@ const McpMarketplaceView = () => {
const [selectedCategory, setSelectedCategory] = useState<string | null>(null)
const [sortBy, setSortBy] = useState<"newest" | "stars" | "name" | "downloadCount">("downloadCount")
const items = mcpMarketplaceCatalog?.items || []
const categories = useMemo(() => {
const uniqueCategories = new Set(items.map((item) => item.category))
return Array.from(uniqueCategories).sort()
@@ -59,7 +58,16 @@ const McpMarketplaceView = () => {
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
const message = event.data
if (message.type === "mcpDownloadDetails") {
if (message.type === "mcpMarketplaceCatalog") {
if (message.error) {
setError(message.error)
} else {
setItems(message.mcpMarketplaceCatalog?.items || [])
setError(null)
}
setIsLoading(false)
setIsRefreshing(false)
} else if (message.type === "mcpDownloadDetails") {
if (message.error) {
setError(message.error)
}
@@ -68,7 +76,7 @@ const McpMarketplaceView = () => {
window.addEventListener("message", handleMessage)
// Fetch marketplace catalog on initial load
// Fetch marketplace catalog
fetchMarketplace()
return () => {
@@ -76,15 +84,6 @@ const McpMarketplaceView = () => {
}
}, [])
useEffect(() => {
// Update loading state when catalog arrives
if (mcpMarketplaceCatalog?.items) {
setIsLoading(false)
setIsRefreshing(false)
setError(null)
}
}, [mcpMarketplaceCatalog])
const fetchMarketplace = (forceRefresh: boolean = false) => {
if (forceRefresh) {
setIsRefreshing(true)
@@ -116,23 +116,6 @@ 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"],
qwen: [
"qwen3-235b-a22b",
"qwen3-32b",
"qwen3-30b-a3b",
"qwen3-14b",
"qwen3-8b",
"qwen3-4b",
"qwen3-1.7b",
"qwen3-0.6b",
"qwen-plus-latest",
"qwen-turbo-latest",
],
}
// This is necessary to ensure dropdown opens downward, important for when this is used in popup
const DROPDOWN_Z_INDEX = OPENROUTER_MODEL_PICKER_Z_INDEX + 2 // Higher than the OpenRouterModelPicker's and ModelSelectorTooltip's z-index
@@ -2195,13 +2178,25 @@ const ApiOptions = ({
{selectedProvider === "nebius" && createDropdown(nebiusModels)}
</DropdownContainer>
{SUPPORTED_THINKING_MODELS[selectedProvider]?.includes(selectedModelId) && (
<ThinkingBudgetSlider
apiConfiguration={apiConfiguration}
setApiConfiguration={setApiConfiguration}
maxBudget={selectedModelInfo.thinkingConfig?.maxBudget}
/>
)}
{selectedProvider === "anthropic" &&
(selectedModelId === "claude-3-7-sonnet-20250219" ||
selectedModelId === "claude-sonnet-4-20250514" ||
selectedModelId === "claude-opus-4-20250514") && (
<ThinkingBudgetSlider
apiConfiguration={apiConfiguration}
setApiConfiguration={setApiConfiguration}
/>
)}
{selectedProvider === "vertex" &&
(selectedModelId === "claude-3-7-sonnet@20250219" ||
selectedModelId === "claude-sonnet-4@20250514" ||
selectedModelId === "claude-opus-4@20250514") && (
<ThinkingBudgetSlider
apiConfiguration={apiConfiguration}
setApiConfiguration={setApiConfiguration}
/>
)}
{selectedProvider === "xai" && selectedModelId.includes("3-mini") && (
<>
@@ -126,10 +126,6 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
setEnableCheckpointsSetting,
mcpMarketplaceEnabled,
setMcpMarketplaceEnabled,
shellIntegrationTimeout,
setShellIntegrationTimeout,
terminalReuseEnabled,
setTerminalReuseEnabled,
setApiConfiguration,
} = useExtensionState()
@@ -142,8 +138,6 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
enableCheckpointsSetting,
mcpMarketplaceEnabled,
chatSettings,
shellIntegrationTimeout,
terminalReuseEnabled,
})
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
@@ -184,8 +178,6 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
telemetrySetting,
enableCheckpointsSetting,
mcpMarketplaceEnabled,
shellIntegrationTimeout,
terminalReuseEnabled,
apiConfiguration: apiConfigurationToSubmit,
})
@@ -208,9 +200,7 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
planActSeparateModelsSetting !== originalState.current.planActSeparateModelsSetting ||
enableCheckpointsSetting !== originalState.current.enableCheckpointsSetting ||
mcpMarketplaceEnabled !== originalState.current.mcpMarketplaceEnabled ||
JSON.stringify(chatSettings) !== JSON.stringify(originalState.current.chatSettings) ||
shellIntegrationTimeout !== originalState.current.shellIntegrationTimeout ||
terminalReuseEnabled !== originalState.current.terminalReuseEnabled
JSON.stringify(chatSettings) !== JSON.stringify(originalState.current.chatSettings)
setHasUnsavedChanges(hasChanges)
}, [
@@ -221,8 +211,6 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
enableCheckpointsSetting,
mcpMarketplaceEnabled,
chatSettings,
shellIntegrationTimeout,
terminalReuseEnabled,
])
// Handle cancel button click
@@ -253,13 +241,6 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
: false,
)
}
// Reset terminal settings
if (typeof setShellIntegrationTimeout === "function") {
setShellIntegrationTimeout(originalState.current.shellIntegrationTimeout)
}
if (typeof setTerminalReuseEnabled === "function") {
setTerminalReuseEnabled(originalState.current.terminalReuseEnabled ?? true)
}
// Close settings view
onDone()
}
@@ -1,12 +1,11 @@
import React, { useState } from "react"
import { VSCodeTextField, VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
import { VSCodeTextField } 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 } =
useExtensionState()
const { shellIntegrationTimeout, setShellIntegrationTimeout } = useExtensionState()
const [inputValue, setInputValue] = useState((shellIntegrationTimeout / 1000).toString())
const [inputError, setInputError] = useState<string | null>(null)
@@ -49,17 +48,6 @@ export const TerminalSettingsSection: React.FC = () => {
}
}
const handleTerminalReuseChange = (event: Event) => {
const target = event.target as HTMLInputElement
const checked = target.checked
// Update local state
setTerminalReuseEnabled(checked)
// TODO: Send to extension using gRPC when the backend is ready
// For now, we'll just update the local state
}
return (
<div id="terminal-settings-section" style={{ marginBottom: 20 }}>
<div style={{ marginBottom: 15 }}>
@@ -85,20 +73,6 @@ export const TerminalSettingsSection: React.FC = () => {
you experience terminal connection timeouts.
</p>
</div>
<div style={{ marginBottom: 15 }}>
<div style={{ display: "flex", alignItems: "center", marginBottom: 8 }}>
<VSCodeCheckbox
checked={terminalReuseEnabled ?? true}
onChange={(event) => handleTerminalReuseChange(event as Event)}>
Enable aggressive terminal reuse
</VSCodeCheckbox>
</div>
<p style={{ fontSize: "12px", color: "var(--vscode-descriptionForeground)", margin: 0 }}>
When enabled, Cline will reuse existing terminal windows that aren't in the current working directory. Disable
this if you experience issues with task lockout after a terminal command.
</p>
</div>
</div>
)
}
+46 -150
View File
@@ -1,8 +1,8 @@
import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from "react"
import { useEvent } from "react-use"
import { StateServiceClient, ModelsServiceClient, UiServiceClient } from "../services/grpc-client"
import { EmptyRequest } from "@shared/proto/common"
import { WebviewProviderType as WebviewProviderTypeEnum, WebviewProviderTypeRequest } from "@shared/proto/ui"
import { convertProtoToClineMessage } from "@shared/proto-conversions/cline-message"
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "@shared/AutoApprovalSettings"
import { DEFAULT_BROWSER_SETTINGS } from "@shared/BrowserSettings"
import { ChatSettings, DEFAULT_CHAT_SETTINGS } from "@shared/ChatSettings"
@@ -18,11 +18,8 @@ import {
requestyDefaultModelInfo,
} from "../../../src/shared/api"
import { McpMarketplaceCatalog, McpServer, McpViewTab } from "../../../src/shared/mcp"
import { ModelsServiceClient, StateServiceClient, UiServiceClient, McpServiceClient } from "../services/grpc-client"
import { convertTextMateToHljs } from "../utils/textMateToHljs"
import { convertOpenRouterCompatibleModelInfoToModelInfoRecord } from "../../../src/shared/proto-conversions/models/openrouter-models-conversion"
import { vscode } from "../utils/vscode"
import { OpenRouterCompatibleModelInfo } from "@shared/proto/models"
interface ExtensionStateContextType extends ExtensionState {
didHydrateState: boolean
@@ -54,7 +51,6 @@ interface ExtensionStateContextType extends ExtensionState {
setEnableCheckpointsSetting: (value: boolean) => void
setMcpMarketplaceEnabled: (value: boolean) => void
setShellIntegrationTimeout: (value: number) => void
setTerminalReuseEnabled: (value: boolean) => void
setChatSettings: (value: ChatSettings) => void
setMcpServers: (value: McpServer[]) => void
setGlobalClineRulesToggles: (toggles: Record<string, boolean>) => void
@@ -93,9 +89,6 @@ const ExtensionStateContext = createContext<ExtensionStateContextType | undefine
export const ExtensionStateContextProvider: React.FC<{
children: React.ReactNode
}> = ({ children }) => {
// Get the current webview provider type
const currentProviderType =
window.WEBVIEW_PROVIDER_TYPE === "sidebar" ? WebviewProviderTypeEnum.SIDEBAR : WebviewProviderTypeEnum.TAB
// UI view state
const [showMcp, setShowMcp] = useState(false)
const [mcpTab, setMcpTab] = useState<McpViewTab | undefined>(undefined)
@@ -178,7 +171,6 @@ export const ExtensionStateContextProvider: React.FC<{
localWorkflowToggles: {},
globalWorkflowToggles: {},
shellIntegrationTimeout: 4000, // default timeout for shell integration
terminalReuseEnabled: true, // default to enabled for backward compatibility
isNewUser: false,
})
const [didHydrateState, setDidHydrateState] = useState(false)
@@ -199,10 +191,47 @@ export const ExtensionStateContextProvider: React.FC<{
const handleMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data
switch (message.type) {
case "action": {
switch (message.action!) {
case "settingsButtonClicked":
navigateToSettings()
break
}
break
}
case "theme": {
if (message.text) {
setTheme(convertTextMateToHljs(JSON.parse(message.text)))
}
break
}
case "workspaceUpdated": {
setFilePaths(message.filePaths ?? [])
break
}
case "partialMessage": {
const partialMessage = message.partialMessage!
setState((prevState) => {
// worth noting it will never be possible for a more up-to-date message to be sent here or in normal messages post since the presentAssistantContent function uses lock
const lastIndex = findLastIndex(prevState.clineMessages, (msg) => msg.ts === partialMessage.ts)
if (lastIndex !== -1) {
const newClineMessages = [...prevState.clineMessages]
newClineMessages[lastIndex] = partialMessage
return { ...prevState, clineMessages: newClineMessages }
}
return prevState
})
break
}
case "openRouterModels": {
const updatedModels = message.openRouterModels ?? {}
setOpenRouterModels({
[openRouterDefaultModelId]: openRouterDefaultModelInfo, // in case the extension sent a model list without the default model
...updatedModels,
})
break
}
case "openAiModels": {
const updatedModels = message.openAiModels ?? []
setOpenAiModels(updatedModels)
@@ -220,6 +249,12 @@ export const ExtensionStateContextProvider: React.FC<{
setMcpServers(message.mcpServers ?? [])
break
}
case "mcpMarketplaceCatalog": {
if (message.mcpMarketplaceCatalog) {
setMcpMarketplaceCatalog(message.mcpMarketplaceCatalog)
}
break
}
}
}, [])
@@ -231,11 +266,6 @@ export const ExtensionStateContextProvider: React.FC<{
const historyButtonClickedSubscriptionRef = useRef<(() => void) | null>(null)
const chatButtonUnsubscribeRef = useRef<(() => void) | null>(null)
const accountButtonClickedSubscriptionRef = useRef<(() => void) | null>(null)
const settingsButtonClickedSubscriptionRef = useRef<(() => void) | null>(null)
const partialMessageUnsubscribeRef = useRef<(() => void) | null>(null)
const mcpMarketplaceUnsubscribeRef = useRef<(() => void) | null>(null)
const themeSubscriptionRef = useRef<(() => void) | null>(null)
const openRouterModelsUnsubscribeRef = useRef<(() => void) | null>(null)
// Subscribe to state updates and UI events using the gRPC streaming API
useEffect(() => {
@@ -365,114 +395,6 @@ export const ExtensionStateContextProvider: React.FC<{
onComplete: () => {},
})
// Set up settings button clicked subscription
settingsButtonClickedSubscriptionRef.current = UiServiceClient.subscribeToSettingsButtonClicked(
WebviewProviderTypeRequest.create({
providerType: currentProviderType,
}),
{
onResponse: () => {
// When settings button is clicked, navigate to settings
navigateToSettings()
},
onError: (error) => {
console.error("Error in settings button clicked subscription:", error)
},
onComplete: () => {
console.log("Settings button clicked subscription completed")
},
},
)
// Subscribe to partial message events
partialMessageUnsubscribeRef.current = UiServiceClient.subscribeToPartialMessage(EmptyRequest.create({}), {
onResponse: (protoMessage) => {
try {
console.log("[PARTIAL] Received partialMessage event from gRPC stream")
// Validate critical fields
if (!protoMessage.ts || protoMessage.ts <= 0) {
console.error("Invalid timestamp in partial message:", protoMessage)
return
}
const partialMessage = convertProtoToClineMessage(protoMessage)
console.log("[PARTIAL] Partial message:", partialMessage)
console.log("\n")
setState((prevState) => {
// worth noting it will never be possible for a more up-to-date message to be sent here or in normal messages post since the presentAssistantContent function uses lock
const lastIndex = findLastIndex(prevState.clineMessages, (msg) => msg.ts === partialMessage.ts)
if (lastIndex !== -1) {
const newClineMessages = [...prevState.clineMessages]
newClineMessages[lastIndex] = partialMessage
return { ...prevState, clineMessages: newClineMessages }
}
return prevState
})
} catch (error) {
console.error("Failed to process partial message:", error, protoMessage)
}
},
onError: (error) => {
console.error("Error in partialMessage subscription:", error)
},
onComplete: () => {
console.log("[DEBUG] partialMessage subscription completed")
},
})
// Subscribe to MCP marketplace catalog updates
mcpMarketplaceUnsubscribeRef.current = McpServiceClient.subscribeToMcpMarketplaceCatalog(EmptyRequest.create({}), {
onResponse: (catalog) => {
console.log("[DEBUG] Received MCP marketplace catalog update from gRPC stream")
setMcpMarketplaceCatalog(catalog)
},
onError: (error) => {
console.error("Error in MCP marketplace catalog subscription:", error)
},
onComplete: () => {
console.log("MCP marketplace catalog subscription completed")
},
})
// Subscribe to theme changes
themeSubscriptionRef.current = UiServiceClient.subscribeToTheme(EmptyRequest.create({}), {
onResponse: (response) => {
if (response.value) {
try {
const themeData = JSON.parse(response.value)
setTheme(convertTextMateToHljs(themeData))
console.log("[DEBUG] Received theme update from gRPC stream")
} catch (error) {
console.error("Error parsing theme data:", error)
}
}
},
onError: (error) => {
console.error("Error in theme subscription:", error)
},
onComplete: () => {
console.log("Theme subscription completed")
},
})
// Subscribe to OpenRouter models updates
openRouterModelsUnsubscribeRef.current = ModelsServiceClient.subscribeToOpenRouterModels(EmptyRequest.create({}), {
onResponse: (response: OpenRouterCompatibleModelInfo) => {
const models = response.models
setOpenRouterModels({
[openRouterDefaultModelId]: openRouterDefaultModelInfo, // in case the extension sent a model list without the default model
...models,
})
},
onError: (error) => {
console.error("Error in OpenRouter models subscription:", error)
},
onComplete: () => {
console.log("OpenRouter models subscription completed")
},
})
// Still send the webviewDidLaunch message for other initialization
vscode.postMessage({ type: "webviewDidLaunch" })
@@ -513,36 +435,15 @@ export const ExtensionStateContextProvider: React.FC<{
accountButtonClickedSubscriptionRef.current()
accountButtonClickedSubscriptionRef.current = null
}
if (settingsButtonClickedSubscriptionRef.current) {
settingsButtonClickedSubscriptionRef.current()
settingsButtonClickedSubscriptionRef.current = null
}
if (partialMessageUnsubscribeRef.current) {
partialMessageUnsubscribeRef.current()
partialMessageUnsubscribeRef.current = null
}
if (mcpMarketplaceUnsubscribeRef.current) {
mcpMarketplaceUnsubscribeRef.current()
mcpMarketplaceUnsubscribeRef.current = null
}
if (themeSubscriptionRef.current) {
themeSubscriptionRef.current()
themeSubscriptionRef.current = null
}
if (openRouterModelsUnsubscribeRef.current) {
openRouterModelsUnsubscribeRef.current()
openRouterModelsUnsubscribeRef.current = null
}
}
}, [])
const refreshOpenRouterModels = useCallback(() => {
ModelsServiceClient.refreshOpenRouterModels(EmptyRequest.create({}))
.then((response: OpenRouterCompatibleModelInfo) => {
const models = response.models
.then((res) => {
setOpenRouterModels({
[openRouterDefaultModelId]: openRouterDefaultModelInfo, // in case the extension sent a model list without the default model
...models,
...res.models,
})
})
.catch((error: Error) => console.error("Failed to refresh OpenRouter models:", error))
@@ -627,11 +528,6 @@ export const ExtensionStateContextProvider: React.FC<{
...prevState,
shellIntegrationTimeout: value,
})),
setTerminalReuseEnabled: (value) =>
setState((prevState) => ({
...prevState,
terminalReuseEnabled: value,
})),
setMcpServers: (mcpServers: McpServer[]) => setMcpServers(mcpServers),
setMcpMarketplaceCatalog: (catalog: McpMarketplaceCatalog) => setMcpMarketplaceCatalog(catalog),
setShowMcp,
+1 -16
View File
@@ -10,13 +10,6 @@ import type { WebviewApi } from "vscode-webview"
* dev server by using native web browser features that mock the functionality
* enabled by acquireVsCodeApi.
*/
declare global {
interface Window {
__is_standalone__?: boolean
standalonePostMessage?: (event: any) => void
}
}
class VSCodeAPIWrapper {
private readonly vsCodeApi: WebviewApi<unknown> | undefined
@@ -39,16 +32,8 @@ class VSCodeAPIWrapper {
public postMessage(message: WebviewMessage) {
if (this.vsCodeApi) {
this.vsCodeApi.postMessage(message)
} else if (window.__is_standalone__) {
if (!window.standalonePostMessage) {
console.warn("Standalone postMessage not found.")
return
}
const json = JSON.stringify(message)
console.log("Standalone postMessage: " + json.slice(0, 200))
window.standalonePostMessage(json)
} else {
console.log("postMessage fallback: ", message)
console.log(message)
}
}