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

|
||||
@@ -1,7 +0,0 @@
|
||||
# Deep Codebase Intelligence
|
||||
|
||||
**Cline starts with broad context and explores deeply where needed.**
|
||||
|
||||
Cline is designed with inherent codebase intelligence. It doesn't operate in a vacuum, but starts with a structural understanding of your project. Before making changes, it performs targeted agentic exploration to gain any additional specific context required, ensuring its actions are always well-informed and aligned with your architecture.
|
||||
|
||||

|
||||
@@ -1,7 +0,0 @@
|
||||
# Always Use the Best Models
|
||||
|
||||
**Connect your keys for Anthropic (Claude), Google (Gemini), OpenAI (GPT), and other leading LLMs.**
|
||||
|
||||
Cline puts you at the forefront of AI. Bring your own API keys for leading models like Anthropic (Claude), Google (Gemini), and OpenAI (GPT). Always leverage the most powerful State-of-the-Art (SOTA) capabilities, ensuring you control both cost and cutting-edge performance.
|
||||
|
||||

|
||||
@@ -1,7 +0,0 @@
|
||||
# Unlock Specialized Capabilities with MCP
|
||||
|
||||
**The Model Context Protocol (MCP) connects Cline to a world of powerful tools.**
|
||||
|
||||
Go beyond local code. With the Model Context Protocol (MCP), Cline accesses vital context from external datasources like databases and APIs. It can interact with these platforms and leverage a growing marketplace of specialized, secure tools to tackle complex, real-world development tasks.
|
||||
|
||||

|
||||
@@ -1,7 +0,0 @@
|
||||
# No Black Box: Full Visibility & Control
|
||||
|
||||
**Cline operates with complete transparency, showing you every file read and every proposed diff.**
|
||||
|
||||
Understand exactly what Cline is doing and why—no obfuscation. Review all actions and approve changes before they're made. Cline uses checkpoints, allowing you to easily revert if needed, maintaining full control over your codebase. With BYO-key, you also have clear cost transparency.
|
||||
|
||||

