Skip to content

Commit cfc133a

Browse files
Garothhugelunggithub-actions[bot]
authored
PROTOBUS: Streaming, State, Service Auto-Config (RooCodeInc#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 <[email protected]> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 311cb3a commit cfc133a

File tree

32 files changed

+1406
-283
lines changed

32 files changed

+1406
-283
lines changed

proto/build-proto.js

Lines changed: 286 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,27 @@ const require = createRequire(import.meta.url)
1212
const protoc = path.join(require.resolve("grpc-tools"), "../bin/protoc")
1313
const tsProtoPlugin = require.resolve("ts-proto/protoc-gen-ts_proto")
1414

15-
// Get script directory and root directory
1615
const __filename = fileURLToPath(import.meta.url)
1716
const SCRIPT_DIR = path.dirname(__filename)
1817
const ROOT_DIR = path.resolve(SCRIPT_DIR, "..")
1918

19+
// List of gRPC services
20+
// To add a new service, simply add it to this map and run this script
21+
// The service handler will be automatically discovered and used by grpc-handler.ts
22+
const serviceNameMap = {
23+
account: "cline.AccountService",
24+
browser: "cline.BrowserService",
25+
checkpoints: "cline.CheckpointsService",
26+
file: "cline.FileService",
27+
mcp: "cline.McpService",
28+
state: "cline.StateService",
29+
task: "cline.TaskService",
30+
web: "cline.WebService",
31+
models: "cline.ModelsService",
32+
// Add new services here - no other code changes needed!
33+
}
34+
const serviceDirs = Object.keys(serviceNameMap).map((serviceKey) => path.join(ROOT_DIR, "src", "core", "controller", serviceKey))
35+
2036
async function main() {
2137
console.log(chalk.bold.blue("Starting Protocol Buffer code generation..."))
2238

@@ -33,6 +49,9 @@ async function main() {
3349
await fs.unlink(path.join(TS_OUT_DIR, file))
3450
}
3551

52+
// Check for missing proto files for services in serviceNameMap
53+
await ensureProtoFilesExist()
54+
3655
// Process all proto files
3756
console.log(chalk.cyan("Processing proto files from"), SCRIPT_DIR)
3857
const protoFiles = await globby("*.proto", { cwd: SCRIPT_DIR })
@@ -64,42 +83,135 @@ async function main() {
6483
console.log(chalk.green("Protocol Buffer code generation completed successfully."))
6584
console.log(chalk.green(`TypeScript files generated in: ${TS_OUT_DIR}`))
6685

67-
// Generate method registration files
6886
await generateMethodRegistrations()
87+
await generateServiceConfig()
88+
await generateGrpcClientConfig()
89+
}
90+
91+
/**
92+
* Generate a gRPC client configuration file for the webview
93+
* This eliminates the need for manual imports and client creation in grpc-client.ts
94+
*/
95+
async function generateGrpcClientConfig() {
96+
console.log(chalk.cyan("Generating gRPC client configuration..."))
97+
98+
const serviceImports = []
99+
const serviceClientCreations = []
100+
const serviceExports = []
101+
102+
// Process each service in the serviceNameMap
103+
for (const [dirName, fullServiceName] of Object.entries(serviceNameMap)) {
104+
const capitalizedName = dirName.charAt(0).toUpperCase() + dirName.slice(1)
105+
106+
// Add import statement
107+
serviceImports.push(`import { ${capitalizedName}ServiceDefinition } from "@shared/proto/${dirName}"`)
108+
109+
// Add client creation
110+
serviceClientCreations.push(
111+
`const ${capitalizedName}ServiceClient = createGrpcClient(${capitalizedName}ServiceDefinition)`,
112+
)
113+
114+
// Add to exports
115+
serviceExports.push(`${capitalizedName}ServiceClient`)
116+
}
117+
118+
// Generate the file content
119+
const content = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
120+
// Generated by proto/build-proto.js
121+
122+
import { createGrpcClient } from "./grpc-client-base"
123+
${serviceImports.join("\n")}
124+
125+
${serviceClientCreations.join("\n")}
126+
127+
export {
128+
${serviceExports.join(",\n\t")}
129+
}`
130+
131+
const configPath = path.join(ROOT_DIR, "webview-ui", "src", "services", "grpc-client.ts")
132+
await fs.writeFile(configPath, content)
133+
console.log(chalk.green(`Generated gRPC client at ${configPath}`))
134+
}
69135

70-
// Make the script executable
71-
try {
72-
await fs.chmod(path.join(SCRIPT_DIR, "build-proto.js"), 0o755)
73-
} catch (error) {
74-
console.warn(chalk.yellow("Warning: Could not make script executable:"), error)
136+
/**
137+
* Parse proto files to extract streaming method information
138+
* @param protoFiles Array of proto file names
139+
* @param scriptDir Directory containing proto files
140+
* @returns Map of service names to their streaming methods
141+
*/
142+
async function parseProtoForStreamingMethods(protoFiles, scriptDir) {
143+
console.log(chalk.cyan("Parsing proto files for streaming methods..."))
144+
145+
// Map of service name to array of streaming method names
146+
const streamingMethodsMap = new Map()
147+
148+
for (const protoFile of protoFiles) {
149+
const content = await fs.readFile(path.join(scriptDir, protoFile), "utf8")
150+
151+
// Extract package name
152+
const packageMatch = content.match(/package\s+([^;]+);/)
153+
const packageName = packageMatch ? packageMatch[1].trim() : "unknown"
154+
155+
// Extract service definitions
156+
const serviceMatches = Array.from(content.matchAll(/service\s+(\w+)\s*\{([^}]+)\}/g))
157+
for (const serviceMatch of serviceMatches) {
158+
const serviceName = serviceMatch[1]
159+
const serviceBody = serviceMatch[2]
160+
const fullServiceName = `${packageName}.${serviceName}`
161+
162+
// Extract method definitions with streaming
163+
const methodMatches = Array.from(
164+
serviceBody.matchAll(/rpc\s+(\w+)\s*\(\s*(stream\s+)?(\w+)\s*\)\s*returns\s*\(\s*(stream\s+)?(\w+)\s*\)/g),
165+
)
166+
167+
const streamingMethods = []
168+
for (const methodMatch of methodMatches) {
169+
const methodName = methodMatch[1]
170+
const isRequestStreaming = !!methodMatch[2]
171+
const requestType = methodMatch[3]
172+
const isResponseStreaming = !!methodMatch[4]
173+
const responseType = methodMatch[5]
174+
175+
if (isResponseStreaming) {
176+
streamingMethods.push({
177+
name: methodName,
178+
requestType,
179+
responseType,
180+
isRequestStreaming,
181+
})
182+
}
183+
}
184+
185+
if (streamingMethods.length > 0) {
186+
streamingMethodsMap.set(fullServiceName, streamingMethods)
187+
}
188+
}
75189
}
190+
191+
return streamingMethodsMap
76192
}
77193

78194
async function generateMethodRegistrations() {
79195
console.log(chalk.cyan("Generating method registration files..."))
80196

81-
const serviceDirs = [
82-
path.join(ROOT_DIR, "src", "core", "controller", "account"),
83-
path.join(ROOT_DIR, "src", "core", "controller", "browser"),
84-
path.join(ROOT_DIR, "src", "core", "controller", "checkpoints"),
85-
path.join(ROOT_DIR, "src", "core", "controller", "file"),
86-
path.join(ROOT_DIR, "src", "core", "controller", "mcp"),
87-
path.join(ROOT_DIR, "src", "core", "controller", "models"),
88-
path.join(ROOT_DIR, "src", "core", "controller", "task"),
89-
path.join(ROOT_DIR, "src", "core", "controller", "web-content"),
90-
// Add more service directories here as needed
91-
]
197+
// Parse proto files for streaming methods
198+
const protoFiles = await globby("*.proto", { cwd: SCRIPT_DIR })
199+
const streamingMethodsMap = await parseProtoForStreamingMethods(protoFiles, SCRIPT_DIR)
92200

93201
for (const serviceDir of serviceDirs) {
94202
try {
95203
await fs.access(serviceDir)
96204
} catch (error) {
97-
console.log(chalk.gray(`Skipping ${serviceDir} - directory does not exist`))
98-
continue
205+
console.log(chalk.cyan(`Creating directory ${serviceDir} for new service`))
206+
await fs.mkdir(serviceDir, { recursive: true })
99207
}
100208

101209
const serviceName = path.basename(serviceDir)
102210
const registryFile = path.join(serviceDir, "methods.ts")
211+
const indexFile = path.join(serviceDir, "index.ts")
212+
213+
const fullServiceName = serviceNameMap[serviceName]
214+
const streamingMethods = streamingMethodsMap.get(fullServiceName) || []
103215

104216
console.log(chalk.cyan(`Generating method registrations for ${serviceName}...`))
105217

@@ -109,8 +221,8 @@ async function generateMethodRegistrations() {
109221
// Filter out index.ts and methods.ts
110222
const implementationFiles = files.filter((file) => file !== "index.ts" && file !== "methods.ts")
111223

112-
// Create the output file with header
113-
let content = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
224+
// Create the methods.ts file with header
225+
let methodsContent = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
114226
// Generated by proto/build-proto.js
115227
116228
// Import all method implementations
@@ -119,31 +231,177 @@ import { registerMethod } from "./index"\n`
119231
// Add imports for all implementation files
120232
for (const file of implementationFiles) {
121233
const baseName = path.basename(file, ".ts")
122-
content += `import { ${baseName} } from "./${baseName}"\n`
234+
methodsContent += `import { ${baseName} } from "./${baseName}"\n`
235+
}
236+
237+
// Add streaming methods information
238+
if (streamingMethods.length > 0) {
239+
methodsContent += `\n// Streaming methods for this service
240+
export const streamingMethods = ${JSON.stringify(
241+
streamingMethods.map((m) => m.name),
242+
null,
243+
2,
244+
)}\n`
123245
}
124246

125247
// Add registration function
126-
content += `\n// Register all ${serviceName} service methods
248+
methodsContent += `\n// Register all ${serviceName} service methods
127249
export function registerAllMethods(): void {
128250
\t// Register each method with the registry\n`
129251

130252
// Add registration statements
131253
for (const file of implementationFiles) {
132254
const baseName = path.basename(file, ".ts")
133-
content += `\tregisterMethod("${baseName}", ${baseName})\n`
255+
const isStreaming = streamingMethods.some((m) => m.name === baseName)
256+
257+
if (isStreaming) {
258+
methodsContent += `\tregisterMethod("${baseName}", ${baseName}, { isStreaming: true })\n`
259+
} else {
260+
methodsContent += `\tregisterMethod("${baseName}", ${baseName})\n`
261+
}
134262
}
135263

136264
// Close the function
137-
content += `}`
265+
methodsContent += `}`
138266

139-
// Write the file
140-
await fs.writeFile(registryFile, content)
267+
// Write the methods.ts file
268+
await fs.writeFile(registryFile, methodsContent)
141269
console.log(chalk.green(`Generated ${registryFile}`))
270+
271+
// Generate index.ts file
272+
const capitalizedServiceName = serviceName.charAt(0).toUpperCase() + serviceName.slice(1)
273+
const indexContent = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
274+
// Generated by proto/build-proto.js
275+
276+
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../grpc-service"
277+
import { StreamingResponseHandler } from "../grpc-handler"
278+
import { registerAllMethods } from "./methods"
279+
280+
// Create ${serviceName} service registry
281+
const ${serviceName}Service = createServiceRegistry("${serviceName}")
282+
283+
// Export the method handler types and registration function
284+
export type ${capitalizedServiceName}MethodHandler = ServiceMethodHandler
285+
export type ${capitalizedServiceName}StreamingMethodHandler = StreamingMethodHandler
286+
export const registerMethod = ${serviceName}Service.registerMethod
287+
288+
// Export the request handlers
289+
export const handle${capitalizedServiceName}ServiceRequest = ${serviceName}Service.handleRequest
290+
export const handle${capitalizedServiceName}ServiceStreamingRequest = ${serviceName}Service.handleStreamingRequest
291+
export const isStreamingMethod = ${serviceName}Service.isStreamingMethod
292+
293+
// Register all ${serviceName} methods
294+
registerAllMethods()`
295+
296+
// Write the index.ts file
297+
await fs.writeFile(indexFile, indexContent)
298+
console.log(chalk.green(`Generated ${indexFile}`))
142299
}
143300

144301
console.log(chalk.green("Method registration files generated successfully."))
145302
}
146303

304+
/**
305+
* Generate a service configuration file that maps service names to their handlers
306+
* This eliminates the need for manual switch/case statements in grpc-handler.ts
307+
*/
308+
async function generateServiceConfig() {
309+
console.log(chalk.cyan("Generating service configuration file..."))
310+
311+
const serviceImports = []
312+
const serviceConfigs = []
313+
314+
// Add all services from the serviceNameMap
315+
for (const [dirName, fullServiceName] of Object.entries(serviceNameMap)) {
316+
const capitalizedName = dirName.charAt(0).toUpperCase() + dirName.slice(1)
317+
serviceImports.push(
318+
`import { handle${capitalizedName}ServiceRequest, handle${capitalizedName}ServiceStreamingRequest } from "./${dirName}/index"`,
319+
)
320+
serviceConfigs.push(`
321+
"${fullServiceName}": {
322+
requestHandler: handle${capitalizedName}ServiceRequest,
323+
streamingHandler: handle${capitalizedName}ServiceStreamingRequest
324+
}`)
325+
}
326+
327+
const content = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
328+
// Generated by proto/build-proto.js
329+
330+
import { Controller } from "./index"
331+
import { StreamingResponseHandler } from "./grpc-handler"
332+
${serviceImports.join("\n")}
333+
334+
/**
335+
* Configuration for a service handler
336+
*/
337+
export interface ServiceHandlerConfig {
338+
requestHandler: (controller: Controller, method: string, message: any) => Promise<any>;
339+
streamingHandler: (controller: Controller, method: string, message: any, responseStream: StreamingResponseHandler, requestId?: string) => Promise<void>;
340+
}
341+
342+
/**
343+
* Map of service names to their handler configurations
344+
*/
345+
export const serviceHandlers: Record<string, ServiceHandlerConfig> = {${serviceConfigs.join(",")}
346+
};`
347+
348+
const configPath = path.join(ROOT_DIR, "src", "core", "controller", "grpc-service-config.ts")
349+
await fs.writeFile(configPath, content)
350+
console.log(chalk.green(`Generated service configuration at ${configPath}`))
351+
}
352+
353+
/**
354+
* Ensure that a .proto file exists for each service in the serviceNameMap
355+
* If a .proto file doesn't exist, create a template file
356+
*/
357+
async function ensureProtoFilesExist() {
358+
console.log(chalk.cyan("Checking for missing proto files..."))
359+
360+
// Get existing proto files
361+
const existingProtoFiles = await globby("*.proto", { cwd: SCRIPT_DIR })
362+
const existingProtoServices = existingProtoFiles.map((file) => path.basename(file, ".proto"))
363+
364+
// Check each service in serviceNameMap
365+
for (const [serviceName, fullServiceName] of Object.entries(serviceNameMap)) {
366+
if (!existingProtoServices.includes(serviceName)) {
367+
console.log(chalk.yellow(`Creating template proto file for ${serviceName}...`))
368+
369+
// Extract service class name from full name (e.g., "cline.ModelsService" -> "ModelsService")
370+
const serviceClassName = fullServiceName.split(".").pop()
371+
372+
// Create template proto file
373+
const protoContent = `syntax = "proto3";
374+
375+
package cline;
376+
option java_package = "bot.cline.proto";
377+
option java_multiple_files = true;
378+
379+
import "common.proto";
380+
381+
// ${serviceClassName} provides methods for managing ${serviceName}
382+
service ${serviceClassName} {
383+
// Add your RPC methods here
384+
// Example (String is from common.proto, responses should be generic types):
385+
// rpc YourMethod(YourRequest) returns (String);
386+
}
387+
388+
// Add your message definitions here
389+
// Example (Requests must always start with Metadata):
390+
// message YourRequest {
391+
// Metadata metadata = 1;
392+
// string stringField = 2;
393+
// int32 int32Field = 3;
394+
// }
395+
`
396+
397+
// Write the template proto file
398+
const protoFilePath = path.join(SCRIPT_DIR, `${serviceName}.proto`)
399+
await fs.writeFile(protoFilePath, protoContent)
400+
console.log(chalk.green(`Created template proto file at ${protoFilePath}`))
401+
}
402+
}
403+
}
404+
147405
// Run the main function
148406
main().catch((error) => {
149407
console.error(chalk.red("Error:"), error)

0 commit comments

Comments
 (0)