mirror of
https://github.com/cline/cline.git
synced 2026-09-24 23:20:16 +08:00
Protobus Host Bridge (#3747)
* WIP host bridge
* Run formatter
* remove tmp impl & rename host grpc client
* gitignore more files
* better layout
* host handler to make other hosts easier to add
* remove adapter pattern
* get host responses correctly
* fix streaming mode for host bridge
* first wip subscription host bridge demo for watching mcp server config
* format, comment
* add cancellation for host grpc stream
* remove unneeded functions from host-grpc-handler
* add a method for canceling request rather than using the registry
* another todo
* use StringRequest for uri.proto
* debounce new file watcher
* remove test setup
* remove some todos and logs
* remove registry use todo
* Revert "remove registry use todo"
This reverts commit 84078d3469.
* fix capitalization of uri.proto
* a better pattern for a callback based bridge without using the requestRegistry directly
---------
Co-authored-by: Andrei Edell <andrei@nugbase.com>
Co-authored-by: Sarah Fortune <sarah.fortune@gmail.com>
Co-authored-by: Andrei Eternal <eternal@cline.bot>
This commit is contained in:
co-authored by
Andrei Edell
Sarah Fortune
Andrei Eternal
parent
47899ac52d
commit
cbcd17764b
@@ -3,6 +3,8 @@ import { RelativePathsRequest, RelativePaths } from "@shared/proto/file"
|
||||
import { FileMethodHandler } from "./index"
|
||||
import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
import { UriServiceClient } from "../../../standalone/services/host-grpc-client"
|
||||
import { Metadata, StringRequest } from "@shared/proto/common"
|
||||
|
||||
/**
|
||||
* Converts a list of URIs to workspace-relative paths
|
||||
@@ -17,7 +19,15 @@ export const getRelativePaths: FileMethodHandler = async (
|
||||
const resolvedPaths = await Promise.all(
|
||||
request.uris.map(async (uriString) => {
|
||||
try {
|
||||
const fileUri = vscode.Uri.parse(uriString, true)
|
||||
// Use the host URI service client instead of directly using vscode.Uri.parse
|
||||
const parseResponse = await UriServiceClient.parse(
|
||||
StringRequest.create({
|
||||
metadata: Metadata.create({}),
|
||||
value: uriString,
|
||||
}),
|
||||
)
|
||||
const fileUri = vscode.Uri.parse(`${parseResponse.scheme}://${parseResponse.authority}${parseResponse.path}`)
|
||||
console.log("[DEBUG] UriServiceClient.parse:", fileUri)
|
||||
const relativePathToGet = vscode.workspace.asRelativePath(fileUri, false)
|
||||
|
||||
// If the path is still absolute, it's outside the workspace
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Controller } from ".."
|
||||
import { Empty } from "../../../shared/proto/common"
|
||||
import { NewTaskRequest } from "../../../shared/proto/task"
|
||||
import { handleFileServiceRequest } from "../file"
|
||||
|
||||
/**
|
||||
* Creates a new task with the given text and optional images
|
||||
|
||||
+39
-13
@@ -17,6 +17,9 @@ import * as fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { z } from "zod"
|
||||
import { WatchServiceClient } from "../../standalone/services/host-grpc-client"
|
||||
import { FileChangeEvent_ChangeType, SubscribeToFileRequest } from "../../shared/proto/host/watch"
|
||||
import { Metadata } from "../../shared/proto/common"
|
||||
import {
|
||||
DEFAULT_MCP_TIMEOUT_SECONDS,
|
||||
McpMode,
|
||||
@@ -118,22 +121,45 @@ export class McpHub {
|
||||
|
||||
private async watchMcpSettingsFile(): Promise<void> {
|
||||
const settingsPath = await this.getMcpSettingsFilePath()
|
||||
this.disposables.push(
|
||||
vscode.workspace.onDidSaveTextDocument(async (document) => {
|
||||
if (arePathsEqual(document.uri.fsPath, settingsPath)) {
|
||||
const settings = await this.readAndValidateMcpSettingsFile()
|
||||
if (settings) {
|
||||
try {
|
||||
vscode.window.showInformationMessage("Updating MCP servers...")
|
||||
await this.updateServerConnections(settings.mcpServers)
|
||||
vscode.window.showInformationMessage("MCP servers updated")
|
||||
} catch (error) {
|
||||
console.error("Failed to process MCP settings change:", error)
|
||||
|
||||
// Subscribe to file changes using the gRPC WatchService
|
||||
console.log("[DEBUG] subscribing to mcp file changes")
|
||||
const cancelSubscription = WatchServiceClient.subscribeToFile(
|
||||
SubscribeToFileRequest.create({
|
||||
metadata: Metadata.create({}),
|
||||
path: settingsPath,
|
||||
}),
|
||||
{
|
||||
onResponse: async (response) => {
|
||||
console.log(
|
||||
`[DEBUG] MCP settings ${response.type === FileChangeEvent_ChangeType.CHANGED ? "changed" : "event"}`,
|
||||
)
|
||||
|
||||
// Only process the file if it was changed (not created or deleted)
|
||||
if (response.type === FileChangeEvent_ChangeType.CHANGED) {
|
||||
const settings = await this.readAndValidateMcpSettingsFile()
|
||||
if (settings) {
|
||||
try {
|
||||
vscode.window.showInformationMessage("Updating MCP servers...")
|
||||
await this.updateServerConnections(settings.mcpServers)
|
||||
vscode.window.showInformationMessage("MCP servers updated")
|
||||
} catch (error) {
|
||||
console.error("Failed to process MCP settings change:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Error watching MCP settings file:", error)
|
||||
},
|
||||
onComplete: () => {
|
||||
console.log("[DEBUG] MCP settings file watch completed")
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// Add the cancellation function to disposables
|
||||
this.disposables.push({ dispose: cancelSubscription })
|
||||
}
|
||||
|
||||
private async initializeMcpServers(): Promise<void> {
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
import { GrpcHandler, StreamingCallbacks } from "../../../hosts/vscode/host-grpc-handler"
|
||||
|
||||
// Generic type for any protobuf service definition
|
||||
export type ProtoService = {
|
||||
name: string
|
||||
fullName: string
|
||||
methods: {
|
||||
[key: string]: {
|
||||
name: string
|
||||
requestType: any
|
||||
responseType: any
|
||||
requestStream: boolean
|
||||
responseStream: boolean
|
||||
options: any
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Define a unified client type that handles both unary and streaming methods
|
||||
export type GrpcClientType<T extends ProtoService> = {
|
||||
[K in keyof T["methods"]]: T["methods"][K]["responseStream"] extends true
|
||||
? (
|
||||
request: InstanceType<T["methods"][K]["requestType"]>,
|
||||
options: StreamingCallbacks<InstanceType<T["methods"][K]["responseType"]>>,
|
||||
) => () => void // Returns a cancel function
|
||||
: (request: InstanceType<T["methods"][K]["requestType"]>) => Promise<InstanceType<T["methods"][K]["responseType"]>>
|
||||
}
|
||||
|
||||
// Create a client for any protobuf service with inferred types
|
||||
export function createGrpcClient<T extends ProtoService>(service: T): GrpcClientType<T> {
|
||||
const client = {} as GrpcClientType<T>
|
||||
const grpcHandler = new GrpcHandler()
|
||||
|
||||
Object.values(service.methods).forEach((method) => {
|
||||
// Streaming method implementation
|
||||
if (method.responseStream) {
|
||||
// Use lowercase method name as the key in the client object
|
||||
const methodKey = method.name.charAt(0).toLowerCase() + method.name.slice(1)
|
||||
client[methodKey as keyof GrpcClientType<T>] = ((
|
||||
request: any,
|
||||
options: StreamingCallbacks<InstanceType<typeof method.responseType>>,
|
||||
) => {
|
||||
// Use handleRequest with streaming callbacks
|
||||
const requestId = uuidv4()
|
||||
console.log(`[DEBUG] Streaming gRPC host call to ${service.fullName}.${methodKey} req:${requestId}`)
|
||||
|
||||
// We need to await the promise and then return the cancel function
|
||||
return (async () => {
|
||||
try {
|
||||
const result = await grpcHandler.handleRequest<InstanceType<typeof method.responseType>>(
|
||||
service.fullName,
|
||||
methodKey,
|
||||
request,
|
||||
requestId,
|
||||
options,
|
||||
)
|
||||
|
||||
// If the result is a function, it's the cancel function
|
||||
if (typeof result === "function") {
|
||||
return result
|
||||
} else {
|
||||
// This shouldn't happen, but just in case
|
||||
console.error(`Expected cancel function but got response object for streaming request: ${requestId}`)
|
||||
return () => {}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error in streaming request: ${error}`)
|
||||
if (options.onError) {
|
||||
options.onError(error instanceof Error ? error : new Error(String(error)))
|
||||
}
|
||||
return () => {}
|
||||
}
|
||||
})()
|
||||
}) as any
|
||||
} else {
|
||||
// Unary method implementation
|
||||
const methodKey = method.name.charAt(0).toLowerCase() + method.name.slice(1)
|
||||
client[methodKey as keyof GrpcClientType<T>] = ((request: any) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const requestId = uuidv4()
|
||||
console.log(`[DEBUG] gRPC host call to ${service.fullName}.${methodKey} req:${requestId}`)
|
||||
try {
|
||||
const response = await grpcHandler.handleRequest(service.fullName, methodKey, request, requestId)
|
||||
console.log(`[DEBUG] gRPC host resp to ${service.fullName}.${methodKey} req:${requestId}`)
|
||||
|
||||
// Check if the response is a function (streaming) or an object (unary)
|
||||
if (typeof response === "function") {
|
||||
// This shouldn't happen for unary requests
|
||||
throw new Error("Received streaming response for unary request")
|
||||
} else if (response && response.message) {
|
||||
resolve(response.message)
|
||||
} else {
|
||||
throw new Error("gRPC response didn't have a message")
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`[DEBUG] gRPC host ERR to ${service.fullName}.${methodKey} req:${requestId} err:${e}`)
|
||||
reject(e)
|
||||
}
|
||||
})
|
||||
}) as any
|
||||
}
|
||||
})
|
||||
|
||||
return client
|
||||
}
|
||||
Reference in New Issue
Block a user