mirror of
https://github.com/cline/cline.git
synced 2025-06-03 03:59:07 +00:00
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:
parent
311cb3ac0a
commit
cfc133acd3
@ -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
13
proto/state.proto
Normal 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;
|
||||
}
|
@ -6,7 +6,7 @@ option java_multiple_files = true;
|
||||
|
||||
import "common.proto";
|
||||
|
||||
service WebContentService {
|
||||
service WebService {
|
||||
rpc checkIsImageUrl(StringRequest) returns (IsImageUrl);
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
}
|
||||
|
124
src/core/controller/grpc-request-registry.ts
Normal file
124
src/core/controller/grpc-request-registry.ts
Normal 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
|
||||
}
|
||||
}
|
70
src/core/controller/grpc-service-config.ts
Normal file
70
src/core/controller/grpc-service-config.ts
Normal 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,
|
||||
},
|
||||
}
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
@ -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> {
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
24
src/core/controller/state/getLatestState.ts
Normal file
24
src/core/controller/state/getLatestState.ts
Normal 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,
|
||||
}
|
||||
}
|
22
src/core/controller/state/index.ts
Normal file
22
src/core/controller/state/index.ts
Normal 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()
|
17
src/core/controller/state/methods.ts
Normal file
17
src/core/controller/state/methods.ts
Normal 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 })
|
||||
}
|
74
src/core/controller/state/subscribeToState.ts
Normal file
74
src/core/controller/state/subscribeToState.ts
Normal 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)
|
||||
}
|
@ -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()
|
||||
|
@ -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()
|
@ -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"
|
||||
|
||||
/**
|
22
src/core/controller/web/index.ts
Normal file
22
src/core/controller/web/index.ts
Normal 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()
|
@ -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)
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
127
src/shared/proto/state.ts
Normal 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
|
||||
}
|
@ -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",
|
@ -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])
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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 = {
|
||||
|
179
webview-ui/src/services/grpc-client-base.ts
Normal file
179
webview-ui/src/services/grpc-client-base.ts
Normal 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
|
||||
}
|
@ -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,
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user