Compare commits

...
Author SHA1 Message Date
Andrei Edell fe2a27fe25 package lock seems necessary now 2025-04-14 18:50:29 -10:00
Andrei Edell 480bc37c6b Merge branch 'main' into protobus 2025-04-14 18:46:57 -10:00
Andrei Edell 9e0d6661a5 change how imports work based on feedback 2025-04-14 18:25:14 -10:00
Andrei Edell 4badb2eb5a don't protoc during package 2025-04-14 18:09:57 -10:00
Andrei Edell 9ec4e1ffde rewrite build-protos in node & update package.json 2025-04-13 15:05:16 -10:00
Andrei Edell 195fdea95c format fix 2025-04-11 10:32:52 -10:00
Andrei Edell 5d48e59276 format fix 2025-04-11 10:28:17 -10:00
Andrei Edell 2fdc285cb5 delete old message passing for getBrowserConnectionInfo 2025-04-11 10:28:17 -10:00
Andrei Edell 52c7b8f94e warn user that they must install protoc deps 2025-04-11 10:28:17 -10:00
Andrei Edell b21717b009 make service implementations more generic 2025-04-11 10:28:17 -10:00
Andrei Edell db70cd5a70 hook protobufs into package.json scripts 2025-04-11 10:28:17 -10:00
Andrei Edell 267f6cd4ca switch to auto-generating the method registration via bash 2025-04-11 10:28:17 -10:00
Andrei Edell a3b9bffc05 better code layout for grpc implementations 2025-04-11 10:28:17 -10:00
Andrei Edell ec291b6ee2 a reflection-based way to create grpc services automatically 2025-04-11 10:28:17 -10:00
Andrei Edell 5c1b15dd8c better client layout & easier service imports 2025-04-11 10:28:17 -10:00
Andrei Edell e38ee596bf delete mcp proto for now 2025-04-11 10:28:17 -10:00
Andrei Edell 7b92356ae1 remove redundant browser-service layer, clean up naming 2025-04-11 10:28:17 -10:00
Andrei Edell af1936ee2b use common.proto 2025-04-11 10:28:17 -10:00
Andrei Edell baa83821c4 remomve some comments 2025-04-11 10:28:17 -10:00
Andrei Edell e0880de85e simplify call a bit more 2025-04-11 10:28:17 -10:00
Andrei Edell 4cd7d90b9e initial working demo 2025-04-11 10:28:15 -10:00
Andrei Edell 97a8b3c55b phase 1 2025-04-11 10:27:00 -10:00
Andrei Edell eeda58c8ac delete old protos for now 2025-04-11 10:24:37 -10:00
Andrei Edell 4dabfee1a6 initial protobuf setup & rough domains 2025-04-11 10:24:37 -10:00
21 changed files with 1116 additions and 120 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
gRPC over vscode message bus to make messaging better
+164 -36
View File
@@ -1,20 +1,22 @@
{
"name": "claude-dev",
"version": "3.12.0",
"version": "3.12.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.12.0",
"version": "3.12.3",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.12.4",
"@anthropic-ai/sdk": "^0.37.0",
"@anthropic-ai/vertex-sdk": "^0.6.4",
"@aws-sdk/client-bedrock-runtime": "^3.758.0",
"@bufbuild/protobuf": "^2.2.5",
"@google-cloud/vertexai": "^1.9.3",
"@google/generative-ai": "^0.18.0",
"@grpc/grpc-js": "^1.9.15",
"@mistralai/mistralai": "^1.5.0",
"@modelcontextprotocol/sdk": "^1.7.0",
"@opentelemetry/api": "^1.4.1",
@@ -79,15 +81,18 @@
"@vscode/test-cli": "^0.0.9",
"@vscode/test-electron": "^2.4.0",
"chai": "^4.3.10",
"chalk": "^5.3.0",
"esbuild": "^0.25.0",
"eslint": "^8.57.0",
"husky": "^9.1.7",
"npm-run-all": "^4.1.5",
"prettier": "^3.3.3",
"protoc-gen-ts": "^0.8.7",
"proxyquire": "^2.1.3",
"should": "^13.2.3",
"sinon": "^19.0.2",
"ts-node": "^10.9.2",
"ts-proto": "^2.6.1",
"typescript": "^5.4.5"
},
"engines": {
@@ -3957,6 +3962,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@bufbuild/protobuf": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.2.5.tgz",
"integrity": "sha512-/g5EzJifw5GF8aren8wZ/G5oMuPoGeS6MQD3ca8ddcvdXR5UELUfdTZITCGNhNXynY/AYl3Z4plmxdj/tRl/hQ==",
"license": "(Apache-2.0 AND BSD-3-Clause)"
},
"node_modules/@changesets/apply-release-plan": {
"version": "7.0.8",
"resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-7.0.8.tgz",
@@ -5566,6 +5577,7 @@
"version": "1.9.15",
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz",
"integrity": "sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==",
"license": "Apache-2.0",
"dependencies": {
"@grpc/proto-loader": "^0.7.8",
"@types/node": ">=12.12.47"
@@ -9919,6 +9931,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/case-anything": {
"version": "2.1.13",
"resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.13.tgz",
"integrity": "sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.13"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/chai": {
"version": "4.3.10",
"resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz",
@@ -9938,35 +9963,18 @@
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
"integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/chalk/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/chardet": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
@@ -10612,6 +10620,19 @@
"node": ">=8"
}
},
"node_modules/detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"detect-libc": "bin/detect-libc.js"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/devtools-protocol": {
"version": "0.0.1342118",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1342118.tgz",
@@ -10722,6 +10743,16 @@
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/dprint-node": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/dprint-node/-/dprint-node-1.0.8.tgz",
"integrity": "sha512-iVKnUtYfGrYcW1ZAlfR/F59cUVL8QIhWoBJoSjkkdua/dkWIgjZfiLMeTjiB06X0ZLkQ0M2C1VbUj/CxkIf1zg==",
"dev": true,
"license": "MIT",
"dependencies": {
"detect-libc": "^1.0.3"
}
},
"node_modules/duck": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz",
@@ -11192,6 +11223,23 @@
"concat-map": "0.0.1"
}
},
"node_modules/eslint/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/eslint/node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -11240,6 +11288,19 @@
"node": ">=8"
}
},
"node_modules/eslint/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/espree": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
@@ -13659,6 +13720,36 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/log-symbols/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/log-symbols/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/long": {
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/long/-/long-5.2.4.tgz",
@@ -14768,19 +14859,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ora/node_modules/chalk": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
"integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/ora/node_modules/emoji-regex": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz",
@@ -15366,6 +15444,20 @@
"node": ">=12.0.0"
}
},
"node_modules/protoc-gen-ts": {
"version": "0.8.7",
"resolved": "https://registry.npmjs.org/protoc-gen-ts/-/protoc-gen-ts-0.8.7.tgz",
"integrity": "sha512-jr4VJey2J9LVYCV7EVyVe53g1VMw28cCmYJhBe5e3YX5wiyiDwgxWxeDf9oTqAe4P1bN/YGAkW2jhlH8LohwiQ==",
"dev": true,
"license": "MIT",
"bin": {
"protoc-gen-ts": "protoc-gen-ts.js"
},
"funding": {
"type": "individual",
"url": "https://www.buymeacoffee.com/thesayyn"
}
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -16970,6 +17062,42 @@
"node": ">=0.3.1"
}
},
"node_modules/ts-poet": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/ts-poet/-/ts-poet-6.11.0.tgz",
"integrity": "sha512-r5AGF8vvb+GjBsnqiTqbLhN1/U2FJt6BI+k0dfCrkKzWvUhNlwMmq9nDHuucHs45LomgHjZPvYj96dD3JawjJA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"dprint-node": "^1.0.8"
}
},
"node_modules/ts-proto": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/ts-proto/-/ts-proto-2.7.0.tgz",
"integrity": "sha512-BGHjse2wTOeswOqnnPKinpxmbaRd882so/e1En6ww59YMG7AO9Kg4vPpJcbVfrpBixPRDqHafXD/RDyd2T99GA==",
"dev": true,
"license": "ISC",
"dependencies": {
"@bufbuild/protobuf": "^2.0.0",
"case-anything": "^2.1.13",
"ts-poet": "^6.7.0",
"ts-proto-descriptors": "2.0.0"
},
"bin": {
"protoc-gen-ts_proto": "protoc-gen-ts_proto"
}
},
"node_modules/ts-proto-descriptors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ts-proto-descriptors/-/ts-proto-descriptors-2.0.0.tgz",
"integrity": "sha512-wHcTH3xIv11jxgkX5OyCSFfw27agpInAd6yh89hKG6zqIXnjW9SYqSER2CVQxdPj4czeOhGagNvZBEbJPy7qkw==",
"dev": true,
"license": "ISC",
"dependencies": {
"@bufbuild/protobuf": "^2.0.0"
}
},
"node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+6
View File
@@ -290,6 +290,7 @@
"watch:esbuild:test": "IS_TEST=true node esbuild.js --watch",
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
"package": "npm run build:webview && npm run check-types && npm run lint && node esbuild.js --production",
"protos": "node proto/build-proto.js && prettier src/shared/proto --write && prettier src/core/controller --write",
"package:test": "IS_TEST=true npm run build:webview:test && npm run check-types && npm run lint && IS_TEST=true node esbuild.js --production",
"build:webview:test": "cd webview-ui && IS_TEST=true npm run build",
"watch:test": "IS_TEST=true npm-run-all -p watch:tsc watch:esbuild:test",
@@ -334,15 +335,18 @@
"@vscode/test-cli": "^0.0.9",
"@vscode/test-electron": "^2.4.0",
"chai": "^4.3.10",
"chalk": "^5.3.0",
"esbuild": "^0.25.0",
"eslint": "^8.57.0",
"husky": "^9.1.7",
"npm-run-all": "^4.1.5",
"prettier": "^3.3.3",
"protoc-gen-ts": "^0.8.7",
"proxyquire": "^2.1.3",
"should": "^13.2.3",
"sinon": "^19.0.2",
"ts-node": "^10.9.2",
"ts-proto": "^2.6.1",
"typescript": "^5.4.5"
},
"dependencies": {
@@ -350,8 +354,10 @@
"@anthropic-ai/sdk": "^0.37.0",
"@anthropic-ai/vertex-sdk": "^0.6.4",
"@aws-sdk/client-bedrock-runtime": "^3.758.0",
"@bufbuild/protobuf": "^2.2.5",
"@google-cloud/vertexai": "^1.9.3",
"@google/generative-ai": "^0.18.0",
"@grpc/grpc-js": "^1.9.15",
"@mistralai/mistralai": "^1.5.0",
"@modelcontextprotocol/sdk": "^1.7.0",
"@opentelemetry/api": "^1.4.1",
+15
View File
@@ -0,0 +1,15 @@
syntax = "proto3";
package cline;
import "common.proto";
service BrowserService {
rpc getBrowserConnectionInfo(EmptyRequest) returns (BrowserConnectionInfo);
}
message BrowserConnectionInfo {
bool is_connected = 1;
bool is_remote = 2;
string host = 3; // Optional, may be empty
}
+157
View File
@@ -0,0 +1,157 @@
#!/usr/bin/env node
import * as fs from "fs/promises"
import * as path from "path"
import { execSync } from "child_process"
import { globby } from "globby"
import chalk from "chalk"
// Get script directory and root directory
const SCRIPT_DIR = path.dirname(new URL(import.meta.url).pathname)
const ROOT_DIR = path.resolve(SCRIPT_DIR, "..")
async function main() {
console.log(chalk.bold.blue("Starting Protocol Buffer code generation..."))
// Check if protoc is installed
try {
const options = { stdio: "ignore" }
execSync("protoc --version", options)
} catch (error) {
console.warn(chalk.yellow("Warning: protoc is not installed. Skipping proto generation."))
console.warn(chalk.yellow("To install Protocol Buffers compiler, visit: https://grpc.io/docs/protoc-installation/"))
process.exit(0) // Exit with success as requested
}
// Check if ts-proto plugin is available
const TS_PROTO_PLUGIN = path.join(ROOT_DIR, "node_modules", ".bin", "protoc-gen-ts_proto")
try {
await fs.access(TS_PROTO_PLUGIN)
} catch (error) {
console.error(chalk.red("Error: ts-proto plugin not found at"), TS_PROTO_PLUGIN)
console.error(chalk.red('Please run "npm install" to install the required dependencies.'))
process.exit(1)
}
// Define output directories
const TS_OUT_DIR = path.join(ROOT_DIR, "src", "shared", "proto")
// Create output directory if it doesn't exist
await fs.mkdir(TS_OUT_DIR, { recursive: true })
// Clean up existing generated files
console.log(chalk.cyan("Cleaning up existing generated TypeScript files..."))
const existingFiles = await globby("**/*.ts", { cwd: TS_OUT_DIR })
for (const file of existingFiles) {
await fs.unlink(path.join(TS_OUT_DIR, file))
}
// Process all proto files
console.log(chalk.cyan("Processing proto files from"), SCRIPT_DIR)
const protoFiles = await globby("**/*.proto", { cwd: SCRIPT_DIR })
for (const protoFile of protoFiles) {
console.log(chalk.cyan(`Generating TypeScript code for ${protoFile}...`))
// Build the protoc command with proper path handling for cross-platform
const protocCommand = [
"protoc",
`--plugin=protoc-gen-ts_proto="${TS_PROTO_PLUGIN}"`,
`--ts_proto_out="${TS_OUT_DIR}"`,
"--ts_proto_opt=outputServices=generic-definitions,env=node,esModuleInterop=true,useDate=false,useOptionals=messages",
`--proto_path="${SCRIPT_DIR}"`,
`"${path.join(SCRIPT_DIR, protoFile)}"`,
].join(" ")
try {
const execOptions = {
stdio: "inherit",
}
execSync(protocCommand, execOptions)
} catch (error) {
console.error(chalk.red(`Error generating TypeScript for ${protoFile}:`), error)
process.exit(1)
}
}
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()
// 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)
}
}
async function generateMethodRegistrations() {
console.log(chalk.cyan("Generating method registration files..."))
const serviceDirs = [
path.join(ROOT_DIR, "src", "core", "controller", "browser"),
// Add more service directories here as needed
]
for (const serviceDir of serviceDirs) {
try {
await fs.access(serviceDir)
} catch (error) {
console.log(chalk.gray(`Skipping ${serviceDir} - directory does not exist`))
continue
}
const serviceName = path.basename(serviceDir)
const registryFile = path.join(serviceDir, "methods.ts")
console.log(chalk.cyan(`Generating method registrations for ${serviceName}...`))
// Get all TypeScript files in the service directory
const files = await globby("*.ts", { cwd: serviceDir })
// Filter out index.ts and methods.ts
const implementationFiles = files.filter((file) => file !== "index.ts" && file !== "methods.ts")
// Create the output file with header
let content = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Import all method implementations
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`
}
// Add registration function
content += `\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`
}
// Close the function
content += `}`
// Write the file
await fs.writeFile(registryFile, content)
console.log(chalk.green(`Generated ${registryFile}`))
}
console.log(chalk.green("Method registration files generated successfully."))
}
// Run the main function
main().catch((error) => {
console.error(chalk.red("Error:"), error)
process.exit(1)
})
+13
View File
@@ -0,0 +1,13 @@
syntax = "proto3";
package cline;
message Metadata {
}
message EmptyRequest {
Metadata metadata = 1;
}
message Empty {
}
+3
View File
@@ -0,0 +1,3 @@
{
"type": "module"
}
@@ -0,0 +1,47 @@
import { BrowserConnectionInfo } from "../../../shared/proto/browser"
import { EmptyRequest } from "../../../shared/proto/common"
import { Controller } from "../index"
import { getAllExtensionState } from "../../storage/state"
/**
* Get information about the current browser connection
* @param controller The controller instance
* @param request The request message
* @returns The browser connection info
*/
export async function getBrowserConnectionInfo(controller: Controller, request: EmptyRequest): Promise<BrowserConnectionInfo> {
try {
// Get browser settings from extension state
const { browserSettings } = await getAllExtensionState(controller.context)
// Check if there's an active browser session by using the controller's handleWebviewMessage approach
// This is similar to what's done in controller/index.ts for the "getBrowserConnectionInfo" message
if (controller.task?.browserSession) {
// Access the browser session through the controller's task property
// Using indexer notation to access private property
const browserSession = controller.task.browserSession
const connectionInfo = browserSession.getConnectionInfo()
// Convert from BrowserSession.BrowserConnectionInfo to proto.BrowserConnectionInfo
return {
isConnected: connectionInfo.isConnected,
isRemote: connectionInfo.isRemote,
host: connectionInfo.host || "", // Ensure host is never undefined
}
}
// Fallback to browser settings if no active browser session
return {
isConnected: false,
isRemote: !!browserSettings.remoteBrowserEnabled,
host: browserSettings.remoteBrowserHost || "",
}
} catch (error: unknown) {
console.error("Error getting browser connection info:", error)
return {
isConnected: false,
isRemote: false,
host: "",
}
}
}
+15
View File
@@ -0,0 +1,15 @@
import { createServiceRegistry, ServiceMethodHandler } from "../grpc-service"
import { registerAllMethods } from "./methods"
// Create browser service registry
const browserService = createServiceRegistry("browser")
// Export the method handler type and registration function
export type BrowserMethodHandler = ServiceMethodHandler
export const registerMethod = browserService.registerMethod
// Export the request handler
export const handleBrowserServiceRequest = browserService.handleRequest
// Register all browser methods
registerAllMethods()
+12
View File
@@ -0,0 +1,12 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Import all method implementations
import { registerMethod } from "./index"
import { getBrowserConnectionInfo } from "./getBrowserConnectionInfo"
// Register all browser service methods
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("getBrowserConnectionInfo", getBrowserConnectionInfo)
}
+81
View File
@@ -0,0 +1,81 @@
import { Controller } from "./index"
import { handleBrowserServiceRequest } from "./browser/index"
import { ExtensionMessage } from "../../shared/ExtensionMessage"
/**
* Handles gRPC requests from the webview
*/
export class GrpcHandler {
constructor(private controller: Controller) {}
/**
* Handle a gRPC request from the webview
* @param service The service name
* @param method The method name
* @param message The request message
* @param requestId The request ID for response correlation
* @returns The response message or error
*/
async handleRequest(
service: string,
method: string,
message: any,
requestId: string,
): Promise<{
message?: any
error?: string
request_id: string
}> {
try {
// Handle BrowserService requests
if (service === "cline.BrowserService") {
return {
message: await handleBrowserServiceRequest(this.controller, method, message),
request_id: requestId,
}
}
throw new Error(`Unknown service: ${service}`)
} catch (error) {
return {
error: error instanceof Error ? error.message : String(error),
request_id: requestId,
}
}
}
}
/**
* Handle a gRPC request from the webview
* @param controller The controller instance
* @param request The gRPC request
*/
export async function handleGrpcRequest(
controller: Controller,
request: {
service: string
method: string
message: any
request_id: string
},
) {
try {
const grpcHandler = new GrpcHandler(controller)
const response = await grpcHandler.handleRequest(request.service, request.method, request.message, request.request_id)
// Send the response back to the webview
await controller.postMessageToWebview({
type: "grpc_response",
grpc_response: response,
})
} catch (error) {
// Send error response
await controller.postMessageToWebview({
type: "grpc_response",
grpc_response: {
error: error instanceof Error ? error.message : String(error),
request_id: request.request_id,
},
})
}
}
+65
View File
@@ -0,0 +1,65 @@
import { Controller } from "./index"
/**
* Generic type for service method handlers
*/
export type ServiceMethodHandler = (controller: Controller, message: any) => Promise<any>
/**
* Generic service registry for gRPC services
*/
export class ServiceRegistry {
private serviceName: string
private methodRegistry: Record<string, ServiceMethodHandler> = {}
/**
* Create a new service registry
* @param serviceName The name of the service (used for logging)
*/
constructor(serviceName: string) {
this.serviceName = serviceName
}
/**
* Register a method handler
* @param methodName The name of the method to register
* @param handler The handler function for the method
*/
registerMethod(methodName: string, handler: ServiceMethodHandler): void {
this.methodRegistry[methodName] = handler
console.log(`Registered ${this.serviceName} method: ${methodName}`)
}
/**
* Handle a service request
* @param controller The controller instance
* @param method The method name
* @param message The request message
* @returns The response message
*/
async handleRequest(controller: Controller, method: string, message: any): Promise<any> {
const handler = this.methodRegistry[method]
if (!handler) {
throw new Error(`Unknown ${this.serviceName} method: ${method}`)
}
return handler(controller, message)
}
}
/**
* Create a service registry factory function
* @param serviceName The name of the service
* @returns An object with register and handle functions
*/
export function createServiceRegistry(serviceName: string) {
const registry = new ServiceRegistry(serviceName)
return {
registerMethod: (methodName: string, handler: ServiceMethodHandler) => registry.registerMethod(methodName, handler),
handleRequest: (controller: Controller, method: string, message: any) =>
registry.handleRequest(controller, method, message),
}
}
+8 -31
View File
@@ -7,6 +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 { buildApiHandler } from "../../api"
import { cleanupLegacyCheckpoints } from "../../integrations/checkpoints/CheckpointMigration"
import { downloadTask } from "../../integrations/misc/export-markdown"
@@ -56,7 +57,7 @@ export class Controller {
private postMessage: (message: ExtensionMessage) => Thenable<boolean> | undefined
private disposables: vscode.Disposable[] = []
private task?: Task
task?: Task
workspaceTracker: WorkspaceTracker
mcpHub: McpHub
accountService: ClineAccountService
@@ -299,36 +300,6 @@ export class Controller {
await this.postStateToWebview()
}
break
case "getBrowserConnectionInfo":
try {
// Get the current browser session from Cline if it exists
if (this.task?.browserSession) {
const connectionInfo = this.task.browserSession.getConnectionInfo()
await this.postMessageToWebview({
type: "browserConnectionInfo",
isConnected: connectionInfo.isConnected,
isRemote: connectionInfo.isRemote,
host: connectionInfo.host,
})
} else {
// If no active browser session, just return the settings
const { browserSettings } = await getAllExtensionState(this.context)
await this.postMessageToWebview({
type: "browserConnectionInfo",
isConnected: false,
isRemote: !!browserSettings.remoteBrowserEnabled,
host: browserSettings.remoteBrowserHost,
})
}
} catch (error) {
console.error("Error getting browser connection info:", error)
await this.postMessageToWebview({
type: "browserConnectionInfo",
isConnected: false,
isRemote: false,
})
}
break
case "testBrowserConnection":
try {
const { browserSettings } = await getAllExtensionState(this.context)
@@ -910,6 +881,12 @@ export class Controller {
}
break
}
case "grpc_request": {
if (message.grpc_request) {
await handleGrpcRequest(this, message.grpc_request)
}
break
}
// Add more switch case statements here as more webview message commands
// are created within the webview context (i.e. inside media/main.js)
}
+6 -7
View File
@@ -43,12 +43,12 @@ export interface ExtensionMessage {
| "totalTasksSize"
| "addToInput"
| "browserConnectionResult"
| "browserConnectionInfo"
| "detectedChromePath"
| "scrollToSettings"
| "browserRelaunchResult"
| "relativePathsResponse" // Handles single and multiple path responses
| "fileSearchResults"
| "grpc_response" // New type for gRPC responses
text?: string
paths?: (string | null)[] // Used for relativePathsResponse
action?:
@@ -109,6 +109,11 @@ export interface ExtensionMessage {
error?: string
}
tab?: McpViewTab
grpc_response?: {
message?: any // JSON serialized protobuf message
request_id: string // Same ID as the request
error?: string // Optional error message
}
}
export type Invoke = "sendMessage" | "primaryButtonClick" | "secondaryButtonClick"
@@ -236,12 +241,6 @@ export type BrowserActionResult = {
currentMousePosition?: string
}
export interface BrowserConnectionInfo {
isConnected: boolean
isRemote: boolean
host?: string
}
export interface ClineAskUseMcpServer {
serverName: string
type: "use_mcp_tool" | "access_mcp_resource"
+7 -1
View File
@@ -74,13 +74,13 @@ export interface WebviewMessage {
| "requestTotalTasksSize"
| "relaunchChromeDebugMode"
| "taskFeedback"
| "getBrowserConnectionInfo"
| "getDetectedChromePath"
| "detectedChromePath"
| "scrollToSettings"
| "getRelativePaths" // Handles single and multiple URI resolution
| "searchFiles"
| "toggleFavoriteModel"
| "grpc_request"
// | "relaunchChromeDebugMode"
text?: string
uris?: string[] // Used for getRelativePaths
@@ -117,6 +117,12 @@ export interface WebviewMessage {
query?: string
// For toggleFavoriteModel
modelId?: string
grpc_request?: {
service: string
method: string
message: any // JSON serialized protobuf message
request_id: string // For correlating requests and responses
}
}
export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse"
+156
View File
@@ -0,0 +1,156 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.7.0
// protoc v6.30.1
// source: browser.proto
/* eslint-disable */
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"
import { EmptyRequest } from "./common"
export const protobufPackage = "cline"
export interface BrowserConnectionInfo {
isConnected: boolean
isRemote: boolean
/** Optional, may be empty */
host: string
}
function createBaseBrowserConnectionInfo(): BrowserConnectionInfo {
return { isConnected: false, isRemote: false, host: "" }
}
export const BrowserConnectionInfo: MessageFns<BrowserConnectionInfo> = {
encode(message: BrowserConnectionInfo, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.isConnected !== false) {
writer.uint32(8).bool(message.isConnected)
}
if (message.isRemote !== false) {
writer.uint32(16).bool(message.isRemote)
}
if (message.host !== "") {
writer.uint32(26).string(message.host)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): BrowserConnectionInfo {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseBrowserConnectionInfo()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 8) {
break
}
message.isConnected = reader.bool()
continue
}
case 2: {
if (tag !== 16) {
break
}
message.isRemote = reader.bool()
continue
}
case 3: {
if (tag !== 26) {
break
}
message.host = reader.string()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): BrowserConnectionInfo {
return {
isConnected: isSet(object.isConnected) ? globalThis.Boolean(object.isConnected) : false,
isRemote: isSet(object.isRemote) ? globalThis.Boolean(object.isRemote) : false,
host: isSet(object.host) ? globalThis.String(object.host) : "",
}
},
toJSON(message: BrowserConnectionInfo): unknown {
const obj: any = {}
if (message.isConnected !== false) {
obj.isConnected = message.isConnected
}
if (message.isRemote !== false) {
obj.isRemote = message.isRemote
}
if (message.host !== "") {
obj.host = message.host
}
return obj
},
create<I extends Exact<DeepPartial<BrowserConnectionInfo>, I>>(base?: I): BrowserConnectionInfo {
return BrowserConnectionInfo.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<BrowserConnectionInfo>, I>>(object: I): BrowserConnectionInfo {
const message = createBaseBrowserConnectionInfo()
message.isConnected = object.isConnected ?? false
message.isRemote = object.isRemote ?? false
message.host = object.host ?? ""
return message
},
}
export type BrowserServiceDefinition = typeof BrowserServiceDefinition
export const BrowserServiceDefinition = {
name: "BrowserService",
fullName: "cline.BrowserService",
methods: {
getBrowserConnectionInfo: {
name: "getBrowserConnectionInfo",
requestType: EmptyRequest,
requestStream: false,
responseType: BrowserConnectionInfo,
responseStream: false,
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
}
+193
View File
@@ -0,0 +1,193 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.7.0
// protoc v6.30.1
// source: common.proto
/* eslint-disable */
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"
export const protobufPackage = "cline"
export interface Metadata {}
export interface EmptyRequest {
metadata?: Metadata | undefined
}
export interface Empty {}
function createBaseMetadata(): Metadata {
return {}
}
export const Metadata: MessageFns<Metadata> = {
encode(_: Metadata, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): Metadata {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseMetadata()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(_: any): Metadata {
return {}
},
toJSON(_: Metadata): unknown {
const obj: any = {}
return obj
},
create<I extends Exact<DeepPartial<Metadata>, I>>(base?: I): Metadata {
return Metadata.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<Metadata>, I>>(_: I): Metadata {
const message = createBaseMetadata()
return message
},
}
function createBaseEmptyRequest(): EmptyRequest {
return { metadata: undefined }
}
export const EmptyRequest: MessageFns<EmptyRequest> = {
encode(message: EmptyRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.metadata !== undefined) {
Metadata.encode(message.metadata, writer.uint32(10).fork()).join()
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): EmptyRequest {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseEmptyRequest()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.metadata = Metadata.decode(reader, reader.uint32())
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): EmptyRequest {
return { metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined }
},
toJSON(message: EmptyRequest): unknown {
const obj: any = {}
if (message.metadata !== undefined) {
obj.metadata = Metadata.toJSON(message.metadata)
}
return obj
},
create<I extends Exact<DeepPartial<EmptyRequest>, I>>(base?: I): EmptyRequest {
return EmptyRequest.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<EmptyRequest>, I>>(object: I): EmptyRequest {
const message = createBaseEmptyRequest()
message.metadata =
object.metadata !== undefined && object.metadata !== null ? Metadata.fromPartial(object.metadata) : undefined
return message
},
}
function createBaseEmpty(): Empty {
return {}
}
export const Empty: MessageFns<Empty> = {
encode(_: Empty, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): Empty {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseEmpty()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(_: any): Empty {
return {}
},
toJSON(_: Empty): unknown {
const obj: any = {}
return obj
},
create<I extends Exact<DeepPartial<Empty>, I>>(base?: I): Empty {
return Empty.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<Empty>, I>>(_: I): Empty {
const message = createBaseEmpty()
return message
},
}
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
}
+23 -13
View File
@@ -27,7 +27,8 @@
"react-use": "^17.6.0",
"react-virtuoso": "^4.12.3",
"rehype-highlight": "^7.0.1",
"styled-components": "^6.1.15"
"styled-components": "^6.1.15",
"uuid": "^9.0.1"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
@@ -40,6 +41,7 @@
"@types/node": "^22.13.4",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@types/uuid": "^9.0.8",
"@types/vscode-webview": "^1.57.5",
"@vitejs/plugin-react-swc": "^3.5.0",
"@vitest/coverage-v8": "^3.0.9",
@@ -3539,6 +3541,13 @@
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
"license": "MIT"
},
"node_modules/@types/uuid": {
"version": "9.0.8",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz",
"integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/vscode-webview": {
"version": "1.57.5",
"resolved": "https://registry.npmjs.org/@types/vscode-webview/-/vscode-webview-1.57.5.tgz",
@@ -6936,18 +6945,6 @@
"uuid": "^9.0.1"
}
},
"node_modules/mermaid/node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/micromark": {
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/micromark/-/micromark-2.11.4.tgz",
@@ -8836,6 +8833,19 @@
}
}
},
"node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/vfile": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
+3 -1
View File
@@ -33,7 +33,8 @@
"react-use": "^17.6.0",
"react-virtuoso": "^4.12.3",
"rehype-highlight": "^7.0.1",
"styled-components": "^6.1.15"
"styled-components": "^6.1.15",
"uuid": "^9.0.1"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
@@ -46,6 +47,7 @@
"@types/node": "^22.13.4",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@types/uuid": "^9.0.8",
"@types/vscode-webview": "^1.57.5",
"@vitejs/plugin-react-swc": "^3.5.0",
"@vitest/coverage-v8": "^3.0.9",
@@ -3,6 +3,8 @@ import { useEffect, useRef, useState } from "react"
import styled from "styled-components"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { vscode } from "@/utils/vscode"
import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
import { BrowserServiceClient } from "../../services/grpc-client"
interface ConnectionInfo {
isConnected: boolean
@@ -21,29 +23,25 @@ export const BrowserSettingsMenu = () => {
})
const popoverRef = useRef<HTMLDivElement>(null)
// Get actual connection info from the browser session
// Get actual connection info from the browser session using gRPC
useEffect(() => {
// Request connection info when component mounts
vscode.postMessage({
type: "getBrowserConnectionInfo",
})
// Listen for connection info updates
const handleMessage = (event: MessageEvent) => {
const message = event.data
if (message.type === "browserConnectionInfo") {
// Function to fetch connection info
;(async () => {
try {
console.log("[DEBUG] SENDING BROWSER CONNECTION INFO REQUEST")
const info = await BrowserServiceClient.getBrowserConnectionInfo({})
console.log("[DEBUG] GOT BROWSER REPLY:", info, typeof info)
setConnectionInfo({
isConnected: message.isConnected,
isRemote: message.isRemote,
host: message.host,
isConnected: info.isConnected,
isRemote: info.isRemote,
host: info.host,
})
} catch (error) {
console.error("Error fetching browser connection info:", error)
}
}
})()
window.addEventListener("message", handleMessage)
return () => {
window.removeEventListener("message", handleMessage)
}
// No need for message event listeners anymore!
}, [browserSettings.remoteBrowserHost, browserSettings.remoteBrowserEnabled])
// Close popover when clicking outside
@@ -84,11 +82,22 @@ export const BrowserSettingsMenu = () => {
const toggleInfoPopover = () => {
setShowInfoPopover(!showInfoPopover)
// Request updated connection info when opening the popover
// Request updated connection info when opening the popover using gRPC
if (!showInfoPopover) {
vscode.postMessage({
type: "getBrowserConnectionInfo",
})
const fetchConnectionInfo = async () => {
try {
const info = await BrowserServiceClient.getBrowserConnectionInfo({})
setConnectionInfo({
isConnected: info.isConnected,
isRemote: info.isRemote,
host: info.host,
})
} catch (error) {
console.error("Error fetching browser connection info:", error)
}
}
fetchConnectionInfo()
}
}
@@ -112,19 +121,27 @@ export const BrowserSettingsMenu = () => {
}
}
// Check connection status every second to keep icon in sync
// Check connection status every second to keep icon in sync using gRPC
useEffect(() => {
// Function to fetch connection info
const fetchConnectionInfo = async () => {
try {
const info = await BrowserServiceClient.getBrowserConnectionInfo({})
setConnectionInfo({
isConnected: info.isConnected,
isRemote: info.isRemote,
host: info.host,
})
} catch (error) {
console.error("Error fetching browser connection info:", error)
}
}
// Request connection info immediately
vscode.postMessage({
type: "getBrowserConnectionInfo",
})
fetchConnectionInfo()
// Set up interval to refresh every second
const intervalId = setInterval(() => {
vscode.postMessage({
type: "getBrowserConnectionInfo",
})
}, 1000)
const intervalId = setInterval(fetchConnectionInfo, 1000)
return () => clearInterval(intervalId)
}, [])
+89
View File
@@ -0,0 +1,89 @@
import { vscode } from "../utils/vscode"
import { v4 as uuidv4 } from "uuid"
import { BrowserServiceDefinition } from "@shared/proto/browser"
import { EmptyRequest } from "@shared/proto/common"
// 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 = "{}"
if (Object.keys(request).length !== 0) {
encodedRequest = request.toJSON()
console.log("[DEBUG] request is", request, encodedRequest)
}
// 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
}
// Create the Browser Service Client singleton with inferred types
// No need for manual interface definition - types are inferred from the service definition
const BrowserServiceClient = createGrpcClient(BrowserServiceDefinition)
// Export the Browser Service Client as a static object
export { BrowserServiceClient }