|
||||
+13
-14
@@ -14,7 +14,8 @@ import { vscode } from "@/utils/vscode"
|
||||
import McpMarketplaceCard from "./McpMarketplaceCard"
|
||||
import McpSubmitCard from "./McpSubmitCard"
|
||||
const McpMarketplaceView = () => {
|
||||
const { mcpServers, mcpMarketplaceCatalog } = useExtensionState()
|
||||
const { mcpServers } = useExtensionState()
|
||||
const [items, setItems] = useState<McpMarketplaceItem[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||
@@ -22,8 +23,6 @@ const McpMarketplaceView = () => {
|
||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(null)
|
||||
const [sortBy, setSortBy] = useState<"newest" | "stars" | "name" | "downloadCount">("downloadCount")
|
||||
|
||||
const items = mcpMarketplaceCatalog?.items || []
|
||||
|
||||
const categories = useMemo(() => {
|
||||
const uniqueCategories = new Set(items.map((item) => item.category))
|
||||
return Array.from(uniqueCategories).sort()
|
||||
@@ -59,7 +58,16 @@ const McpMarketplaceView = () => {
|
||||
useEffect(() => {
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
const message = event.data
|
||||
if (message.type === "mcpDownloadDetails") {
|
||||
if (message.type === "mcpMarketplaceCatalog") {
|
||||
if (message.error) {
|
||||
setError(message.error)
|
||||
} else {
|
||||
setItems(message.mcpMarketplaceCatalog?.items || [])
|
||||
setError(null)
|
||||
}
|
||||
setIsLoading(false)
|
||||
setIsRefreshing(false)
|
||||
} else if (message.type === "mcpDownloadDetails") {
|
||||
if (message.error) {
|
||||
setError(message.error)
|
||||
}
|
||||
@@ -68,7 +76,7 @@ const McpMarketplaceView = () => {
|
||||
|
||||
window.addEventListener("message", handleMessage)
|
||||
|
||||
// Fetch marketplace catalog on initial load
|
||||
// Fetch marketplace catalog
|
||||
fetchMarketplace()
|
||||
|
||||
return () => {
|
||||
@@ -76,15 +84,6 @@ const McpMarketplaceView = () => {
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
// Update loading state when catalog arrives
|
||||
if (mcpMarketplaceCatalog?.items) {
|
||||
setIsLoading(false)
|
||||
setIsRefreshing(false)
|
||||
setError(null)
|
||||
}
|
||||
}, [mcpMarketplaceCatalog])
|
||||
|
||||
const fetchMarketplace = (forceRefresh: boolean = false) => {
|
||||
if (forceRefresh) {
|
||||
setIsRefreshing(true)
|
||||
|
||||
@@ -116,23 +116,6 @@ const OpenRouterBalanceDisplay = ({ apiKey }: { apiKey: string }) => {
|
||||
)
|
||||
}
|
||||
|
||||
const SUPPORTED_THINKING_MODELS: Record<string, string[]> = {
|
||||
anthropic: ["claude-3-7-sonnet-20250219", "claude-sonnet-4-20250514", "claude-opus-4-20250514"],
|
||||
vertex: ["claude-3-7-sonnet@20250219", "claude-sonnet-4@20250514", "claude-opus-4@20250514"],
|
||||
qwen: [
|
||||
"qwen3-235b-a22b",
|
||||
"qwen3-32b",
|
||||
"qwen3-30b-a3b",
|
||||
"qwen3-14b",
|
||||
"qwen3-8b",
|
||||
"qwen3-4b",
|
||||
"qwen3-1.7b",
|
||||
"qwen3-0.6b",
|
||||
"qwen-plus-latest",
|
||||
"qwen-turbo-latest",
|
||||
],
|
||||
}
|
||||
|
||||
// This is necessary to ensure dropdown opens downward, important for when this is used in popup
|
||||
const DROPDOWN_Z_INDEX = OPENROUTER_MODEL_PICKER_Z_INDEX + 2 // Higher than the OpenRouterModelPicker's and ModelSelectorTooltip's z-index
|
||||
|
||||
@@ -2195,13 +2178,25 @@ const ApiOptions = ({
|
||||
{selectedProvider === "nebius" && createDropdown(nebiusModels)}
|
||||
</DropdownContainer>
|
||||
|
||||
{SUPPORTED_THINKING_MODELS[selectedProvider]?.includes(selectedModelId) && (
|
||||
<ThinkingBudgetSlider
|
||||
apiConfiguration={apiConfiguration}
|
||||
setApiConfiguration={setApiConfiguration}
|
||||
maxBudget={selectedModelInfo.thinkingConfig?.maxBudget}
|
||||
/>
|
||||
)}
|
||||
{selectedProvider === "anthropic" &&
|
||||
(selectedModelId === "claude-3-7-sonnet-20250219" ||
|
||||
selectedModelId === "claude-sonnet-4-20250514" ||
|
||||
selectedModelId === "claude-opus-4-20250514") && (
|
||||
<ThinkingBudgetSlider
|
||||
apiConfiguration={apiConfiguration}
|
||||
setApiConfiguration={setApiConfiguration}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedProvider === "vertex" &&
|
||||
(selectedModelId === "claude-3-7-sonnet@20250219" ||
|
||||
selectedModelId === "claude-sonnet-4@20250514" ||
|
||||
selectedModelId === "claude-opus-4@20250514") && (
|
||||
<ThinkingBudgetSlider
|
||||
apiConfiguration={apiConfiguration}
|
||||
setApiConfiguration={setApiConfiguration}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedProvider === "xai" && selectedModelId.includes("3-mini") && (
|
||||
<>
|
||||
|
||||
@@ -126,10 +126,6 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
setEnableCheckpointsSetting,
|
||||
mcpMarketplaceEnabled,
|
||||
setMcpMarketplaceEnabled,
|
||||
shellIntegrationTimeout,
|
||||
setShellIntegrationTimeout,
|
||||
terminalReuseEnabled,
|
||||
setTerminalReuseEnabled,
|
||||
setApiConfiguration,
|
||||
} = useExtensionState()
|
||||
|
||||
@@ -142,8 +138,6 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
enableCheckpointsSetting,
|
||||
mcpMarketplaceEnabled,
|
||||
chatSettings,
|
||||
shellIntegrationTimeout,
|
||||
terminalReuseEnabled,
|
||||
})
|
||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
|
||||
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
|
||||
@@ -184,8 +178,6 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
telemetrySetting,
|
||||
enableCheckpointsSetting,
|
||||
mcpMarketplaceEnabled,
|
||||
shellIntegrationTimeout,
|
||||
terminalReuseEnabled,
|
||||
apiConfiguration: apiConfigurationToSubmit,
|
||||
})
|
||||
|
||||
@@ -208,9 +200,7 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
planActSeparateModelsSetting !== originalState.current.planActSeparateModelsSetting ||
|
||||
enableCheckpointsSetting !== originalState.current.enableCheckpointsSetting ||
|
||||
mcpMarketplaceEnabled !== originalState.current.mcpMarketplaceEnabled ||
|
||||
JSON.stringify(chatSettings) !== JSON.stringify(originalState.current.chatSettings) ||
|
||||
shellIntegrationTimeout !== originalState.current.shellIntegrationTimeout ||
|
||||
terminalReuseEnabled !== originalState.current.terminalReuseEnabled
|
||||
JSON.stringify(chatSettings) !== JSON.stringify(originalState.current.chatSettings)
|
||||
|
||||
setHasUnsavedChanges(hasChanges)
|
||||
}, [
|
||||
@@ -221,8 +211,6 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
enableCheckpointsSetting,
|
||||
mcpMarketplaceEnabled,
|
||||
chatSettings,
|
||||
shellIntegrationTimeout,
|
||||
terminalReuseEnabled,
|
||||
])
|
||||
|
||||
// Handle cancel button click
|
||||
@@ -253,13 +241,6 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
|
||||
: false,
|
||||
)
|
||||
}
|
||||
// Reset terminal settings
|
||||
if (typeof setShellIntegrationTimeout === "function") {
|
||||
setShellIntegrationTimeout(originalState.current.shellIntegrationTimeout)
|
||||
}
|
||||
if (typeof setTerminalReuseEnabled === "function") {
|
||||
setTerminalReuseEnabled(originalState.current.terminalReuseEnabled ?? true)
|
||||
}
|
||||
// Close settings view
|
||||
onDone()
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import React, { useState } from "react"
|
||||
import { VSCodeTextField, VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
|
||||
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { StateServiceClient } from "@/services/grpc-client"
|
||||
import { Int64, Int64Request } from "@shared/proto/common"
|
||||
|
||||
export const TerminalSettingsSection: React.FC = () => {
|
||||
const { shellIntegrationTimeout, setShellIntegrationTimeout, terminalReuseEnabled, setTerminalReuseEnabled } =
|
||||
useExtensionState()
|
||||
const { shellIntegrationTimeout, setShellIntegrationTimeout } = useExtensionState()
|
||||
const [inputValue, setInputValue] = useState((shellIntegrationTimeout / 1000).toString())
|
||||
const [inputError, setInputError] = useState<string | null>(null)
|
||||
|
||||
@@ -49,17 +48,6 @@ export const TerminalSettingsSection: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleTerminalReuseChange = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
const checked = target.checked
|
||||
|
||||
// Update local state
|
||||
setTerminalReuseEnabled(checked)
|
||||
|
||||
// TODO: Send to extension using gRPC when the backend is ready
|
||||
// For now, we'll just update the local state
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="terminal-settings-section" style={{ marginBottom: 20 }}>
|
||||
<div style={{ marginBottom: 15 }}>
|
||||
@@ -85,20 +73,6 @@ export const TerminalSettingsSection: React.FC = () => {
|
||||
you experience terminal connection timeouts.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 15 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", marginBottom: 8 }}>
|
||||
<VSCodeCheckbox
|
||||
checked={terminalReuseEnabled ?? true}
|
||||
onChange={(event) => handleTerminalReuseChange(event as Event)}>
|
||||
Enable aggressive terminal reuse
|
||||
</VSCodeCheckbox>
|
||||
</div>
|
||||
<p style={{ fontSize: "12px", color: "var(--vscode-descriptionForeground)", margin: 0 }}>
|
||||
When enabled, Cline will reuse existing terminal windows that aren't in the current working directory. Disable
|
||||
this if you experience issues with task lockout after a terminal command.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from "react"
|
||||
import { useEvent } from "react-use"
|
||||
import { StateServiceClient, ModelsServiceClient, UiServiceClient } from "../services/grpc-client"
|
||||
import { EmptyRequest } from "@shared/proto/common"
|
||||
import { WebviewProviderType as WebviewProviderTypeEnum, WebviewProviderTypeRequest } from "@shared/proto/ui"
|
||||
import { convertProtoToClineMessage } from "@shared/proto-conversions/cline-message"
|
||||
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "@shared/AutoApprovalSettings"
|
||||
import { DEFAULT_BROWSER_SETTINGS } from "@shared/BrowserSettings"
|
||||
import { ChatSettings, DEFAULT_CHAT_SETTINGS } from "@shared/ChatSettings"
|
||||
@@ -18,11 +18,8 @@ import {
|
||||
requestyDefaultModelInfo,
|
||||
} from "../../../src/shared/api"
|
||||
import { McpMarketplaceCatalog, McpServer, McpViewTab } from "../../../src/shared/mcp"
|
||||
import { ModelsServiceClient, StateServiceClient, UiServiceClient, McpServiceClient } from "../services/grpc-client"
|
||||
import { convertTextMateToHljs } from "../utils/textMateToHljs"
|
||||
import { convertOpenRouterCompatibleModelInfoToModelInfoRecord } from "../../../src/shared/proto-conversions/models/openrouter-models-conversion"
|
||||
import { vscode } from "../utils/vscode"
|
||||
import { OpenRouterCompatibleModelInfo } from "@shared/proto/models"
|
||||
|
||||
interface ExtensionStateContextType extends ExtensionState {
|
||||
didHydrateState: boolean
|
||||
@@ -54,7 +51,6 @@ interface ExtensionStateContextType extends ExtensionState {
|
||||
setEnableCheckpointsSetting: (value: boolean) => void
|
||||
setMcpMarketplaceEnabled: (value: boolean) => void
|
||||
setShellIntegrationTimeout: (value: number) => void
|
||||
setTerminalReuseEnabled: (value: boolean) => void
|
||||
setChatSettings: (value: ChatSettings) => void
|
||||
setMcpServers: (value: McpServer[]) => void
|
||||
setGlobalClineRulesToggles: (toggles: Record<string, boolean>) => void
|
||||
@@ -93,9 +89,6 @@ const ExtensionStateContext = createContext<ExtensionStateContextType | undefine
|
||||
export const ExtensionStateContextProvider: React.FC<{
|
||||
children: React.ReactNode
|
||||
}> = ({ children }) => {
|
||||
// Get the current webview provider type
|
||||
const currentProviderType =
|
||||
window.WEBVIEW_PROVIDER_TYPE === "sidebar" ? WebviewProviderTypeEnum.SIDEBAR : WebviewProviderTypeEnum.TAB
|
||||
// UI view state
|
||||
const [showMcp, setShowMcp] = useState(false)
|
||||
const [mcpTab, setMcpTab] = useState<McpViewTab | undefined>(undefined)
|
||||
@@ -178,7 +171,6 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
localWorkflowToggles: {},
|
||||
globalWorkflowToggles: {},
|
||||
shellIntegrationTimeout: 4000, // default timeout for shell integration
|
||||
terminalReuseEnabled: true, // default to enabled for backward compatibility
|
||||
isNewUser: false,
|
||||
})
|
||||
const [didHydrateState, setDidHydrateState] = useState(false)
|
||||
@@ -199,10 +191,47 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
const handleMessage = useCallback((event: MessageEvent) => {
|
||||
const message: ExtensionMessage = event.data
|
||||
switch (message.type) {
|
||||
case "action": {
|
||||
switch (message.action!) {
|
||||
case "settingsButtonClicked":
|
||||
navigateToSettings()
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
case "theme": {
|
||||
if (message.text) {
|
||||
setTheme(convertTextMateToHljs(JSON.parse(message.text)))
|
||||
}
|
||||
break
|
||||
}
|
||||
case "workspaceUpdated": {
|
||||
setFilePaths(message.filePaths ?? [])
|
||||
break
|
||||
}
|
||||
case "partialMessage": {
|
||||
const partialMessage = message.partialMessage!
|
||||
setState((prevState) => {
|
||||
// worth noting it will never be possible for a more up-to-date message to be sent here or in normal messages post since the presentAssistantContent function uses lock
|
||||
const lastIndex = findLastIndex(prevState.clineMessages, (msg) => msg.ts === partialMessage.ts)
|
||||
if (lastIndex !== -1) {
|
||||
const newClineMessages = [...prevState.clineMessages]
|
||||
newClineMessages[lastIndex] = partialMessage
|
||||
return { ...prevState, clineMessages: newClineMessages }
|
||||
}
|
||||
return prevState
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case "openRouterModels": {
|
||||
const updatedModels = message.openRouterModels ?? {}
|
||||
setOpenRouterModels({
|
||||
[openRouterDefaultModelId]: openRouterDefaultModelInfo, // in case the extension sent a model list without the default model
|
||||
...updatedModels,
|
||||
})
|
||||
break
|
||||
}
|
||||
case "openAiModels": {
|
||||
const updatedModels = message.openAiModels ?? []
|
||||
setOpenAiModels(updatedModels)
|
||||
@@ -220,6 +249,12 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
setMcpServers(message.mcpServers ?? [])
|
||||
break
|
||||
}
|
||||
case "mcpMarketplaceCatalog": {
|
||||
if (message.mcpMarketplaceCatalog) {
|
||||
setMcpMarketplaceCatalog(message.mcpMarketplaceCatalog)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
@@ -231,11 +266,6 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
const historyButtonClickedSubscriptionRef = useRef<(() => void) | null>(null)
|
||||
const chatButtonUnsubscribeRef = useRef<(() => void) | null>(null)
|
||||
const accountButtonClickedSubscriptionRef = useRef<(() => void) | null>(null)
|
||||
const settingsButtonClickedSubscriptionRef = useRef<(() => void) | null>(null)
|
||||
const partialMessageUnsubscribeRef = useRef<(() => void) | null>(null)
|
||||
const mcpMarketplaceUnsubscribeRef = useRef<(() => void) | null>(null)
|
||||
const themeSubscriptionRef = useRef<(() => void) | null>(null)
|
||||
const openRouterModelsUnsubscribeRef = useRef<(() => void) | null>(null)
|
||||
|
||||
// Subscribe to state updates and UI events using the gRPC streaming API
|
||||
useEffect(() => {
|
||||
@@ -365,114 +395,6 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
onComplete: () => {},
|
||||
})
|
||||
|
||||
// Set up settings button clicked subscription
|
||||
settingsButtonClickedSubscriptionRef.current = UiServiceClient.subscribeToSettingsButtonClicked(
|
||||
WebviewProviderTypeRequest.create({
|
||||
providerType: currentProviderType,
|
||||
}),
|
||||
{
|
||||
onResponse: () => {
|
||||
// When settings button is clicked, navigate to settings
|
||||
navigateToSettings()
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Error in settings button clicked subscription:", error)
|
||||
},
|
||||
onComplete: () => {
|
||||
console.log("Settings button clicked subscription completed")
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// Subscribe to partial message events
|
||||
partialMessageUnsubscribeRef.current = UiServiceClient.subscribeToPartialMessage(EmptyRequest.create({}), {
|
||||
onResponse: (protoMessage) => {
|
||||
try {
|
||||
console.log("[PARTIAL] Received partialMessage event from gRPC stream")
|
||||
|
||||
// Validate critical fields
|
||||
if (!protoMessage.ts || protoMessage.ts <= 0) {
|
||||
console.error("Invalid timestamp in partial message:", protoMessage)
|
||||
return
|
||||
}
|
||||
|
||||
const partialMessage = convertProtoToClineMessage(protoMessage)
|
||||
console.log("[PARTIAL] Partial message:", partialMessage)
|
||||
console.log("\n")
|
||||
setState((prevState) => {
|
||||
// worth noting it will never be possible for a more up-to-date message to be sent here or in normal messages post since the presentAssistantContent function uses lock
|
||||
const lastIndex = findLastIndex(prevState.clineMessages, (msg) => msg.ts === partialMessage.ts)
|
||||
if (lastIndex !== -1) {
|
||||
const newClineMessages = [...prevState.clineMessages]
|
||||
newClineMessages[lastIndex] = partialMessage
|
||||
return { ...prevState, clineMessages: newClineMessages }
|
||||
}
|
||||
return prevState
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to process partial message:", error, protoMessage)
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Error in partialMessage subscription:", error)
|
||||
},
|
||||
onComplete: () => {
|
||||
console.log("[DEBUG] partialMessage subscription completed")
|
||||
},
|
||||
})
|
||||
|
||||
// Subscribe to MCP marketplace catalog updates
|
||||
mcpMarketplaceUnsubscribeRef.current = McpServiceClient.subscribeToMcpMarketplaceCatalog(EmptyRequest.create({}), {
|
||||
onResponse: (catalog) => {
|
||||
console.log("[DEBUG] Received MCP marketplace catalog update from gRPC stream")
|
||||
setMcpMarketplaceCatalog(catalog)
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Error in MCP marketplace catalog subscription:", error)
|
||||
},
|
||||
onComplete: () => {
|
||||
console.log("MCP marketplace catalog subscription completed")
|
||||
},
|
||||
})
|
||||
|
||||
// Subscribe to theme changes
|
||||
themeSubscriptionRef.current = UiServiceClient.subscribeToTheme(EmptyRequest.create({}), {
|
||||
onResponse: (response) => {
|
||||
if (response.value) {
|
||||
try {
|
||||
const themeData = JSON.parse(response.value)
|
||||
setTheme(convertTextMateToHljs(themeData))
|
||||
console.log("[DEBUG] Received theme update from gRPC stream")
|
||||
} catch (error) {
|
||||
console.error("Error parsing theme data:", error)
|
||||
}
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Error in theme subscription:", error)
|
||||
},
|
||||
onComplete: () => {
|
||||
console.log("Theme subscription completed")
|
||||
},
|
||||
})
|
||||
|
||||
// Subscribe to OpenRouter models updates
|
||||
openRouterModelsUnsubscribeRef.current = ModelsServiceClient.subscribeToOpenRouterModels(EmptyRequest.create({}), {
|
||||
onResponse: (response: OpenRouterCompatibleModelInfo) => {
|
||||
const models = response.models
|
||||
setOpenRouterModels({
|
||||
[openRouterDefaultModelId]: openRouterDefaultModelInfo, // in case the extension sent a model list without the default model
|
||||
...models,
|
||||
})
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Error in OpenRouter models subscription:", error)
|
||||
},
|
||||
onComplete: () => {
|
||||
console.log("OpenRouter models subscription completed")
|
||||
},
|
||||
})
|
||||
|
||||
// Still send the webviewDidLaunch message for other initialization
|
||||
vscode.postMessage({ type: "webviewDidLaunch" })
|
||||
|
||||
@@ -513,36 +435,15 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
accountButtonClickedSubscriptionRef.current()
|
||||
accountButtonClickedSubscriptionRef.current = null
|
||||
}
|
||||
if (settingsButtonClickedSubscriptionRef.current) {
|
||||
settingsButtonClickedSubscriptionRef.current()
|
||||
settingsButtonClickedSubscriptionRef.current = null
|
||||
}
|
||||
if (partialMessageUnsubscribeRef.current) {
|
||||
partialMessageUnsubscribeRef.current()
|
||||
partialMessageUnsubscribeRef.current = null
|
||||
}
|
||||
if (mcpMarketplaceUnsubscribeRef.current) {
|
||||
mcpMarketplaceUnsubscribeRef.current()
|
||||
mcpMarketplaceUnsubscribeRef.current = null
|
||||
}
|
||||
if (themeSubscriptionRef.current) {
|
||||
themeSubscriptionRef.current()
|
||||
themeSubscriptionRef.current = null
|
||||
}
|
||||
if (openRouterModelsUnsubscribeRef.current) {
|
||||
openRouterModelsUnsubscribeRef.current()
|
||||
openRouterModelsUnsubscribeRef.current = null
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const refreshOpenRouterModels = useCallback(() => {
|
||||
ModelsServiceClient.refreshOpenRouterModels(EmptyRequest.create({}))
|
||||
.then((response: OpenRouterCompatibleModelInfo) => {
|
||||
const models = response.models
|
||||
.then((res) => {
|
||||
setOpenRouterModels({
|
||||
[openRouterDefaultModelId]: openRouterDefaultModelInfo, // in case the extension sent a model list without the default model
|
||||
...models,
|
||||
...res.models,
|
||||
})
|
||||
})
|
||||
.catch((error: Error) => console.error("Failed to refresh OpenRouter models:", error))
|
||||
@@ -627,11 +528,6 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
...prevState,
|
||||
shellIntegrationTimeout: value,
|
||||
})),
|
||||
setTerminalReuseEnabled: (value) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
terminalReuseEnabled: value,
|
||||
})),
|
||||
setMcpServers: (mcpServers: McpServer[]) => setMcpServers(mcpServers),
|
||||
setMcpMarketplaceCatalog: (catalog: McpMarketplaceCatalog) => setMcpMarketplaceCatalog(catalog),
|
||||
setShowMcp,
|
||||
|
||||
@@ -10,13 +10,6 @@ import type { WebviewApi } from "vscode-webview"
|
||||
* dev server by using native web browser features that mock the functionality
|
||||
* enabled by acquireVsCodeApi.
|
||||
*/
|
||||
declare global {
|
||||
interface Window {
|
||||
__is_standalone__?: boolean
|
||||
standalonePostMessage?: (event: any) => void
|
||||
}
|
||||
}
|
||||
|
||||
class VSCodeAPIWrapper {
|
||||
private readonly vsCodeApi: WebviewApi<unknown> | undefined
|
||||
|
||||
@@ -39,16 +32,8 @@ class VSCodeAPIWrapper {
|
||||
public postMessage(message: WebviewMessage) {
|
||||
if (this.vsCodeApi) {
|
||||
this.vsCodeApi.postMessage(message)
|
||||
} else if (window.__is_standalone__) {
|
||||
if (!window.standalonePostMessage) {
|
||||
console.warn("Standalone postMessage not found.")
|
||||
return
|
||||
}
|
||||
const json = JSON.stringify(message)
|
||||
console.log("Standalone postMessage: " + json.slice(0, 200))
|
||||
window.standalonePostMessage(json)
|
||||
} else {
|
||||
console.log("postMessage fallback: ", message)
|
||||
console.log(message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user