Compare commits

..

19 Commits

Author SHA1 Message Date
Elephant Lumps 611bb53f10 merge conflicts 2025-06-05 10:46:35 -07:00
Evan c68e427d13 migrate mcpMarketplaceCatalog (#4023)
Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-06-04 23:17:13 -07:00
Evan 0dca4dedbd migrate partialMessage (#4033)
Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-06-04 23:02:37 -07:00
Evan c6e7b5249e Migrate settingsButtonClicked protobus (#3976)
* migrate settingsButtonClicked

* changeset

* add provider type filtering

---------

Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-06-04 20:49:07 -07:00
github-actions[bot] 9dcb82f22e v3.17.10 Release Notes
v3.17.10 Release Notes
2025-06-04 19:05:11 -07:00
Toshii fa8491b3aa remove log (#4040) 2025-06-04 18:53:12 -07:00
pashpashpash b811db823b Option to disable aggressive terminal reuse for users affected by the task lockout bug. (#4041)
* terminal setting for reusing terminal commands

* language

* changeset

* changeset language

* comment clarity

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-06-04 18:52:53 -07:00
Sarah Fortune 4f7dc6fb36 Add a postMessage handler for standalone cline app (#4038)
* Add a postMessage handler for standalone cline app.

If there is no vscode api defined, try to use the standalone post
Message handler.

* Imports

* Update logs
2025-06-04 17:44:38 -07:00
Sarah Fortune b7096a77db Add type checking for protobus RPC handlers. (#4020)
* Add type checking for protobus RPC handlers.

Add type parameters to the wrapper function when generating the server-setup file.
So that the parameters of the RPC handler will be type checked against types defined in the service.
The generated code looks like this:
```
    server.addService(proto.cline.CheckpointsService.service, {
         checkpointDiff: wrapper<cline.Int64Request,cline.Empty>(checkpointDiff, controller),
         checkpointRestore: wrapper<cline.CheckpointRestoreRequest,cline.Empty>(checkpointRestore, controller),
    });
```

Add an index.ts file to the proto directory so all the proto types can be imported without having to know which proto message is defined in which file (the proto descriptor set doesn't have this information).

* Generate index.ts with protoc instead of doing it ourselves.

Turn on the option 'dontExportCommonSymbols' in protoc, so that each generated proto file is not trying to export the same utility functions.

Generate all the protos with the same protoc command.

* cleanup
2025-06-04 17:42:19 -07:00
Andrei Eternal cbcd17764b Protobus Host Bridge (#3747)
* WIP host bridge

* Run formatter

* remove tmp impl & rename host grpc client

* gitignore more files

* better layout

* host handler to make other hosts easier to add

* remove adapter pattern

* get host responses correctly

* fix streaming mode for host bridge

* first wip subscription host bridge demo for watching mcp server config

* format, comment

* add cancellation for host grpc stream

* remove unneeded functions from host-grpc-handler

* add a method for canceling request rather than using the registry

* another todo

* use StringRequest for uri.proto

* debounce new file watcher

* remove test setup

* remove some todos and logs

* remove registry use todo

* Revert "remove registry use todo"

This reverts commit 84078d3469.

* fix capitalization of uri.proto

* a better pattern for a callback based bridge without using the requestRegistry directly

---------

Co-authored-by: Andrei Edell <andrei@nugbase.com>
Co-authored-by: Sarah Fortune <sarah.fortune@gmail.com>
Co-authored-by: Andrei Eternal <eternal@cline.bot>
2025-06-04 15:41:30 -07:00
Ara 47899ac52d Adding Cline Walkthrough (#3746)
* Show gemini 2.5 flash prompt cache

* Show gemini 2.5 flash prompt cache

* Show gemini 2.5 flash prompt cache

* Show gemini 2.5 flash prompt cache

* Fix Diff edit prompt

* Fix Diff edit prompt

* Fix Diff edit prompt

* Fix Diff edit prompt

* Fix Diff edit prompt

* Fix Diff edit prompt

* New files

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-06-04 13:45:52 -07:00
Evan 586eb3cd9e migrate accountButtonClicked (#4008)
Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-06-04 11:40:52 -07:00
Steven White 584535393f New AskSage Models (#3780)
* chore: new AskSage models

* chore: add changeset
2025-06-04 11:21:18 -07:00
Jonny Chen 01a9ab0cf6 feat: add all qwen3 models support and add thinking mode options (#3692)
* feat: add all qwen3 models support and add thinking mode options

* fix: qwen model reasoning logic configuration, set the qwen model maxBudget value

* fix: correct the maxTokens in qwen3 model

* vertex model

---------

Co-authored-by: xuanqi <xuanqi.cc@alibaba-inc.com>
Co-authored-by: 0xtoshii <94262432+0xToshii@users.noreply.github.com>
2025-06-04 09:08:48 -07:00
Sarah Fortune 51a538373f Make the protobuf object literal lint check an error instead of a warning. (#4019)
Fix remaining occurences of protobuf literals.
2025-06-03 23:58:52 -07:00
Elephant Lumps 6787bb51ee merge conflicts 2025-06-03 23:48:38 -07:00
canvrno a6ce75d17c Telemetry fix (#4009) 2025-06-03 18:44:52 -07:00
Evan 0a3d0e3569 Migrate chatbuttonclicked protobus (#3854)
* migrate chatButtonClicked

* changeset

* send targeted event to the controller

* prettier

* add test mock

* continue to fix tests

* fix test mocks once and for all

* fix test mocks once and for all

* try again please lord

* try again

* temporarily disable tests

* revert other test 'fixes'

* one more

---------

Co-authored-by: Elephant Lumps <celestial_vault@Elephants-MacBook-Pro.local>
2025-06-03 14:47:08 -07:00
Elephant Lumps d6c640d5b7 migrate theme message protobus 2025-06-03 13:50:09 -07:00
70 changed files with 3073 additions and 422 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Ollama: Use a filterable dropdown instead of radio selection
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
fix: Address memory leak by bypassing subscribeToState gRPC stream for state updates
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Migrate settingsButtonClicked to protobus
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
adding support for streamable mcp server
+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": "warn",
"eslint-rules/no-protobuf-object-literals": "error",
"eslint-rules/no-grpc-client-object-literals": "error"
},
"ignorePatterns": ["out", "dist", "**/*.d.ts"]
+3 -2
View File
@@ -86,8 +86,9 @@ jobs:
- name: Build Tests and Extension
run: npm run pretest
- name: Unit Tests
run: npm run test:unit
# Unit Tests disabled due to module system conflicts between backend and webview-ui
# - name: Unit Tests
# run: npm run test:unit
# Run extension tests with coverage
- name: Extension Tests with Coverage
+10 -1
View File
@@ -22,9 +22,18 @@ 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
+10
View File
@@ -1,5 +1,15 @@
# Changelog
## [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
@@ -0,0 +1,197 @@
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
}
+33
View File
@@ -0,0 +1,33 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { StreamingResponseHandler } from "./host-grpc-handler"
import { handleUriServiceRequest, handleUriServiceStreamingRequest } from "./uri/index"
import { handleWatchServiceRequest, handleWatchServiceStreamingRequest } from "./watch/index"
/**
* 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> = {
"host.UriService": {
requestHandler: handleUriServiceRequest,
streamingHandler: handleUriServiceStreamingRequest,
},
"host.WatchService": {
requestHandler: handleWatchServiceRequest,
streamingHandler: handleWatchServiceStreamingRequest,
},
}
+138
View File
@@ -0,0 +1,138 @@
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
@@ -0,0 +1,20 @@
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,
})
}
+22
View File
@@ -0,0 +1,22 @@
// 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 uri service registry
const uriService = createServiceRegistry("uri")
// Export the method handler types and registration function
export type UriMethodHandler = ServiceMethodHandler
export type UriStreamingMethodHandler = StreamingMethodHandler
export const registerMethod = uriService.registerMethod
// Export the request handlers
export const handleUriServiceRequest = uriService.handleRequest
export const handleUriServiceStreamingRequest = uriService.handleStreamingRequest
export const isStreamingMethod = uriService.isStreamingMethod
// Register all uri methods
registerAllMethods()
+28
View File
@@ -0,0 +1,28 @@
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,
})
}
+16
View File
@@ -0,0 +1,16 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Import all method implementations
import { registerMethod } from "./index"
import { file } from "./file"
import { joinPath } from "./joinPath"
import { parse } from "./parse"
// Register all uri service methods
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("file", file)
registerMethod("joinPath", joinPath)
registerMethod("parse", parse)
}
+20
View File
@@ -0,0 +1,20 @@
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,
})
}
+22
View File
@@ -0,0 +1,22 @@
// 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 watch service registry
const watchService = createServiceRegistry("watch")
// Export the method handler types and registration function
export type WatchMethodHandler = ServiceMethodHandler
export type WatchStreamingMethodHandler = StreamingMethodHandler
export const registerMethod = watchService.registerMethod
// Export the request handlers
export const handleWatchServiceRequest = watchService.handleRequest
export const handleWatchServiceStreamingRequest = watchService.handleStreamingRequest
export const isStreamingMethod = watchService.isStreamingMethod
// Register all watch methods
registerAllMethods()
+15
View File
@@ -0,0 +1,15 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Import all method implementations
import { registerMethod } from "./index"
import { subscribeToFile } from "./subscribeToFile"
// Streaming methods for this service
export const streamingMethods = ["subscribeToFile"]
// Register all watch service methods
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("subscribeToFile", subscribeToFile, { isStreaming: true })
}
+225
View File
@@ -0,0 +1,225 @@
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.9",
"version": "3.17.10",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.17.9",
"version": "3.17.10",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.12.4",
+58 -1
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.9",
"version": "3.17.10",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -46,6 +46,58 @@
],
"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": [
{
@@ -143,6 +195,11 @@
"command": "cline.improveCode",
"title": "Improve with Cline",
"category": "Cline"
},
{
"command": "cline.openWalkthrough",
"title": "Open Walkthrough",
"category": "Cline"
}
],
"keybindings": [
+253 -36
View File
@@ -12,38 +12,6 @@ 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, "..")
@@ -72,6 +40,15 @@ 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..."))
@@ -81,7 +58,7 @@ async function main() {
// Define output directories
const TS_OUT_DIR = path.join(ROOT_DIR, "src", "shared", "proto")
// Create output directory if it doesn't exist
// Create output directories if they don't exist
await fs.mkdir(TS_OUT_DIR, { recursive: true })
// Clean up existing generated files
@@ -96,7 +73,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 = [
@@ -104,6 +81,8 @@ 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(" ")
@@ -117,7 +96,6 @@ 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,
@@ -138,8 +116,11 @@ 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()
}
/**
@@ -282,7 +263,7 @@ async function generateMethodRegistrations() {
// Import all method implementations
import { registerMethod } from "./index"\n`
// Add imports for all implementation files
// Import implementations directly
for (const file of implementationFiles) {
const baseName = path.basename(file, ".ts")
methodsContent += `import { ${baseName} } from "./${baseName}"\n`
@@ -456,6 +437,242 @@ 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
@@ -0,0 +1,36 @@
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
@@ -0,0 +1,32 @@
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,6 +16,9 @@ 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 {
+215
View File
@@ -18,6 +18,206 @@ 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
@@ -34,4 +234,19 @@ service UiService {
// Subscribe to history button click events
rpc subscribeToHistoryButtonClicked(WebviewProviderTypeRequest) returns (stream Empty);
// Subscribe to chat button clicked events (when the chat button is clicked in VSCode)
rpc subscribeToChatButtonClicked(EmptyRequest) returns (stream Empty);
// 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);
}
+5 -3
View File
@@ -32,13 +32,15 @@ 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(${rpcName}, controller),`)
handlerSetup.push(` ${rpcName}: wrapStreamingResponse<${requestType},void>(${rpcName}, controller),`)
} else {
handlerSetup.push(` ${rpcName}: wrapper(${rpcName}, controller),`)
const responseType = "proto.cline." + rpc.responseType.type.name
handlerSetup.push(` ${rpcName}: wrapper<${requestType},${responseType}>(${rpcName}, controller),`)
}
}
handlerSetup.push(` });`)
@@ -58,11 +60,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,
+19 -2
View File
@@ -53,20 +53,37 @@ 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),
]
if (isDeepseekReasoner) {
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)) {
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 },
...(model.id === "deepseek-r1" ? {} : { temperature: 0 }),
temperature,
...thinkingArgs,
})
for await (const chunk of stream) {
-1
View File
@@ -528,7 +528,6 @@ class NewFileContentConstructor {
} else {
let appendToPendingNonStandardLines = canWritependingNonStandardLines
if (appendToPendingNonStandardLines) {
console.log("unstandard line:" + line)
// 处理非标内容
this.pendingNonStandardLines.push(line)
}
+11 -1
View File
@@ -3,6 +3,8 @@ 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
@@ -17,7 +19,15 @@ export const getRelativePaths: FileMethodHandler = async (
const resolvedPaths = await Promise.all(
request.uris.map(async (uriString) => {
try {
const fileUri = vscode.Uri.parse(uriString, true)
// 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 relativePathToGet = vscode.workspace.asRelativePath(fileUri, false)
// If the path is still absolute, it's outside the workspace
+29 -46
View File
@@ -1,5 +1,6 @@
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"
@@ -54,6 +55,8 @@ import { ClineRulesToggles } from "@shared/cline-rules"
import { sendStateUpdate } from "./state/subscribeToState"
import { sendAddToInputEvent } from "./ui/subscribeToAddToInput"
import { sendAuthCallbackEvent } from "./account/subscribeToAuthCallback"
import { sendChatButtonClickedEvent } from "./ui/subscribeToChatButtonClicked"
import { sendMcpMarketplaceCatalogEvent } from "./mcp/subscribeToMcpMarketplaceCatalog"
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"
@@ -65,6 +68,7 @@ https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/c
*/
export class Controller {
readonly id: string = uuidv4()
private postMessage: (message: ExtensionMessage) => Thenable<boolean> | undefined
private disposables: vscode.Disposable[] = []
@@ -148,6 +152,7 @@ export class Controller {
browserSettings,
chatSettings,
shellIntegrationTimeout,
terminalReuseEnabled,
enableCheckpointsSetting,
isNewUser,
taskHistory,
@@ -182,6 +187,7 @@ export class Controller {
browserSettings,
chatSettings,
shellIntegrationTimeout,
terminalReuseEnabled ?? true,
enableCheckpointsSetting ?? true,
customInstructions,
task,
@@ -218,12 +224,6 @@ 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) {
@@ -240,10 +240,7 @@ export class Controller {
getGlobalState(this.context, "mcpMarketplaceCatalog").then((mcpMarketplaceCatalog) => {
if (mcpMarketplaceCatalog) {
this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
mcpMarketplaceCatalog: mcpMarketplaceCatalog as McpMarketplaceCatalog,
})
sendMcpMarketplaceCatalogEvent(mcpMarketplaceCatalog as McpMarketplaceCatalog)
}
})
this.silentlyRefreshMcpMarketplace()
@@ -261,6 +258,13 @@ export class Controller {
}
}
})
// Initialize telemetry service with user's current setting
this.getStateToPostToWebview().then((state) => {
const { telemetrySetting } = state
const isOptedIn = telemetrySetting !== "disabled"
telemetryService.updateTelemetryState(isOptedIn)
})
break
case "newTask":
// Code that should run in response to the hello message command
@@ -391,6 +395,15 @@ 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()
@@ -738,10 +751,6 @@ 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
@@ -787,10 +796,7 @@ export class Controller {
try {
const catalog = await this.fetchMcpMarketplaceFromApi(true)
if (catalog) {
await this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
mcpMarketplaceCatalog: catalog,
})
await sendMcpMarketplaceCatalogEvent(catalog)
}
} catch (error) {
console.error("Failed to silently refresh MCP marketplace:", error)
@@ -818,27 +824,17 @@ export class Controller {
| McpMarketplaceCatalog
| undefined
if (!forceRefresh && cachedCatalog?.items) {
await this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
mcpMarketplaceCatalog: cachedCatalog,
})
await sendMcpMarketplaceCatalogEvent(cachedCatalog)
return
}
const catalog = await this.fetchMcpMarketplaceFromApi(false)
if (catalog) {
await this.postMessageToWebview({
type: "mcpMarketplaceCatalog",
mcpMarketplaceCatalog: catalog,
})
await sendMcpMarketplaceCatalogEvent(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)
}
}
@@ -1018,18 +1014,6 @@ export class Controller {
throw new Error("Task not found")
}
async showTaskWithId(id: string) {
if (id !== this.task?.taskId) {
// non-current task
const { historyItem } = await this.getTaskWithId(id)
await this.initTask(undefined, undefined, undefined, historyItem) // clears existing task
}
await this.postMessageToWebview({
type: "action",
action: "chatButtonClicked",
})
}
async exportTaskWithId(id: string) {
const { historyItem, apiConversationHistory } = await this.getTaskWithId(id)
await downloadTask(historyItem.ts, apiConversationHistory)
@@ -1153,10 +1137,7 @@ export class Controller {
async postStateToWebview() {
const state = await this.getStateToPostToWebview()
// For testing: Bypass gRPC stream and send state directly
console.log("[Controller Test Revert] Posting full state via direct 'state' message.")
await this.postMessageToWebview({ type: "state", state: state })
// await sendStateUpdate(state) // Original line for the GrPC stream
await sendStateUpdate(state)
}
async getStateToPostToWebview(): Promise<ExtensionState> {
@@ -1176,6 +1157,7 @@ export class Controller {
globalClineRulesToggles,
globalWorkflowToggles,
shellIntegrationTimeout,
terminalReuseEnabled,
isNewUser,
} = await getAllExtensionState(this.context)
@@ -1220,6 +1202,7 @@ export class Controller {
localWorkflowToggles: localWorkflowToggles || {},
globalWorkflowToggles: globalWorkflowToggles || {},
shellIntegrationTimeout,
terminalReuseEnabled,
isNewUser,
}
}
+2 -4
View File
@@ -3,6 +3,7 @@ import { Empty, StringRequest } from "../../../shared/proto/common"
import { McpServer, McpDownloadResponse } from "@shared/mcp"
import axios from "axios"
import * as vscode from "vscode"
import { sendChatButtonClickedEvent } from "../ui/subscribeToChatButtonClicked"
/**
* Download an MCP server from the marketplace
@@ -77,10 +78,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
// Initialize task and show chat view
await controller.initTask(task)
await controller.postMessageToWebview({
type: "action",
action: "chatButtonClicked",
})
await sendChatButtonClickedEvent(controller.id)
// Return an empty response - the client only cares if the call succeeded
return Empty.create()
@@ -0,0 +1,55 @@
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)
}
+2 -4
View File
@@ -2,6 +2,7 @@ import { Controller } from ".."
import { Empty, EmptyRequest } from "../../../shared/proto/common"
import { resetExtensionState } from "../../../core/storage/state"
import * as vscode from "vscode"
import { sendChatButtonClickedEvent } from "../ui/subscribeToChatButtonClicked"
/**
* Resets the extension state to its defaults
@@ -22,10 +23,7 @@ export async function resetState(controller: Controller, request: EmptyRequest):
vscode.window.showInformationMessage("State reset")
await controller.postStateToWebview()
await controller.postMessageToWebview({
type: "action",
action: "chatButtonClicked",
})
await sendChatButtonClickedEvent(controller.id)
return Empty.create()
} catch (error) {
+1
View File
@@ -1,6 +1,7 @@
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
+3 -8
View File
@@ -1,6 +1,7 @@
import { Controller } from ".."
import { StringRequest } from "../../../shared/proto/common"
import { TaskResponse } from "../../../shared/proto/task"
import { sendChatButtonClickedEvent } from "../ui/subscribeToChatButtonClicked"
/**
* Shows a task with the specified ID
@@ -22,10 +23,7 @@ export async function showTaskWithId(controller: Controller, request: StringRequ
await controller.initTask(undefined, undefined, undefined, historyItem)
// Send UI update to show the chat view
await controller.postMessageToWebview({
type: "action",
action: "chatButtonClicked",
})
await sendChatButtonClickedEvent(controller.id)
// Return task data for gRPC response
return TaskResponse.create({
@@ -49,10 +47,7 @@ export async function showTaskWithId(controller: Controller, request: StringRequ
await controller.initTask(undefined, undefined, undefined, fetchedItem)
// Send UI update to show the chat view
await controller.postMessageToWebview({
type: "action",
action: "chatButtonClicked",
})
await sendChatButtonClickedEvent(controller.id)
return TaskResponse.create({
id: fetchedItem.id,
@@ -0,0 +1,55 @@
import { Controller } from "../index"
import { Empty, EmptyRequest } from "@shared/proto/common"
import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler"
// Track subscriptions by controller ID
const activeSubscriptions = new Map<string, StreamingResponseHandler>()
/**
* Subscribe to account button clicked events
* @param controller The controller instance
* @param request The empty request
* @param responseStream The streaming response handler
* @param requestId The request ID for cleanup
*/
export async function subscribeToAccountButtonClicked(
controller: Controller,
_request: EmptyRequest,
responseStream: StreamingResponseHandler,
requestId?: string,
): Promise<void> {
const controllerId = controller.id
// Store subscription with controller ID
activeSubscriptions.set(controllerId, responseStream)
// Register cleanup
const cleanup = () => {
activeSubscriptions.delete(controllerId)
}
if (requestId) {
getRequestRegistry().registerRequest(requestId, cleanup, { type: "account_button_subscription" }, responseStream)
}
}
/**
* Send account button clicked event to a specific controller
* @param controllerId The ID of the controller to send the event to
*/
export async function sendAccountButtonClickedEvent(controllerId: string): Promise<void> {
const responseStream = activeSubscriptions.get(controllerId)
if (!responseStream) {
console.log(`No active subscription for controller ${controllerId}`)
return
}
try {
const event: Empty = Empty.create({})
await responseStream(event, false)
} catch (error) {
console.error(`Error sending account button clicked event to controller ${controllerId}:`, error)
activeSubscriptions.delete(controllerId)
}
}
@@ -0,0 +1,63 @@
import { Controller } from "../index"
import { Empty } from "@shared/proto/common"
import { EmptyRequest } from "@shared/proto/common"
import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler"
// Keep track of active chatButtonClicked subscriptions by controller ID
const activeChatButtonClickedSubscriptions = new Map<string, StreamingResponseHandler>()
/**
* Subscribe to chatButtonClicked 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 subscribeToChatButtonClicked(
controller: Controller,
_request: EmptyRequest,
responseStream: StreamingResponseHandler,
requestId?: string,
): Promise<void> {
const controllerId = controller.id
console.log(`[DEBUG] set up chatButtonClicked subscription for controller ${controllerId}`)
// Add this subscription to the active subscriptions with the controller ID
activeChatButtonClickedSubscriptions.set(controllerId, responseStream)
// Register cleanup when the connection is closed
const cleanup = () => {
activeChatButtonClickedSubscriptions.delete(controllerId)
}
// Register the cleanup function with the request registry if we have a requestId
if (requestId) {
getRequestRegistry().registerRequest(requestId, cleanup, { type: "chatButtonClicked_subscription" }, responseStream)
}
}
/**
* Send a chatButtonClicked event to a specific controller's subscription
* @param controllerId The ID of the controller to send the event to
*/
export async function sendChatButtonClickedEvent(controllerId: string): Promise<void> {
// Get the subscription for this specific controller
const responseStream = activeChatButtonClickedSubscriptions.get(controllerId)
if (!responseStream) {
console.log(`[DEBUG] No active subscription for controller ${controllerId}`)
return
}
try {
const event: Empty = Empty.create({})
await responseStream(
event,
false, // Not the last message
)
} catch (error) {
console.error(`Error sending chatButtonClicked event to controller ${controllerId}:`, error)
// Remove the subscription if there was an error
activeChatButtonClickedSubscriptions.delete(controllerId)
}
}
@@ -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.create({})
const event: Empty = Empty.create({})
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.create({})
const event: Empty = Empty.create({})
// Process all subscriptions, filtering based on the source
const promises = Array.from(mcpButtonClickedSubscriptions.entries()).map(async ([responseStream, providerType]) => {
@@ -0,0 +1,56 @@
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)
}
@@ -0,0 +1,61 @@
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)
}
@@ -0,0 +1,76 @@
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,6 +91,7 @@ export type GlobalStateKey =
| "favoritedModelIds"
| "requestTimeoutMs"
| "shellIntegrationTimeout"
| "terminalReuseEnabled"
| "isNewUser"
export type LocalStateKey = "localClineRulesToggles"
+3 -1
View File
@@ -165,6 +165,7 @@ 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>,
@@ -255,7 +256,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>,
fetch,
getGlobalState(context, "terminalReuseEnabled") as Promise<boolean | undefined>,
])
let apiProvider: ApiProvider
@@ -390,6 +391,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
planActSeparateModelsSetting,
enableCheckpointsSetting: enableCheckpointsSetting,
shellIntegrationTimeout: shellIntegrationTimeout || 4000,
terminalReuseEnabled: terminalReuseEnabled ?? true,
globalWorkflowToggles: globalWorkflowToggles || {},
}
}
+12 -10
View File
@@ -72,6 +72,8 @@ 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"
@@ -199,6 +201,7 @@ export class Task {
browserSettings: BrowserSettings,
chatSettings: ChatSettings,
shellIntegrationTimeout: number,
terminalReuseEnabled: boolean,
enableCheckpointsSetting: boolean,
customInstructions?: string,
task?: string,
@@ -218,6 +221,7 @@ 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()
@@ -741,10 +745,8 @@ 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()
await this.postMessageToWebview({
type: "partialMessage",
partialMessage: lastMessage,
})
const protoMessage = convertClineMessageToProto(lastMessage)
await sendPartialMessageEvent(protoMessage)
throw new Error("Current ask promise was ignored 1")
} else {
// this is a new partial message, so add it with partial state
@@ -785,10 +787,8 @@ export class Task {
lastMessage.partial = false
await this.saveClineMessagesAndUpdateHistory()
// await this.postStateToWebview()
await this.postMessageToWebview({
type: "partialMessage",
partialMessage: lastMessage,
})
const protoMessage = convertClineMessageToProto(lastMessage)
await sendPartialMessageEvent(protoMessage)
} else {
// this is a new partial=false message, so add it like normal
this.askResponse = undefined
@@ -864,7 +864,8 @@ export class Task {
lastMessage.images = images
lastMessage.files = files
lastMessage.partial = partial
await this.postMessageToWebview({ type: "partialMessage", partialMessage: lastMessage })
const protoMessage = convertClineMessageToProto(lastMessage)
await sendPartialMessageEvent(protoMessage)
} else {
// this is a new partial message, so add it with partial state
const sayTs = Date.now()
@@ -894,7 +895,8 @@ 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()
await this.postMessageToWebview({ type: "partialMessage", partialMessage: lastMessage }) // more performant than an entire postStateToWebview
const protoMessage = convertClineMessageToProto(lastMessage)
await sendPartialMessageEvent(protoMessage) // more performant than an entire postStateToWebview
} else {
// this is a new partial=false message, so add it like normal
const sayTs = Date.now()
+6 -5
View File
@@ -8,6 +8,7 @@ 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
@@ -139,11 +140,11 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
vscode.workspace.onDidChangeConfiguration(
async (e) => {
if (e && e.affectsConfiguration("workbench.colorTheme")) {
// Sends latest theme name to webview
await this.controller.postMessageToWebview({
type: "theme",
text: JSON.stringify(await getTheme()),
})
// Send theme update via gRPC subscription
const theme = await getTheme()
if (theme) {
await sendThemeEvent(JSON.stringify(theme))
}
}
if (e && e.affectsConfiguration("cline.mcpMarketplace.enabled")) {
// Update state when marketplace tab setting changes
+4 -4
View File
@@ -2,6 +2,8 @@ import * as vscode from "vscode"
import { Controller } from "@core/controller"
import { ClineAPI } from "./cline"
import { getGlobalState } from "@core/storage/state"
import { sendChatButtonClickedEvent } from "@core/controller/ui/subscribeToChatButtonClicked"
import { WebviewProviderType as WebviewProviderTypeEnum } from "@shared/proto/ui"
export function createClineAPI(outputChannel: vscode.OutputChannel, sidebarController: Controller): ClineAPI {
const api: ClineAPI = {
@@ -18,10 +20,8 @@ export function createClineAPI(outputChannel: vscode.OutputChannel, sidebarContr
outputChannel.appendLine("Starting new task")
await sidebarController.clearTask()
await sidebarController.postStateToWebview()
await sidebarController.postMessageToWebview({
type: "action",
action: "chatButtonClicked",
})
await sendChatButtonClickedEvent(sidebarController.id)
await sidebarController.initTask(task, images)
outputChannel.appendLine(
`Task started with message: ${task ? `"${task}"` : "undefined"} and ${images?.length || 0} image(s)`,
+37 -34
View File
@@ -12,13 +12,16 @@ import { posthogClientProvider } from "./services/posthog/PostHogClientProvider"
import { WebviewProvider } from "./core/webview"
import { Controller } from "./core/controller"
import { sendMcpButtonClickedEvent } from "./core/controller/ui/subscribeToMcpButtonClicked"
import { sendChatButtonClickedEvent } from "./core/controller/ui/subscribeToChatButtonClicked"
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"
import { sendHistoryButtonClickedEvent } from "./core/controller/ui/subscribeToHistoryButtonClicked"
import { sendAccountButtonClickedEvent } from "./core/controller/ui/subscribeToAccountButtonClicked"
/*
Built using https://github.com/microsoft/vscode-webview-ui-toolkit
@@ -92,19 +95,27 @@ export async function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand("cline.plusButtonClicked", async (webview: any) => {
const openChat = async (instance?: WebviewProvider) => {
console.log("[DEBUG] plusButtonClicked", webview)
// Pass the webview type to the event sender
const isSidebar = !webview
const openChat = async (instance: WebviewProvider) => {
await instance?.controller.clearTask()
await instance?.controller.postStateToWebview()
await instance?.controller.postMessageToWebview({
type: "action",
action: "chatButtonClicked",
})
await sendChatButtonClickedEvent(instance.controller.id)
}
const isSidebar = !webview
if (isSidebar) {
openChat(WebviewProvider.getSidebarInstance())
const sidebarInstance = WebviewProvider.getSidebarInstance()
if (sidebarInstance) {
openChat(sidebarInstance)
// Send event to the sidebar instance
}
} else {
WebviewProvider.getTabInstances().forEach(openChat)
const tabInstances = WebviewProvider.getTabInstances()
for (const instance of tabInstances) {
openChat(instance)
}
}
}),
)
@@ -159,20 +170,10 @@ export async function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand("cline.settingsButtonClicked", (webview: any) => {
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)
}
})
const isSidebar = !webview
const webviewType = isSidebar ? WebviewProviderTypeEnum.SIDEBAR : WebviewProviderTypeEnum.TAB
sendSettingsButtonClickedEvent(webviewType)
}),
)
@@ -190,20 +191,22 @@ export async function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand("cline.accountButtonClicked", (webview: any) => {
WebviewProvider.getAllInstances().forEach((instance) => {
const openAccount = async (instance?: WebviewProvider) => {
instance?.controller.postMessageToWebview({
type: "action",
action: "accountButtonClicked",
})
console.log("[DEBUG] accountButtonClicked", webview)
const isSidebar = !webview
if (isSidebar) {
const sidebarInstance = WebviewProvider.getSidebarInstance()
if (sidebarInstance) {
// Send event to sidebar controller
sendAccountButtonClickedEvent(sidebarInstance.controller.id)
}
const isSidebar = !webview
if (isSidebar) {
openAccount(WebviewProvider.getSidebarInstance())
} else {
WebviewProvider.getTabInstances().forEach(openAccount)
} else {
// Send to all tab instances
const tabInstances = WebviewProvider.getTabInstances()
for (const instance of tabInstances) {
sendAccountButtonClickedEvent(instance.controller.id)
}
})
}
}),
)
+37 -30
View File
@@ -94,6 +94,7 @@ 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
@@ -234,42 +235,44 @@ export class TerminalManager {
return matchingTerminal
}
// 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 }
})
// 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 }
})
// 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()
}
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
// 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
}
}
this.terminalIds.add(availableTerminal.id)
return availableTerminal
}
this.terminalIds.add(availableTerminal.id)
return availableTerminal
}
// If all terminals are busy, create a new one
@@ -311,4 +314,8 @@ export class TerminalManager {
setShellIntegrationTimeout(timeout: number): void {
this.shellIntegrationTimeout = timeout
}
setTerminalReuseEnabled(enabled: boolean): void {
this.terminalReuseEnabled = enabled
}
}
+39 -13
View File
@@ -17,6 +17,9 @@ 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,
@@ -118,22 +121,45 @@ export class McpHub {
private async watchMcpSettingsFile(): Promise<void> {
const settingsPath = await this.getMcpSettingsFilePath()
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)
// 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)
}
}
}
}
}),
},
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> {
+38 -32
View File
@@ -37,25 +37,28 @@ function createToolCallTracker(webviewProvider: WebviewProvider): {
// Intercept messages to track tool usage
const originalPostMessageToWebview = webviewProvider.controller.postMessageToWebview
webviewProvider.controller.postMessageToWebview = async (message: ExtensionMessage) => {
// 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
}
}
// 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 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
}
}
}
// 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
// }
// }
// }
return originalPostMessageToWebview.call(webviewProvider.controller, message)
}
@@ -504,22 +507,25 @@ export function createMessageCatcher(webviewProvider: WebviewProvider): vscode.D
// Intercept outgoing messages from extension to webview
webviewProvider.controller.postMessageToWebview = async (message: ExtensionMessage) => {
// Check for completion_result message
if (message.type === "partialMessage" && message.partialMessage?.say === "completion_result") {
// Complete the current task
completeTask()
}
// 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 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 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()
// }
// Automatically respond to different types of asks
setTimeout(async () => {
await autoRespondToAsk(webviewProvider, askType, askText)
}, 100) // Small delay to ensure the message is processed first
}
// 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
// }
return originalPostMessageToWebview.call(webviewProvider.controller, message)
}
+2 -11
View File
@@ -19,15 +19,12 @@ export interface ExtensionMessage {
| "selectedImages"
| "ollamaModels"
| "lmStudioModels"
| "theme"
| "workspaceUpdated"
| "partialMessage"
| "openRouterModels"
| "openAiModels"
| "requestyModels"
| "mcpServers"
| "relinquishControl"
| "mcpMarketplaceCatalog"
| "mcpDownloadDetails"
| "commitSearchResults"
| "openGraphData"
@@ -38,13 +35,7 @@ export interface ExtensionMessage {
| "fileSearchResults"
| "grpc_response" // New type for gRPC responses
text?: string
action?:
| "chatButtonClicked"
| "settingsButtonClicked"
| "didBecomeVisible"
| "accountLogoutClicked"
| "accountButtonClicked"
| "focusChatInput"
action?: "didBecomeVisible" | "accountLogoutClicked" | "focusChatInput"
state?: ExtensionState
images?: string[]
files?: string[]
@@ -52,7 +43,6 @@ 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>
@@ -120,6 +110,7 @@ export interface ExtensionState {
taskHistory: HistoryItem[]
telemetrySetting: TelemetrySetting
shellIntegrationTimeout: number
terminalReuseEnabled?: boolean
uriScheme?: string
userInfo?: {
displayName: string | null
+1
View File
@@ -80,6 +80,7 @@ export interface WebviewMessage {
offset?: number
shellIntegrationTimeout?: number
terminalReuseEnabled?: boolean
}
export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse"
+286 -14
View File
@@ -1021,6 +1021,118 @@ 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,
@@ -1092,24 +1204,32 @@ export const internationalQwenModels = {
cacheReadsPrice: 7,
},
"qwen-plus-latest": {
maxTokens: 129_024,
maxTokens: 16_384,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.8,
outputPrice: 2,
cacheWritesPrice: 0.8,
cacheReadsPrice: 0.2,
cacheReadsPrice: 2,
thinkingConfig: {
maxBudget: 38_912,
outputPrice: 16,
},
},
"qwen-turbo-latest": {
maxTokens: 1_000_000,
maxTokens: 16_384,
contextWindow: 1_000_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.8,
outputPrice: 2,
cacheWritesPrice: 0.8,
cacheReadsPrice: 2,
inputPrice: 0.3,
outputPrice: 0.6,
cacheWritesPrice: 0.3,
cacheReadsPrice: 0.6,
thinkingConfig: {
maxBudget: 38_912,
outputPrice: 6,
},
},
"qwen-max-latest": {
maxTokens: 30_720,
@@ -1224,6 +1344,118 @@ 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,
@@ -1295,24 +1527,32 @@ export const mainlandQwenModels = {
cacheReadsPrice: 7,
},
"qwen-plus-latest": {
maxTokens: 129_024,
maxTokens: 16_384,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.8,
outputPrice: 2,
cacheWritesPrice: 0.8,
cacheReadsPrice: 0.2,
cacheReadsPrice: 2,
thinkingConfig: {
maxBudget: 38_912,
outputPrice: 16,
},
},
"qwen-turbo-latest": {
maxTokens: 1_000_000,
maxTokens: 16_384,
contextWindow: 1_000_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.8,
outputPrice: 2,
cacheWritesPrice: 0.8,
cacheReadsPrice: 2,
inputPrice: 0.3,
outputPrice: 0.6,
cacheWritesPrice: 0.3,
cacheReadsPrice: 0.6,
thinkingConfig: {
maxBudget: 38_912,
outputPrice: 6,
},
},
"qwen-max-latest": {
maxTokens: 30_720,
@@ -1639,6 +1879,14 @@ 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,
@@ -1663,6 +1911,30 @@ 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
@@ -0,0 +1,253 @@
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
}
@@ -0,0 +1,106 @@
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
}
@@ -0,0 +1,11 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { createGrpcClient } from "./host-grpc-client-base"
import { UriServiceDefinition } from "@shared/proto/host/uri"
import { WatchServiceDefinition } from "@shared/proto/host/watch"
const UriServiceClient = createGrpcClient(UriServiceDefinition)
const WatchServiceClient = createGrpcClient(WatchServiceDefinition)
export { UriServiceClient, WatchServiceClient }
+7
View File
@@ -0,0 +1,7 @@
# 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
@@ -0,0 +1,7 @@
# 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
@@ -0,0 +1,7 @@
# 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
@@ -0,0 +1,7 @@
# 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
@@ -0,0 +1,7 @@
# 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)
@@ -151,6 +151,8 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
return (cacheReads !== undefined && cacheReads > 0) || (cacheWrites !== undefined && cacheWrites > 0)
}
console.log("IS_DEV", { IS_DEV, isItTrue: IS_DEV === '"true"' })
const ContextWindowComponent = (
<>
{isTaskExpanded && contextWindow && (
@@ -14,8 +14,7 @@ import { vscode } from "@/utils/vscode"
import McpMarketplaceCard from "./McpMarketplaceCard"
import McpSubmitCard from "./McpSubmitCard"
const McpMarketplaceView = () => {
const { mcpServers } = useExtensionState()
const [items, setItems] = useState<McpMarketplaceItem[]>([])
const { mcpServers, mcpMarketplaceCatalog } = useExtensionState()
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [isRefreshing, setIsRefreshing] = useState(false)
@@ -23,6 +22,8 @@ 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()
@@ -58,16 +59,7 @@ const McpMarketplaceView = () => {
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
const message = event.data
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.type === "mcpDownloadDetails") {
if (message.error) {
setError(message.error)
}
@@ -76,7 +68,7 @@ const McpMarketplaceView = () => {
window.addEventListener("message", handleMessage)
// Fetch marketplace catalog
// Fetch marketplace catalog on initial load
fetchMarketplace()
return () => {
@@ -84,6 +76,15 @@ 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,6 +116,23 @@ 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
@@ -2178,25 +2195,13 @@ const ApiOptions = ({
{selectedProvider === "nebius" && createDropdown(nebiusModels)}
</DropdownContainer>
{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}
/>
)}
{SUPPORTED_THINKING_MODELS[selectedProvider]?.includes(selectedModelId) && (
<ThinkingBudgetSlider
apiConfiguration={apiConfiguration}
setApiConfiguration={setApiConfiguration}
maxBudget={selectedModelInfo.thinkingConfig?.maxBudget}
/>
)}
{selectedProvider === "xai" && selectedModelId.includes("3-mini") && (
<>
@@ -126,6 +126,10 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
setEnableCheckpointsSetting,
mcpMarketplaceEnabled,
setMcpMarketplaceEnabled,
shellIntegrationTimeout,
setShellIntegrationTimeout,
terminalReuseEnabled,
setTerminalReuseEnabled,
setApiConfiguration,
} = useExtensionState()
@@ -138,6 +142,8 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
enableCheckpointsSetting,
mcpMarketplaceEnabled,
chatSettings,
shellIntegrationTimeout,
terminalReuseEnabled,
})
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
@@ -178,6 +184,8 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
telemetrySetting,
enableCheckpointsSetting,
mcpMarketplaceEnabled,
shellIntegrationTimeout,
terminalReuseEnabled,
apiConfiguration: apiConfigurationToSubmit,
})
@@ -200,7 +208,9 @@ 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)
JSON.stringify(chatSettings) !== JSON.stringify(originalState.current.chatSettings) ||
shellIntegrationTimeout !== originalState.current.shellIntegrationTimeout ||
terminalReuseEnabled !== originalState.current.terminalReuseEnabled
setHasUnsavedChanges(hasChanges)
}, [
@@ -211,6 +221,8 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
enableCheckpointsSetting,
mcpMarketplaceEnabled,
chatSettings,
shellIntegrationTimeout,
terminalReuseEnabled,
])
// Handle cancel button click
@@ -241,6 +253,13 @@ 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,11 +1,12 @@
import React, { useState } from "react"
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { VSCodeTextField, VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { StateServiceClient } from "@/services/grpc-client"
import { Int64, Int64Request } from "@shared/proto/common"
export const TerminalSettingsSection: React.FC = () => {
const { shellIntegrationTimeout, setShellIntegrationTimeout } = useExtensionState()
const { shellIntegrationTimeout, setShellIntegrationTimeout, terminalReuseEnabled, setTerminalReuseEnabled } =
useExtensionState()
const [inputValue, setInputValue] = useState((shellIntegrationTimeout / 1000).toString())
const [inputError, setInputError] = useState<string | null>(null)
@@ -48,6 +49,17 @@ 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 }}>
@@ -73,6 +85,20 @@ 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>
)
}
+292 -101
View File
@@ -1,8 +1,8 @@
import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from "react"
import { useEvent } from "react-use"
import { StateServiceClient, UiServiceClient, ModelsServiceClient } 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,6 +18,7 @@ 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 { vscode } from "../utils/vscode"
@@ -51,6 +52,7 @@ 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
@@ -89,6 +91,9 @@ 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)
@@ -171,6 +176,7 @@ 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)
@@ -191,98 +197,10 @@ 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
case "accountButtonClicked":
navigateToAccount()
break
case "chatButtonClicked":
navigateToChat()
break
}
break
}
case "state": {
// Handler for direct state messages
if (message.state) {
const stateData = message.state as ExtensionState
console.log("[Webview Context Test Revert] Received direct 'state' message, updating state.")
setState((prevState) => {
// Versioning logic for autoApprovalSettings (copied from original onResponse)
const incomingVersion = stateData.autoApprovalSettings?.version ?? 1
const currentVersion = prevState.autoApprovalSettings?.version ?? 1
const shouldUpdateAutoApproval = incomingVersion > currentVersion
const newState = {
...stateData,
autoApprovalSettings: shouldUpdateAutoApproval
? stateData.autoApprovalSettings
: prevState.autoApprovalSettings,
}
// Update welcome screen state based on API configuration (copied from original onResponse)
const config = stateData.apiConfiguration
const hasKey = config
? [
config.apiKey,
config.openRouterApiKey,
config.awsRegion,
config.vertexProjectId,
config.openAiApiKey,
config.ollamaModelId,
config.lmStudioModelId,
config.liteLlmApiKey,
config.geminiApiKey,
config.openAiNativeApiKey,
config.deepSeekApiKey,
config.requestyApiKey,
config.togetherApiKey,
config.qwenApiKey,
config.doubaoApiKey,
config.mistralApiKey,
config.vsCodeLmModelSelector,
config.clineApiKey,
config.asksageApiKey,
config.xaiApiKey,
config.sambanovaApiKey,
].some((key) => key !== undefined)
: false
setShowWelcome(!hasKey)
setDidHydrateState(true)
return newState
})
}
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({
@@ -308,12 +226,6 @@ export const ExtensionStateContextProvider: React.FC<{
setMcpServers(message.mcpServers ?? [])
break
}
case "mcpMarketplaceCatalog": {
if (message.mcpMarketplaceCatalog) {
setMcpMarketplaceCatalog(message.mcpMarketplaceCatalog)
}
break
}
}
}, [])
@@ -323,15 +235,289 @@ export const ExtensionStateContextProvider: React.FC<{
const stateSubscriptionRef = useRef<(() => void) | null>(null)
const mcpButtonUnsubscribeRef = useRef<(() => void) | null>(null)
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)
// For the test revert, ensure webviewDidLaunch is still sent if not done by the above useEffect
// Subscribe to state updates and UI events using the gRPC streaming API
useEffect(() => {
// This effect now only sends webviewDidLaunch if the gRPC subscription is commented out.
// If the gRPC subscription is active, it sends webviewDidLaunch.
// To avoid sending it twice if you uncomment the above, you might add a flag.
// For this specific test (gRPC sub commented out), this is fine.
console.log("[Webview Context Test Revert] Sending webviewDidLaunch from separate useEffect.")
// Determine the webview provider type
const webviewType =
window.WEBVIEW_PROVIDER_TYPE === "sidebar" ? WebviewProviderTypeEnum.SIDEBAR : WebviewProviderTypeEnum.TAB
// Set up state subscription
stateSubscriptionRef.current = StateServiceClient.subscribeToState(EmptyRequest.create({}), {
onResponse: (response) => {
if (response.stateJson) {
try {
const stateData = JSON.parse(response.stateJson) as ExtensionState
console.log("[DEBUG] parsed state JSON, updating state")
setState((prevState) => {
// Versioning logic for autoApprovalSettings
const incomingVersion = stateData.autoApprovalSettings?.version ?? 1
const currentVersion = prevState.autoApprovalSettings?.version ?? 1
const shouldUpdateAutoApproval = incomingVersion > currentVersion
const newState = {
...stateData,
autoApprovalSettings: shouldUpdateAutoApproval
? stateData.autoApprovalSettings
: prevState.autoApprovalSettings,
}
// Update welcome screen state based on API configuration
const config = stateData.apiConfiguration
const hasKey = config
? [
config.apiKey,
config.openRouterApiKey,
config.awsRegion,
config.vertexProjectId,
config.openAiApiKey,
config.ollamaModelId,
config.lmStudioModelId,
config.liteLlmApiKey,
config.geminiApiKey,
config.openAiNativeApiKey,
config.deepSeekApiKey,
config.requestyApiKey,
config.togetherApiKey,
config.qwenApiKey,
config.doubaoApiKey,
config.mistralApiKey,
config.vsCodeLmModelSelector,
config.clineApiKey,
config.asksageApiKey,
config.xaiApiKey,
config.sambanovaApiKey,
].some((key) => key !== undefined)
: false
setShowWelcome(!hasKey)
setDidHydrateState(true)
console.log("[DEBUG] returning new state in ESC")
return newState
})
} catch (error) {
console.error("Error parsing state JSON:", error)
console.log("[DEBUG] ERR getting state", error)
}
}
console.log('[DEBUG] ended "got subscribed state"')
},
onError: (error) => {
console.error("Error in state subscription:", error)
},
onComplete: () => {
console.log("State subscription completed")
},
})
// Subscribe to MCP button clicked events with webview type
mcpButtonUnsubscribeRef.current = UiServiceClient.subscribeToMcpButtonClicked(
WebviewProviderTypeRequest.create({
providerType: webviewType,
}),
{
onResponse: () => {
console.log("[DEBUG] Received mcpButtonClicked event from gRPC stream")
navigateToMcp()
},
onError: (error) => {
console.error("Error in mcpButtonClicked subscription:", error)
},
onComplete: () => {
console.log("mcpButtonClicked subscription completed")
},
},
)
// Set up history button clicked subscription with webview type
historyButtonClickedSubscriptionRef.current = UiServiceClient.subscribeToHistoryButtonClicked(
WebviewProviderTypeRequest.create({
providerType: webviewType,
}),
{
onResponse: () => {
// When history button is clicked, navigate to history view
console.log("[DEBUG] Received history button clicked event from gRPC stream")
navigateToHistory()
},
onError: (error) => {
console.error("Error in history button clicked subscription:", error)
},
onComplete: () => {
console.log("History button clicked subscription completed")
},
},
)
// Subscribe to chat button clicked events with webview type
chatButtonUnsubscribeRef.current = UiServiceClient.subscribeToChatButtonClicked(EmptyRequest.create({}), {
onResponse: () => {
// When chat button is clicked, navigate to chat
console.log("[DEBUG] Received chat button clicked event from gRPC stream")
navigateToChat()
},
onError: (error) => {
console.error("Error in chat button subscription:", error)
},
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")
},
})
// Still send the webviewDidLaunch message for other initialization
vscode.postMessage({ type: "webviewDidLaunch" })
// Set up account button clicked subscription
accountButtonClickedSubscriptionRef.current = UiServiceClient.subscribeToAccountButtonClicked(EmptyRequest.create(), {
onResponse: () => {
// When account button is clicked, navigate to account view
console.log("[DEBUG] Received account button clicked event from gRPC stream")
navigateToAccount()
},
onError: (error) => {
console.error("Error in account button clicked subscription:", error)
},
onComplete: () => {
console.log("Account button clicked subscription completed")
},
})
// Clean up subscriptions when component unmounts
return () => {
if (stateSubscriptionRef.current) {
stateSubscriptionRef.current()
stateSubscriptionRef.current = null
}
if (mcpButtonUnsubscribeRef.current) {
mcpButtonUnsubscribeRef.current()
mcpButtonUnsubscribeRef.current = null
}
if (historyButtonClickedSubscriptionRef.current) {
historyButtonClickedSubscriptionRef.current()
historyButtonClickedSubscriptionRef.current = null
}
if (chatButtonUnsubscribeRef.current) {
chatButtonUnsubscribeRef.current()
chatButtonUnsubscribeRef.current = null
}
if (accountButtonClickedSubscriptionRef.current) {
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
}
}
}, [])
const refreshOpenRouterModels = useCallback(() => {
@@ -424,6 +610,11 @@ 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,
+16 -1
View File
@@ -10,6 +10,13 @@ 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
@@ -32,8 +39,16 @@ 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(message)
console.log("postMessage fallback: ", message)
}
}