PROTOBUS: Streaming, State, Service Auto-Config (#3253)

* round 1

* round 2 - searchFiles integration attempt

* undo streaming search experiments

* Start state.proto and related migrations

* state subscription

* get the main state flow using it

* correct stream ending early, debug statements

* clean up build-proto service config

* autogenerate index.tses

* auto-generate grpc-client service exports

* rename web-content -> web to make codegen work

* cleaned up streaming flow & cancels

* v3.14.0 Release Notes

v3.14.0 Release Notes

* prettier

* uhh prettier ?

* rename GrpcRequestRegistry file

* auto-generate directory for new services in the config

* generate template proto if it doesn't exist and provide instructions

* format fix

* add models service back to new system

---------

Co-authored-by: Andrei Edell <andrei@nugbase.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Andrei Eternal 2025-05-06 13:42:36 -07:00 committed by GitHub
parent 311cb3ac0a
commit cfc133acd3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 1406 additions and 283 deletions

View File

@ -12,11 +12,27 @@ const require = createRequire(import.meta.url)
const protoc = path.join(require.resolve("grpc-tools"), "../bin/protoc")
const tsProtoPlugin = require.resolve("ts-proto/protoc-gen-ts_proto")
// Get script directory and root directory
const __filename = fileURLToPath(import.meta.url)
const SCRIPT_DIR = path.dirname(__filename)
const ROOT_DIR = path.resolve(SCRIPT_DIR, "..")
// List of gRPC services
// To add a new service, simply add it to this map and run this script
// The service handler will be automatically discovered and used by grpc-handler.ts
const serviceNameMap = {
account: "cline.AccountService",
browser: "cline.BrowserService",
checkpoints: "cline.CheckpointsService",
file: "cline.FileService",
mcp: "cline.McpService",
state: "cline.StateService",
task: "cline.TaskService",
web: "cline.WebService",
models: "cline.ModelsService",
// Add new services here - no other code changes needed!
}
const serviceDirs = Object.keys(serviceNameMap).map((serviceKey) => path.join(ROOT_DIR, "src", "core", "controller", serviceKey))
async function main() {
console.log(chalk.bold.blue("Starting Protocol Buffer code generation..."))
@ -33,6 +49,9 @@ async function main() {
await fs.unlink(path.join(TS_OUT_DIR, file))
}
// Check for missing proto files for services in serviceNameMap
await ensureProtoFilesExist()
// Process all proto files
console.log(chalk.cyan("Processing proto files from"), SCRIPT_DIR)
const protoFiles = await globby("*.proto", { cwd: SCRIPT_DIR })
@ -64,42 +83,135 @@ async function main() {
console.log(chalk.green("Protocol Buffer code generation completed successfully."))
console.log(chalk.green(`TypeScript files generated in: ${TS_OUT_DIR}`))
// Generate method registration files
await generateMethodRegistrations()
await generateServiceConfig()
await generateGrpcClientConfig()
}
// Make the script executable
try {
await fs.chmod(path.join(SCRIPT_DIR, "build-proto.js"), 0o755)
} catch (error) {
console.warn(chalk.yellow("Warning: Could not make script executable:"), error)
/**
* Generate a gRPC client configuration file for the webview
* This eliminates the need for manual imports and client creation in grpc-client.ts
*/
async function generateGrpcClientConfig() {
console.log(chalk.cyan("Generating gRPC client configuration..."))
const serviceImports = []
const serviceClientCreations = []
const serviceExports = []
// Process each service in the serviceNameMap
for (const [dirName, fullServiceName] of Object.entries(serviceNameMap)) {
const capitalizedName = dirName.charAt(0).toUpperCase() + dirName.slice(1)
// Add import statement
serviceImports.push(`import { ${capitalizedName}ServiceDefinition } from "@shared/proto/${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 "./grpc-client-base"
${serviceImports.join("\n")}
${serviceClientCreations.join("\n")}
export {
${serviceExports.join(",\n\t")}
}`
const configPath = path.join(ROOT_DIR, "webview-ui", "src", "services", "grpc-client.ts")
await fs.writeFile(configPath, content)
console.log(chalk.green(`Generated gRPC client at ${configPath}`))
}
/**
* Parse proto files to extract streaming method information
* @param protoFiles Array of proto file names
* @param scriptDir Directory containing proto files
* @returns Map of service names to their streaming methods
*/
async function parseProtoForStreamingMethods(protoFiles, scriptDir) {
console.log(chalk.cyan("Parsing proto files for streaming methods..."))
// Map of service name to array of streaming method names
const streamingMethodsMap = new Map()
for (const protoFile of protoFiles) {
const content = await fs.readFile(path.join(scriptDir, protoFile), "utf8")
// Extract package name
const packageMatch = content.match(/package\s+([^;]+);/)
const packageName = packageMatch ? packageMatch[1].trim() : "unknown"
// Extract service definitions
const serviceMatches = Array.from(content.matchAll(/service\s+(\w+)\s*\{([^}]+)\}/g))
for (const serviceMatch of serviceMatches) {
const serviceName = serviceMatch[1]
const serviceBody = serviceMatch[2]
const fullServiceName = `${packageName}.${serviceName}`
// Extract method definitions with streaming
const methodMatches = Array.from(
serviceBody.matchAll(/rpc\s+(\w+)\s*\(\s*(stream\s+)?(\w+)\s*\)\s*returns\s*\(\s*(stream\s+)?(\w+)\s*\)/g),
)
const streamingMethods = []
for (const methodMatch of methodMatches) {
const methodName = methodMatch[1]
const isRequestStreaming = !!methodMatch[2]
const requestType = methodMatch[3]
const isResponseStreaming = !!methodMatch[4]
const responseType = methodMatch[5]
if (isResponseStreaming) {
streamingMethods.push({
name: methodName,
requestType,
responseType,
isRequestStreaming,
})
}
}
if (streamingMethods.length > 0) {
streamingMethodsMap.set(fullServiceName, streamingMethods)
}
}
}
return streamingMethodsMap
}
async function generateMethodRegistrations() {
console.log(chalk.cyan("Generating method registration files..."))
const serviceDirs = [
path.join(ROOT_DIR, "src", "core", "controller", "account"),
path.join(ROOT_DIR, "src", "core", "controller", "browser"),
path.join(ROOT_DIR, "src", "core", "controller", "checkpoints"),
path.join(ROOT_DIR, "src", "core", "controller", "file"),
path.join(ROOT_DIR, "src", "core", "controller", "mcp"),
path.join(ROOT_DIR, "src", "core", "controller", "models"),
path.join(ROOT_DIR, "src", "core", "controller", "task"),
path.join(ROOT_DIR, "src", "core", "controller", "web-content"),
// Add more service directories here as needed
]
// Parse proto files for streaming methods
const protoFiles = await globby("*.proto", { cwd: SCRIPT_DIR })
const streamingMethodsMap = await parseProtoForStreamingMethods(protoFiles, SCRIPT_DIR)
for (const serviceDir of serviceDirs) {
try {
await fs.access(serviceDir)
} catch (error) {
console.log(chalk.gray(`Skipping ${serviceDir} - directory does not exist`))
continue
console.log(chalk.cyan(`Creating directory ${serviceDir} for new 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 = serviceNameMap[serviceName]
const streamingMethods = streamingMethodsMap.get(fullServiceName) || []
console.log(chalk.cyan(`Generating method registrations for ${serviceName}...`))
@ -109,8 +221,8 @@ async function generateMethodRegistrations() {
// Filter out index.ts and methods.ts
const implementationFiles = files.filter((file) => file !== "index.ts" && file !== "methods.ts")
// Create the output file with header
let content = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// 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
@ -119,31 +231,177 @@ import { registerMethod } from "./index"\n`
// Add imports for all implementation files
for (const file of implementationFiles) {
const baseName = path.basename(file, ".ts")
content += `import { ${baseName} } from "./${baseName}"\n`
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
content += `\n// Register all ${serviceName} service methods
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")
content += `\tregisterMethod("${baseName}", ${baseName})\n`
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
content += `}`
methodsContent += `}`
// Write the file
await fs.writeFile(registryFile, content)
// 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 "../grpc-service"
import { StreamingResponseHandler } from "../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("Method registration files generated successfully."))
}
/**
* Generate a service configuration file that maps service names to their handlers
* This eliminates the need for manual switch/case statements in grpc-handler.ts
*/
async function generateServiceConfig() {
console.log(chalk.cyan("Generating service configuration file..."))
const serviceImports = []
const serviceConfigs = []
// Add all services from the serviceNameMap
for (const [dirName, fullServiceName] of Object.entries(serviceNameMap)) {
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 { Controller } from "./index"
import { StreamingResponseHandler } from "./grpc-handler"
${serviceImports.join("\n")}
/**
* Configuration for a service handler
*/
export interface ServiceHandlerConfig {
requestHandler: (controller: Controller, method: string, message: any) => Promise<any>;
streamingHandler: (controller: Controller, method: string, message: any, responseStream: StreamingResponseHandler, requestId?: string) => Promise<void>;
}
/**
* Map of service names to their handler configurations
*/
export const serviceHandlers: Record<string, ServiceHandlerConfig> = {${serviceConfigs.join(",")}
};`
const configPath = path.join(ROOT_DIR, "src", "core", "controller", "grpc-service-config.ts")
await fs.writeFile(configPath, content)
console.log(chalk.green(`Generated service configuration at ${configPath}`))
}
/**
* Ensure that a .proto file exists for each service in the serviceNameMap
* If a .proto file doesn't exist, create a template file
*/
async function ensureProtoFilesExist() {
console.log(chalk.cyan("Checking for missing proto files..."))
// Get existing proto files
const existingProtoFiles = await globby("*.proto", { cwd: SCRIPT_DIR })
const existingProtoServices = existingProtoFiles.map((file) => path.basename(file, ".proto"))
// Check each service in serviceNameMap
for (const [serviceName, fullServiceName] of Object.entries(serviceNameMap)) {
if (!existingProtoServices.includes(serviceName)) {
console.log(chalk.yellow(`Creating template proto file for ${serviceName}...`))
// Extract service class name from full name (e.g., "cline.ModelsService" -> "ModelsService")
const serviceClassName = fullServiceName.split(".").pop()
// Create template proto file
const protoContent = `syntax = "proto3";
package cline;
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
// ${serviceClassName} provides methods for managing ${serviceName}
service ${serviceClassName} {
// Add your RPC methods here
// Example (String is from common.proto, responses should be generic types):
// rpc YourMethod(YourRequest) returns (String);
}
// Add your message definitions here
// Example (Requests must always start with Metadata):
// message YourRequest {
// Metadata metadata = 1;
// string stringField = 2;
// int32 int32Field = 3;
// }
`
// Write the template proto file
const protoFilePath = path.join(SCRIPT_DIR, `${serviceName}.proto`)
await fs.writeFile(protoFilePath, protoContent)
console.log(chalk.green(`Created template proto file at ${protoFilePath}`))
}
}
}
// Run the main function
main().catch((error) => {
console.error(chalk.red("Error:"), error)

13
proto/state.proto Normal file
View File

@ -0,0 +1,13 @@
syntax = "proto3";
package cline;
import "common.proto";
service StateService {
rpc getLatestState(EmptyRequest) returns (State);
rpc subscribeToState(EmptyRequest) returns (stream State);
}
message State {
string state_json = 1;
}

View File

@ -6,7 +6,7 @@ option java_multiple_files = true;
import "common.proto";
service WebContentService {
service WebService {
rpc checkIsImageUrl(StringRequest) returns (IsImageUrl);
}

View File

@ -1,14 +1,22 @@
import { createServiceRegistry, ServiceMethodHandler } from "../grpc-service"
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../grpc-service"
import { StreamingResponseHandler } from "../grpc-handler"
import { registerAllMethods } from "./methods"
// Create account service registry
const accountService = createServiceRegistry("account")
// Export the method handler type and registration function
// Export the method handler types and registration function
export type AccountMethodHandler = ServiceMethodHandler
export type AccountStreamingMethodHandler = StreamingMethodHandler
export const registerMethod = accountService.registerMethod
// Export the request handler
// Export the request handlers
export const handleAccountServiceRequest = accountService.handleRequest
export const handleAccountServiceStreamingRequest = accountService.handleStreamingRequest
export const isStreamingMethod = accountService.isStreamingMethod
// Register all account methods
registerAllMethods()

View File

@ -1,15 +1,22 @@
import { createServiceRegistry, ServiceMethodHandler } from "../grpc-service"
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../grpc-service"
import { StreamingResponseHandler } from "../grpc-handler"
import { registerAllMethods } from "./methods"
// Create browser service registry
const browserService = createServiceRegistry("browser")
// Export the method handler type and registration function
// Export the method handler types and registration function
export type BrowserMethodHandler = ServiceMethodHandler
export type BrowserStreamingMethodHandler = StreamingMethodHandler
export const registerMethod = browserService.registerMethod
// Export the request handler
// Export the request handlers
export const handleBrowserServiceRequest = browserService.handleRequest
export const handleBrowserServiceStreamingRequest = browserService.handleStreamingRequest
export const isStreamingMethod = browserService.isStreamingMethod
// Register all browser methods
registerAllMethods()

View File

@ -1,15 +1,22 @@
import { createServiceRegistry, ServiceMethodHandler } from "../grpc-service"
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../grpc-service"
import { StreamingResponseHandler } from "../grpc-handler"
import { registerAllMethods } from "./methods"
// Create checkpoints service registry
const checkpointsService = createServiceRegistry("checkpoints")
// Export the method handler type and registration function
// Export the method handler types and registration function
export type CheckpointsMethodHandler = ServiceMethodHandler
export type CheckpointsStreamingMethodHandler = StreamingMethodHandler
export const registerMethod = checkpointsService.registerMethod
// Export the request handler
// Export the request handlers
export const handleCheckpointsServiceRequest = checkpointsService.handleRequest
export const handleCheckpointsServiceStreamingRequest = checkpointsService.handleStreamingRequest
export const isStreamingMethod = checkpointsService.isStreamingMethod
// Register all checkpoints methods
registerAllMethods()

View File

@ -1,15 +1,22 @@
import { createServiceRegistry, ServiceMethodHandler } from "../grpc-service"
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../grpc-service"
import { StreamingResponseHandler } from "../grpc-handler"
import { registerAllMethods } from "./methods"
// Create file service registry
const fileService = createServiceRegistry("file")
// Export the method handler type and registration function
// Export the method handler types and registration function
export type FileMethodHandler = ServiceMethodHandler
export type FileStreamingMethodHandler = StreamingMethodHandler
export const registerMethod = fileService.registerMethod
// Export the request handler
// Export the request handlers
export const handleFileServiceRequest = fileService.handleRequest
export const handleFileServiceStreamingRequest = fileService.handleStreamingRequest
export const isStreamingMethod = fileService.isStreamingMethod
// Register all file methods
registerAllMethods()

View File

@ -1,12 +1,11 @@
import { Controller } from "./index"
import { handleAccountServiceRequest } from "./account"
import { handleBrowserServiceRequest } from "./browser/index"
import { handleFileServiceRequest } from "./file"
import { handleTaskServiceRequest } from "./task"
import { handleCheckpointsServiceRequest } from "./checkpoints"
import { handleMcpServiceRequest } from "./mcp"
import { handleWebContentServiceRequest } from "./web-content"
import { handleModelsServiceRequest } from "./models"
import { serviceHandlers } from "./grpc-service-config"
import { GrpcRequestRegistry } from "./grpc-request-registry"
/**
* Type definition for a streaming response handler
*/
export type StreamingResponseHandler = (response: any, isLast?: boolean, sequenceNumber?: number) => Promise<void>
/**
* Handles gRPC requests from the webview
@ -20,62 +19,37 @@ export class GrpcHandler {
* @param method The method name
* @param message The request message
* @param requestId The request ID for response correlation
* @returns The response message or error
* @param isStreaming Whether this is a streaming request
* @returns The response message or error for unary requests, void for streaming requests
*/
async handleRequest(
service: string,
method: string,
message: any,
requestId: string,
isStreaming: boolean = false,
): Promise<{
message?: any
error?: string
request_id: string
}> {
} | void> {
try {
switch (service) {
case "cline.AccountService":
return {
message: await handleAccountServiceRequest(this.controller, method, message),
request_id: requestId,
}
case "cline.BrowserService":
return {
message: await handleBrowserServiceRequest(this.controller, method, message),
request_id: requestId,
}
case "cline.CheckpointsService":
return {
message: await handleCheckpointsServiceRequest(this.controller, method, message),
request_id: requestId,
}
case "cline.FileService":
return {
message: await handleFileServiceRequest(this.controller, method, message),
request_id: requestId,
}
case "cline.TaskService":
return {
message: await handleTaskServiceRequest(this.controller, method, message),
request_id: requestId,
}
case "cline.McpService":
return {
message: await handleMcpServiceRequest(this.controller, method, message),
request_id: requestId,
}
case "cline.WebContentService":
return {
message: await handleWebContentServiceRequest(this.controller, method, message),
request_id: requestId,
}
case "cline.ModelsService":
return {
message: await handleModelsServiceRequest(this.controller, method, message),
request_id: requestId,
}
default:
throw new Error(`Unknown service: ${service}`)
// If this is a streaming request, use the streaming handler
if (isStreaming) {
await this.handleStreamingRequest(service, method, message, requestId)
return
}
// Get the service handler from the config
const serviceConfig = serviceHandlers[service]
if (!serviceConfig) {
throw new Error(`Unknown service: ${service}`)
}
// Handle unary request
return {
message: await serviceConfig.requestHandler(this.controller, method, message),
request_id: requestId,
}
} catch (error) {
return {
@ -84,8 +58,66 @@ export class GrpcHandler {
}
}
}
/**
* 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> {
// Create a response stream function
const responseStream: StreamingResponseHandler = async (
response: any,
isLast: boolean = false,
sequenceNumber?: number,
) => {
await this.controller.postMessageToWebview({
type: "grpc_response",
grpc_response: {
message: response,
request_id: requestId,
is_streaming: !isLast,
sequence_number: sequenceNumber,
},
})
}
try {
// Get the service handler from the config
const serviceConfig = serviceHandlers[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`)
}
// Handle streaming request and pass the requestId to all streaming handlers
await serviceConfig.streamingHandler(this.controller, 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
} catch (error) {
// Send error response
await this.controller.postMessageToWebview({
type: "grpc_response",
grpc_response: {
error: error instanceof Error ? error.message : String(error),
request_id: requestId,
is_streaming: false,
},
})
}
}
}
// Registry to track active gRPC requests and their cleanup functions
const requestRegistry = new GrpcRequestRegistry()
/**
* Handle a gRPC request from the webview
* @param controller The controller instance
@ -98,11 +130,35 @@ export async function handleGrpcRequest(
method: string
message: any
request_id: string
is_streaming?: boolean
},
) {
try {
const grpcHandler = new GrpcHandler(controller)
const response = await grpcHandler.handleRequest(request.service, request.method, request.message, request.request_id)
// For streaming requests, handleRequest handles sending responses directly
if (request.is_streaming) {
try {
await grpcHandler.handleRequest(request.service, request.method, request.message, request.request_id, true)
} finally {
// Note: We don't automatically clean up here anymore
// The request will be cleaned up when it completes or is cancelled
}
return
}
// For unary requests, we get a response and send it back
const response = (await grpcHandler.handleRequest(
request.service,
request.method,
request.message,
request.request_id,
false,
)) as {
message?: any
error?: string
request_id: string
}
// Send the response back to the webview
await controller.postMessageToWebview({
@ -120,3 +176,39 @@ export async function handleGrpcRequest(
})
}
}
/**
* Handle a gRPC request cancellation from the webview
* @param controller The controller instance
* @param request The cancellation request
*/
export async function handleGrpcRequestCancel(
controller: Controller,
request: {
request_id: string
},
) {
const cancelled = requestRegistry.cancelRequest(request.request_id)
if (cancelled) {
// Send a cancellation confirmation
await controller.postMessageToWebview({
type: "grpc_response",
grpc_response: {
message: { cancelled: true },
request_id: request.request_id,
is_streaming: false,
},
})
} else {
console.log(`[DEBUG] Request not found for cancellation: ${request.request_id}`)
}
}
/**
* Get the request registry instance
* This allows other parts of the code to access the registry
*/
export function getRequestRegistry(): GrpcRequestRegistry {
return requestRegistry
}

View File

@ -0,0 +1,124 @@
import { StreamingResponseHandler } from "./grpc-handler"
/**
* Information about a registered gRPC request
*/
export interface RequestInfo {
/**
* Function to clean up resources when the request is cancelled or completed
*/
cleanup: () => void
/**
* Optional metadata about the request
*/
metadata?: any
/**
* Timestamp when the request was registered
*/
timestamp: Date
/**
* The streaming response handler for this request
*/
responseStream?: StreamingResponseHandler
}
/**
* Registry for managing gRPC request lifecycles
* This class provides a centralized way to track active requests and their cleanup functions
*/
export class GrpcRequestRegistry {
/**
* Map of request IDs to request information
*/
private activeRequests = new Map<string, RequestInfo>()
/**
* Register a new request with its cleanup function
* @param requestId The unique ID of the request
* @param cleanup Function to clean up resources when the request is cancelled
* @param metadata Optional metadata about the request
* @param responseStream Optional streaming response handler
*/
public registerRequest(
requestId: string,
cleanup: () => void,
metadata?: any,
responseStream?: StreamingResponseHandler,
): void {
this.activeRequests.set(requestId, {
cleanup,
metadata,
timestamp: new Date(),
responseStream,
})
console.log(`[DEBUG] Registered request: ${requestId}`)
}
/**
* Cancel a request and clean up its resources
* @param requestId The ID of the request to cancel
* @returns True if the request was found and cancelled, false otherwise
*/
public cancelRequest(requestId: string): boolean {
const requestInfo = this.activeRequests.get(requestId)
if (requestInfo) {
try {
requestInfo.cleanup()
console.log(`[DEBUG] Cleaned up request: ${requestId}`)
} catch (error) {
console.error(`Error cleaning up request ${requestId}:`, error)
}
this.activeRequests.delete(requestId)
return true
}
return false
}
/**
* Get information about a request
* @param requestId The ID of the request
* @returns The request information, or undefined if not found
*/
public getRequestInfo(requestId: string): RequestInfo | undefined {
return this.activeRequests.get(requestId)
}
/**
* Check if a request exists in the registry
* @param requestId The ID of the request
* @returns True if the request exists, false otherwise
*/
public hasRequest(requestId: string): boolean {
return this.activeRequests.has(requestId)
}
/**
* Get all active requests
* @returns An array of [requestId, requestInfo] pairs
*/
public getAllRequests(): [string, RequestInfo][] {
return Array.from(this.activeRequests.entries())
}
/**
* Clean up stale requests that have been active for too long
* @param maxAgeMs Maximum age in milliseconds before a request is considered stale
* @returns The number of requests that were cleaned up
*/
public cleanupStaleRequests(maxAgeMs: number): number {
const now = new Date()
let cleanedCount = 0
for (const [requestId, info] of this.activeRequests.entries()) {
if (now.getTime() - info.timestamp.getTime() > maxAgeMs) {
this.cancelRequest(requestId)
cleanedCount++
}
}
return cleanedCount
}
}

View File

@ -0,0 +1,70 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { Controller } from "./index"
import { StreamingResponseHandler } from "./grpc-handler"
import { handleAccountServiceRequest, handleAccountServiceStreamingRequest } from "./account/index"
import { handleBrowserServiceRequest, handleBrowserServiceStreamingRequest } from "./browser/index"
import { handleCheckpointsServiceRequest, handleCheckpointsServiceStreamingRequest } from "./checkpoints/index"
import { handleFileServiceRequest, handleFileServiceStreamingRequest } from "./file/index"
import { handleMcpServiceRequest, handleMcpServiceStreamingRequest } from "./mcp/index"
import { handleStateServiceRequest, handleStateServiceStreamingRequest } from "./state/index"
import { handleTaskServiceRequest, handleTaskServiceStreamingRequest } from "./task/index"
import { handleWebServiceRequest, handleWebServiceStreamingRequest } from "./web/index"
import { handleModelsServiceRequest, handleModelsServiceStreamingRequest } from "./models/index"
/**
* Configuration for a service handler
*/
export interface ServiceHandlerConfig {
requestHandler: (controller: Controller, method: string, message: any) => Promise<any>
streamingHandler: (
controller: Controller,
method: string,
message: any,
responseStream: StreamingResponseHandler,
requestId?: string,
) => Promise<void>
}
/**
* Map of service names to their handler configurations
*/
export const serviceHandlers: Record<string, ServiceHandlerConfig> = {
"cline.AccountService": {
requestHandler: handleAccountServiceRequest,
streamingHandler: handleAccountServiceStreamingRequest,
},
"cline.BrowserService": {
requestHandler: handleBrowserServiceRequest,
streamingHandler: handleBrowserServiceStreamingRequest,
},
"cline.CheckpointsService": {
requestHandler: handleCheckpointsServiceRequest,
streamingHandler: handleCheckpointsServiceStreamingRequest,
},
"cline.FileService": {
requestHandler: handleFileServiceRequest,
streamingHandler: handleFileServiceStreamingRequest,
},
"cline.McpService": {
requestHandler: handleMcpServiceRequest,
streamingHandler: handleMcpServiceStreamingRequest,
},
"cline.StateService": {
requestHandler: handleStateServiceRequest,
streamingHandler: handleStateServiceStreamingRequest,
},
"cline.TaskService": {
requestHandler: handleTaskServiceRequest,
streamingHandler: handleTaskServiceStreamingRequest,
},
"cline.WebService": {
requestHandler: handleWebServiceRequest,
streamingHandler: handleWebServiceStreamingRequest,
},
"cline.ModelsService": {
requestHandler: handleModelsServiceRequest,
streamingHandler: handleModelsServiceStreamingRequest,
},
}

View File

@ -1,16 +1,36 @@
import { Controller } from "./index"
import { StreamingResponseHandler } from "./grpc-handler"
/**
* Generic type for service method handlers
*/
export type ServiceMethodHandler = (controller: Controller, message: any) => Promise<any>
/**
* Type for streaming method handlers
*/
export type StreamingMethodHandler = (
controller: Controller,
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
@ -24,10 +44,37 @@ export class ServiceRegistry {
* 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): void {
this.methodRegistry[methodName] = handler
console.log(`Registered ${this.serviceName} method: ${methodName}`)
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]
}
/**
@ -41,11 +88,41 @@ export class ServiceRegistry {
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(controller, message)
}
/**
* Handle a streaming service request
* @param controller The controller instance
* @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(
controller: Controller,
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(controller, message, responseStream, requestId)
}
}
/**
@ -57,9 +134,20 @@ export function createServiceRegistry(serviceName: string) {
const registry = new ServiceRegistry(serviceName)
return {
registerMethod: (methodName: string, handler: ServiceMethodHandler) => registry.registerMethod(methodName, handler),
registerMethod: (methodName: string, handler: ServiceMethodHandler | StreamingMethodHandler, metadata?: MethodMetadata) =>
registry.registerMethod(methodName, handler, metadata),
handleRequest: (controller: Controller, method: string, message: any) =>
registry.handleRequest(controller, method, message),
handleStreamingRequest: (
controller: Controller,
method: string,
message: any,
responseStream: StreamingResponseHandler,
requestId?: string,
) => registry.handleStreamingRequest(controller, method, message, responseStream, requestId),
isStreamingMethod: (method: string) => registry.isStreamingMethod(method),
}
}

View File

@ -7,7 +7,7 @@ import { setTimeout as setTimeoutPromise } from "node:timers/promises"
import pWaitFor from "p-wait-for"
import * as path from "path"
import * as vscode from "vscode"
import { handleGrpcRequest } from "./grpc-handler"
import { handleGrpcRequest, handleGrpcRequestCancel } from "./grpc-handler"
import { buildApiHandler } from "@api/index"
import { cleanupLegacyCheckpoints } from "@integrations/checkpoints/CheckpointMigration"
import { downloadTask } from "@integrations/misc/export-markdown"
@ -48,6 +48,7 @@ import {
} from "../storage/state"
import { Task, cwd } from "../task"
import { ClineRulesToggles } from "@shared/cline-rules"
import { sendStateUpdate } from "./state/subscribeToState"
import { refreshClineRulesToggles } from "@core/context/instructions/user-instructions/cline-rules"
import { refreshExternalRulesToggles } from "@core/context/instructions/user-instructions/external-rules"
@ -377,9 +378,6 @@ export class Controller {
}
break
}
case "getLatestState":
await this.postStateToWebview()
break
case "accountLogoutClicked": {
await this.handleSignOut()
break
@ -653,6 +651,12 @@ export class Controller {
}
break
}
case "grpc_request_cancel": {
if (message.grpc_request_cancel) {
await handleGrpcRequestCancel(this, message.grpc_request_cancel)
}
break
}
case "copyToClipboard": {
try {
@ -1654,7 +1658,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
async postStateToWebview() {
const state = await this.getStateToPostToWebview()
this.postMessageToWebview({ type: "state", state })
await sendStateUpdate(state)
}
async getStateToPostToWebview(): Promise<ExtensionState> {

View File

@ -1,15 +1,22 @@
import { createServiceRegistry, ServiceMethodHandler } from "../grpc-service"
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../grpc-service"
import { StreamingResponseHandler } from "../grpc-handler"
import { registerAllMethods } from "./methods"
// Create MCP service registry
// Create mcp service registry
const mcpService = createServiceRegistry("mcp")
// Export the method handler type and registration function
// Export the method handler types and registration function
export type McpMethodHandler = ServiceMethodHandler
export type McpStreamingMethodHandler = StreamingMethodHandler
export const registerMethod = mcpService.registerMethod
// Export the request handler
// Export the request handlers
export const handleMcpServiceRequest = mcpService.handleRequest
export const handleMcpServiceStreamingRequest = mcpService.handleStreamingRequest
export const isStreamingMethod = mcpService.isStreamingMethod
// Register all mcp methods
registerAllMethods()

View File

@ -1,15 +1,22 @@
import { createServiceRegistry, ServiceMethodHandler } from "../grpc-service"
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../grpc-service"
import { StreamingResponseHandler } from "../grpc-handler"
import { registerAllMethods } from "./methods"
// Create models service registry
const modelsService = createServiceRegistry("models")
// Export the method handler type and registration function
// Export the method handler types and registration function
export type ModelsMethodHandler = ServiceMethodHandler
export type ModelsStreamingMethodHandler = StreamingMethodHandler
export const registerMethod = modelsService.registerMethod
// Export the request handler
// Export the request handlers
export const handleModelsServiceRequest = modelsService.handleRequest
export const handleModelsServiceStreamingRequest = modelsService.handleStreamingRequest
export const isStreamingMethod = modelsService.isStreamingMethod
// Register all models methods
registerAllMethods()

View File

@ -0,0 +1,24 @@
import * as vscode from "vscode"
import { Controller } from "../index"
import { EmptyRequest } from "../../../shared/proto/common"
import { State } from "../../../shared/proto/state"
import { ExtensionState } from "../../../shared/ExtensionMessage"
/**
* Get the latest extension state
* @param controller The controller instance
* @param request The empty request
* @returns The current extension state
*/
export async function getLatestState(controller: Controller, request: EmptyRequest): Promise<State> {
// Get the state using the existing method
const state = await controller.getStateToPostToWebview()
// Convert the state to a JSON string
const stateJson = JSON.stringify(state)
// Return the state as a JSON string
return {
stateJson,
}
}

View File

@ -0,0 +1,22 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../grpc-service"
import { StreamingResponseHandler } from "../grpc-handler"
import { registerAllMethods } from "./methods"
// Create state service registry
const stateService = createServiceRegistry("state")
// Export the method handler types and registration function
export type StateMethodHandler = ServiceMethodHandler
export type StateStreamingMethodHandler = StreamingMethodHandler
export const registerMethod = stateService.registerMethod
// Export the request handlers
export const handleStateServiceRequest = stateService.handleRequest
export const handleStateServiceStreamingRequest = stateService.handleStreamingRequest
export const isStreamingMethod = stateService.isStreamingMethod
// Register all state methods
registerAllMethods()

View File

@ -0,0 +1,17 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Import all method implementations
import { registerMethod } from "./index"
import { getLatestState } from "./getLatestState"
import { subscribeToState } from "./subscribeToState"
// Streaming methods for this service
export const streamingMethods = ["subscribeToState"]
// Register all state service methods
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("getLatestState", getLatestState)
registerMethod("subscribeToState", subscribeToState, { isStreaming: true })
}

View File

@ -0,0 +1,74 @@
import * as vscode from "vscode"
import { Controller } from "../index"
import { EmptyRequest } from "../../../shared/proto/common"
import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler"
// Keep track of active state subscriptions
const activeStateSubscriptions = new Set<StreamingResponseHandler>()
/**
* Subscribe to state 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 subscribeToState(
controller: Controller,
request: EmptyRequest,
responseStream: StreamingResponseHandler,
requestId?: string,
): Promise<void> {
// Send the initial state
const initialState = await controller.getStateToPostToWebview()
const initialStateJson = JSON.stringify(initialState)
console.log("[DEBUG] set up state subscription")
await responseStream({
stateJson: initialStateJson,
})
// Add this subscription to the active subscriptions
activeStateSubscriptions.add(responseStream)
// Register cleanup when the connection is closed
const cleanup = () => {
activeStateSubscriptions.delete(responseStream)
console.log("[DEBUG] Cleaned up state subscription")
}
// Register the cleanup function with the request registry if we have a requestId
if (requestId) {
getRequestRegistry().registerRequest(requestId, cleanup, { type: "state_subscription" }, responseStream)
}
}
/**
* Send a state update to all active subscribers
* @param state The state to send
*/
export async function sendStateUpdate(state: any): Promise<void> {
const stateJson = JSON.stringify(state)
// Send the update to all active subscribers
const promises = Array.from(activeStateSubscriptions).map(async (responseStream) => {
try {
// The issue might be that we're not properly formatting the response
// Let's ensure we're sending a properly formatted State message
await responseStream(
{
stateJson,
},
false, // Not the last message
)
console.log("[DEBUG] sending followup state", stateJson.length, "chars")
} catch (error) {
console.error("Error sending state update:", error)
// Remove the subscription if there was an error
activeStateSubscriptions.delete(responseStream)
}
})
await Promise.all(promises)
}

View File

@ -1,15 +1,22 @@
import { createServiceRegistry, ServiceMethodHandler } from "../grpc-service"
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../grpc-service"
import { StreamingResponseHandler } from "../grpc-handler"
import { registerAllMethods } from "./methods"
// Create task service registry
const taskService = createServiceRegistry("task")
// Export the method handler type and registration function
// Export the method handler types and registration function
export type TaskMethodHandler = ServiceMethodHandler
export type TaskStreamingMethodHandler = StreamingMethodHandler
export const registerMethod = taskService.registerMethod
// Export the request handler
// Export the request handlers
export const handleTaskServiceRequest = taskService.handleRequest
export const handleTaskServiceStreamingRequest = taskService.handleStreamingRequest
export const isStreamingMethod = taskService.isStreamingMethod
// Register all task methods
registerAllMethods()

View File

@ -1,15 +0,0 @@
import { createServiceRegistry, ServiceMethodHandler } from "../grpc-service"
import { registerAllMethods } from "./methods"
// Create web content service registry
const webContentService = createServiceRegistry("web-content")
// Export the method handler type and registration function
export type WebContentMethodHandler = ServiceMethodHandler
export const registerMethod = webContentService.registerMethod
// Export the request handler
export const handleWebContentServiceRequest = webContentService.handleRequest
// Register all web content methods
registerAllMethods()

View File

@ -1,6 +1,6 @@
import { Controller } from "../index"
import { StringRequest } from "../../../shared/proto/common"
import { IsImageUrl } from "../../../shared/proto/web_content"
import { IsImageUrl } from "../../../shared/proto/web"
import { detectImageUrl } from "@integrations/misc/link-preview"
/**

View File

@ -0,0 +1,22 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../grpc-service"
import { StreamingResponseHandler } from "../grpc-handler"
import { registerAllMethods } from "./methods"
// Create web service registry
const webService = createServiceRegistry("web")
// Export the method handler types and registration function
export type WebMethodHandler = ServiceMethodHandler
export type WebStreamingMethodHandler = StreamingMethodHandler
export const registerMethod = webService.registerMethod
// Export the request handlers
export const handleWebServiceRequest = webService.handleRequest
export const handleWebServiceStreamingRequest = webService.handleStreamingRequest
export const isStreamingMethod = webService.isStreamingMethod
// Register all web methods
registerAllMethods()

View File

@ -5,7 +5,7 @@
import { registerMethod } from "./index"
import { checkIsImageUrl } from "./checkIsImageUrl"
// Register all web-content service methods
// Register all web service methods
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("checkIsImageUrl", checkIsImageUrl)

View File

@ -105,6 +105,8 @@ export interface ExtensionMessage {
message?: any // JSON serialized protobuf message
request_id: string // Same ID as the request
error?: string // Optional error message
is_streaming?: boolean // Whether this is part of a streaming response
sequence_number?: number // For ordering chunks in streaming responses
}
}

View File

@ -36,7 +36,6 @@ export interface WebviewMessage {
| "openExtensionSettings"
| "requestVsCodeLmModels"
| "toggleToolAutoApprove"
| "getLatestState"
| "accountLogoutClicked"
| "showAccountViewClicked"
| "authStateChanged"
@ -61,6 +60,7 @@ export interface WebviewMessage {
| "searchFiles"
| "toggleFavoriteModel"
| "grpc_request"
| "grpc_request_cancel"
| "toggleClineRule"
| "toggleCursorRule"
| "toggleWindsurfRule"
@ -109,6 +109,10 @@ export interface WebviewMessage {
method: string
message: any // JSON serialized protobuf message
request_id: string // For correlating requests and responses
is_streaming?: boolean // Whether this is a streaming request
}
grpc_request_cancel?: {
request_id: string // ID of the request to cancel
}
// For cline rules
isGlobal?: boolean

127
src/shared/proto/state.ts Normal file
View File

@ -0,0 +1,127 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.7.0
// protoc v3.19.1
// source: state.proto
/* eslint-disable */
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"
import { EmptyRequest } from "./common"
export const protobufPackage = "cline"
export interface State {
stateJson: string
}
function createBaseState(): State {
return { stateJson: "" }
}
export const State: MessageFns<State> = {
encode(message: State, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.stateJson !== "") {
writer.uint32(10).string(message.stateJson)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): State {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseState()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.stateJson = reader.string()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): State {
return { stateJson: isSet(object.stateJson) ? globalThis.String(object.stateJson) : "" }
},
toJSON(message: State): unknown {
const obj: any = {}
if (message.stateJson !== "") {
obj.stateJson = message.stateJson
}
return obj
},
create<I extends Exact<DeepPartial<State>, I>>(base?: I): State {
return State.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<State>, I>>(object: I): State {
const message = createBaseState()
message.stateJson = object.stateJson ?? ""
return message
},
}
export type StateServiceDefinition = typeof StateServiceDefinition
export const StateServiceDefinition = {
name: "StateService",
fullName: "cline.StateService",
methods: {
getLatestState: {
name: "getLatestState",
requestType: EmptyRequest,
requestStream: false,
responseType: State,
responseStream: false,
options: {},
},
subscribeToState: {
name: "subscribeToState",
requestType: EmptyRequest,
requestStream: false,
responseType: State,
responseStream: true,
options: {},
},
},
} as const
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined
export type DeepPartial<T> = T extends Builtin
? T
: T extends globalThis.Array<infer U>
? globalThis.Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: T extends {}
? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>
type KeysOfUnion<T> = T extends T ? keyof T : never
export type Exact<P, I extends P> = P extends Builtin
? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never }
function isSet(value: any): boolean {
return value !== null && value !== undefined
}
export interface MessageFns<T> {
encode(message: T, writer?: BinaryWriter): BinaryWriter
decode(input: BinaryReader | Uint8Array, length?: number): T
fromJSON(object: any): T
toJSON(message: T): unknown
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T
}

View File

@ -2,7 +2,7 @@
// versions:
// protoc-gen-ts_proto v2.7.0
// protoc v3.19.1
// source: web_content.proto
// source: web.proto
/* eslint-disable */
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"
@ -91,10 +91,10 @@ export const IsImageUrl: MessageFns<IsImageUrl> = {
},
}
export type WebContentServiceDefinition = typeof WebContentServiceDefinition
export const WebContentServiceDefinition = {
name: "WebContentService",
fullName: "cline.WebContentService",
export type WebServiceDefinition = typeof WebServiceDefinition
export const WebServiceDefinition = {
name: "WebService",
fullName: "cline.WebService",
methods: {
checkIsImageUrl: {
name: "checkIsImageUrl",

View File

@ -27,7 +27,8 @@ import {
import { useMetaKeyDetection, useShortcut } from "@/utils/hooks"
import { validateApiConfiguration, validateModelId } from "@/utils/validate"
import { vscode } from "@/utils/vscode"
import { FileServiceClient } from "@/services/grpc-client"
import { EmptyRequest } from "@shared/proto/common"
import { FileServiceClient, StateServiceClient } from "@/services/grpc-client"
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
import Thumbnails from "@/components/common/Thumbnails"
import Tooltip from "@/components/common/Tooltip"
@ -875,7 +876,13 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
if (!apiValidationResult && !modelIdValidationResult) {
vscode.postMessage({ type: "apiConfiguration", apiConfiguration })
} else {
vscode.postMessage({ type: "getLatestState" })
StateServiceClient.getLatestState(EmptyRequest.create())
.then(() => {
console.log("State refreshed")
})
.catch((error) => {
console.error("Error refreshing state:", error)
})
}
}, [apiConfiguration, openRouterModels])

View File

@ -1,4 +1,4 @@
import { WebContentServiceClient } from "@/services/grpc-client"
import { WebServiceClient } from "@/services/grpc-client"
// Safely create a URL object with error handling and ensure HTTPS
export const safeCreateUrl = (url: string): URL | null => {
@ -145,7 +145,7 @@ export const checkIfImageUrl = async (url: string): Promise<boolean> => {
})
// Create the actual service call
const servicePromise = WebContentServiceClient.checkIsImageUrl({ value: url })
const servicePromise = WebServiceClient.checkIsImageUrl({ value: url })
.then((result) => result.isImage)
.catch((error) => {
console.error("Error checking if URL is an image via gRPC:", error)

View File

@ -1,5 +1,7 @@
import React, { createContext, useCallback, useContext, useEffect, useState } from "react"
import React, { createContext, useCallback, useContext, useEffect, useState, useRef } from "react"
import { useEvent } from "react-use"
import { StateServiceClient } from "../services/grpc-client"
import { EmptyRequest } from "@shared/proto/common"
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "@shared/AutoApprovalSettings"
import { ExtensionMessage, ExtensionState, DEFAULT_PLATFORM } from "@shared/ExtensionMessage"
import {
@ -92,50 +94,6 @@ export const ExtensionStateContextProvider: React.FC<{
const handleMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data
switch (message.type) {
case "state": {
setState((prevState) => {
const incoming = message.state!
// Versioning logic for autoApprovalSettings
const incomingVersion = incoming.autoApprovalSettings?.version ?? 1
const currentVersion = prevState.autoApprovalSettings?.version ?? 1
const shouldUpdateAutoApproval = incomingVersion > currentVersion
return {
...incoming,
autoApprovalSettings: shouldUpdateAutoApproval
? incoming.autoApprovalSettings
: prevState.autoApprovalSettings,
}
})
const config = message.state?.apiConfiguration
const hasKey = config
? [
config.apiKey,
config.openRouterApiKey,
config.awsRegion,
config.vertexProjectId,
config.openAiApiKey,
config.ollamaModelId,
config.lmStudioModelId,
config.liteLlmApiKey,
config.geminiApiKey,
config.openAiNativeApiKey,
config.deepSeekApiKey,
config.requestyApiKey,
config.togetherApiKey,
config.qwenApiKey,
config.doubaoApiKey,
config.mistralApiKey,
config.vsCodeLmModelSelector,
config.clineApiKey,
config.asksageApiKey,
config.xaiApiKey,
config.sambanovaApiKey,
].some((key) => key !== undefined)
: false
setShowWelcome(!hasKey)
setDidHydrateState(true)
break
}
case "theme": {
if (message.text) {
setTheme(convertTextMateToHljs(JSON.parse(message.text)))
@ -201,8 +159,95 @@ export const ExtensionStateContextProvider: React.FC<{
useEvent("message", handleMessage)
// Reference to store the state subscription cancellation function
const stateSubscriptionRef = useRef<(() => void) | null>(null)
// Subscribe to state updates using the new gRPC streaming API
useEffect(() => {
// Set up state subscription
stateSubscriptionRef.current = StateServiceClient.subscribeToState(
{},
{
onResponse: (response) => {
console.log("[DEBUG] got state update via subscription", response)
if (response.stateJson) {
try {
const stateData = JSON.parse(response.stateJson) as ExtensionState
console.log("[DEBUG] parsed state JSON, updating state")
setState((prevState) => {
// Versioning logic for autoApprovalSettings
const incomingVersion = stateData.autoApprovalSettings?.version ?? 1
const currentVersion = prevState.autoApprovalSettings?.version ?? 1
const shouldUpdateAutoApproval = incomingVersion > currentVersion
const newState = {
...stateData,
autoApprovalSettings: shouldUpdateAutoApproval
? stateData.autoApprovalSettings
: prevState.autoApprovalSettings,
}
// Update welcome screen state based on API configuration
const config = stateData.apiConfiguration
const hasKey = config
? [
config.apiKey,
config.openRouterApiKey,
config.awsRegion,
config.vertexProjectId,
config.openAiApiKey,
config.ollamaModelId,
config.lmStudioModelId,
config.liteLlmApiKey,
config.geminiApiKey,
config.openAiNativeApiKey,
config.deepSeekApiKey,
config.requestyApiKey,
config.togetherApiKey,
config.qwenApiKey,
config.doubaoApiKey,
config.mistralApiKey,
config.vsCodeLmModelSelector,
config.clineApiKey,
config.asksageApiKey,
config.xaiApiKey,
config.sambanovaApiKey,
].some((key) => key !== undefined)
: false
setShowWelcome(!hasKey)
setDidHydrateState(true)
console.log("[DEBUG] returning new state in ESC")
return newState
})
} catch (error) {
console.error("Error parsing state JSON:", error)
console.log("[DEBUG] ERR getting state", error)
}
}
console.log('[DEBUG] ended "got subscribed state"')
},
onError: (error) => {
console.error("Error in state subscription:", error)
},
onComplete: () => {
console.log("State subscription completed")
},
},
)
// Still send the webviewDidLaunch message for other initialization
vscode.postMessage({ type: "webviewDidLaunch" })
// Clean up subscription when component unmounts
return () => {
if (stateSubscriptionRef.current) {
stateSubscriptionRef.current()
stateSubscriptionRef.current = null
}
}
}, [])
const contextValue: ExtensionStateContextType = {

View File

@ -0,0 +1,179 @@
import { vscode } from "../utils/vscode"
import { v4 as uuidv4 } from "uuid"
// 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: {
onResponse: (response: InstanceType<T["methods"][K]["responseType"]>) => void
onError?: (error: Error) => void
onComplete?: () => void
},
) => () => void // Returns a cancel function
: (request: InstanceType<T["methods"][K]["requestType"]>) => Promise<InstanceType<T["methods"][K]["responseType"]>>
}
/**
* Helper function to encode request objects
*/
function encodeRequest(request: any): any {
if (request === null || request === undefined) {
return {}
} else if (typeof request.toJSON === "function") {
return request.toJSON()
} else if (typeof request === "object") {
return { ...request }
} else {
return { value: request }
}
}
// 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>
// For each method in the service
Object.values(service.methods).forEach((method) => {
if (method.responseStream) {
// Streaming method implementation
client[method.name as keyof GrpcClientType<T>] = ((
request: any,
options: {
onResponse: (response: any) => void
onError?: (error: Error) => void
onComplete?: () => void
},
) => {
const requestId = uuidv4()
// Set up listener for streaming responses
const handleResponse = (event: MessageEvent) => {
const message = event.data
if (message.type === "grpc_response" && message.grpc_response?.request_id === requestId) {
if (message.grpc_response.error) {
// Handle error
if (options.onError) {
options.onError(new Error(message.grpc_response.error))
}
// Only remove the event listener on error
window.removeEventListener("message", handleResponse)
} else if (message.grpc_response.is_streaming === false) {
// End of stream
if (message.grpc_response.message) {
// Process final message if present
const responseType = method.responseType
const response = responseType.fromJSON(message.grpc_response.message)
options.onResponse(response)
}
if (options.onComplete) {
options.onComplete()
}
// Only remove the event listener when the stream is explicitly ended
window.removeEventListener("message", handleResponse)
} else {
// Process streaming message
if (message.grpc_response.message) {
const responseType = method.responseType
const response = responseType.fromJSON(message.grpc_response.message)
console.log("[DEBUG] Received streaming response:", message.grpc_response.message)
options.onResponse(response)
}
}
}
}
window.addEventListener("message", handleResponse)
// Send the streaming request
const encodedRequest = encodeRequest(request)
vscode.postMessage({
type: "grpc_request",
grpc_request: {
service: service.fullName,
method: method.name,
message: encodedRequest,
request_id: requestId,
is_streaming: true,
},
})
// Return a function to cancel the stream
return () => {
window.removeEventListener("message", handleResponse)
// Send cancellation message
vscode.postMessage({
type: "grpc_request_cancel",
grpc_request_cancel: {
request_id: requestId,
},
})
console.log(`[DEBUG] Sent cancellation for request: ${requestId}`)
}
}) as any
} else {
// Unary method implementation
client[method.name as keyof GrpcClientType<T>] = ((request: any) => {
return new Promise((resolve, reject) => {
const requestId = uuidv4()
// Set up one-time listener for this specific request
const handleResponse = (event: MessageEvent) => {
const message = event.data
if (message.type === "grpc_response" && message.grpc_response?.request_id === requestId) {
// Remove listener once we get our response
window.removeEventListener("message", handleResponse)
if (message.grpc_response.error) {
reject(new Error(message.grpc_response.error))
} else {
// Convert JSON back to protobuf message
const responseType = method.responseType
const response = responseType.fromJSON(message.grpc_response.message)
console.log("[DEBUG] grpc-client sending response:", response)
resolve(response)
}
}
}
window.addEventListener("message", handleResponse)
// Send the request
const encodedRequest = encodeRequest(request)
vscode.postMessage({
type: "grpc_request",
grpc_request: {
service: service.fullName,
method: method.name,
message: encodedRequest,
request_id: requestId,
is_streaming: false,
},
})
})
}) as any
}
})
return client
}

View File

@ -1,119 +1,35 @@
import { vscode } from "../utils/vscode"
import { v4 as uuidv4 } from "uuid"
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { createGrpcClient } from "./grpc-client-base"
import { AccountServiceDefinition } from "@shared/proto/account"
import { BrowserServiceDefinition } from "@shared/proto/browser"
import { CheckpointsServiceDefinition } from "@shared/proto/checkpoints"
import { EmptyRequest } from "@shared/proto/common"
import { FileServiceDefinition } from "@shared/proto/file"
import { McpServiceDefinition } from "@shared/proto/mcp"
import { ModelsServiceDefinition } from "@shared/proto/models"
import { StateServiceDefinition } from "@shared/proto/state"
import { TaskServiceDefinition } from "@shared/proto/task"
import { WebContentServiceDefinition } from "@shared/proto/web_content"
// Generic type for any protobuf service definition
type ProtoService = {
name: string
fullName: string
methods: {
[key: string]: {
name: string
requestType: any
responseType: any
requestStream: boolean
responseStream: boolean
options: any
}
}
}
// Define a generic type that extracts method signatures from a service definition
type GrpcClientType<T extends ProtoService> = {
[K in keyof T["methods"]]: (
request: InstanceType<T["methods"][K]["requestType"]>,
) => Promise<InstanceType<T["methods"][K]["responseType"]>>
}
// Create a client for any protobuf service with inferred types
function createGrpcClient<T extends ProtoService>(service: T): GrpcClientType<T> {
const client = {} as GrpcClientType<T>
// For each method in the service
Object.values(service.methods).forEach((method) => {
// Create a function that matches the method signature
client[method.name as keyof GrpcClientType<T>] = ((request: any) => {
return new Promise((resolve, reject) => {
const requestId = uuidv4()
// Set up one-time listener for this specific request
const handleResponse = (event: MessageEvent) => {
const message = event.data
if (message.type === "grpc_response" && message.grpc_response?.request_id === requestId) {
// Remove listener once we get our response
window.removeEventListener("message", handleResponse)
if (message.grpc_response.error) {
reject(new Error(message.grpc_response.error))
} else {
// Convert JSON back to protobuf message
const responseType = method.responseType
const response = responseType.fromJSON(message.grpc_response.message)
console.log("[DEBUG] grpc-client sending response:", response)
resolve(response)
}
}
}
window.addEventListener("message", handleResponse)
let encodedRequest = {}
// Handle different types of requests
if (request === null || request === undefined) {
// Empty request
encodedRequest = {}
} else if (typeof request.toJSON === "function") {
// Proper protobuf object
encodedRequest = request.toJSON()
} else if (typeof request === "object") {
// Plain JavaScript object
encodedRequest = { ...request }
} else {
// Fallback
encodedRequest = { value: request }
}
// Send the request
vscode.postMessage({
type: "grpc_request",
grpc_request: {
service: service.fullName,
method: method.name,
message: encodedRequest, // Convert protobuf to JSON
request_id: requestId,
},
})
})
}) as any
})
return client
}
import { WebServiceDefinition } from "@shared/proto/web"
import { ModelsServiceDefinition } from "@shared/proto/models"
const AccountServiceClient = createGrpcClient(AccountServiceDefinition)
const BrowserServiceClient = createGrpcClient(BrowserServiceDefinition)
const CheckpointsServiceClient = createGrpcClient(CheckpointsServiceDefinition)
const FileServiceClient = createGrpcClient(FileServiceDefinition)
const McpServiceClient = createGrpcClient(McpServiceDefinition)
const ModelsServiceClient = createGrpcClient(ModelsServiceDefinition)
const StateServiceClient = createGrpcClient(StateServiceDefinition)
const TaskServiceClient = createGrpcClient(TaskServiceDefinition)
const WebContentServiceClient = createGrpcClient(WebContentServiceDefinition)
const WebServiceClient = createGrpcClient(WebServiceDefinition)
const ModelsServiceClient = createGrpcClient(ModelsServiceDefinition)
export {
AccountServiceClient,
BrowserServiceClient,
CheckpointsServiceClient,
FileServiceClient,
TaskServiceClient,
McpServiceClient,
StateServiceClient,
TaskServiceClient,
WebServiceClient,
ModelsServiceClient,
WebContentServiceClient,
}