mirror of
https://github.com/cline/cline.git
synced 2026-09-09 15:02:23 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6fefa133e | ||
|
|
b66b695321 | ||
|
|
005576ba73 | ||
|
|
978b61df84 | ||
|
|
f61bbe84eb | ||
|
|
5a34c78aa3 |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: respect setting litellm models for plan and act
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Migrate didBecomeVisible to protobus
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Prevent reading IS_DEV from the users environment
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix(bedrock): remove custom Model encode
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
The close task button and delete task button in the task header are now correctly announced by screen readers.
|
||||
+1
-8
@@ -21,14 +21,7 @@
|
||||
"semi": "off",
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
"eslint-rules/no-protobuf-object-literals": "error",
|
||||
"eslint-rules/no-grpc-client-object-literals": "error",
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
{
|
||||
"selector": "VariableDeclarator[id.type=\"ObjectPattern\"][init.object.name=\"process\"][init.property.name=\"env\"]",
|
||||
"message": "Use process.env.VARIABLE_NAME directly instead of destructuring"
|
||||
}
|
||||
]
|
||||
"eslint-rules/no-grpc-client-object-literals": "error"
|
||||
},
|
||||
"ignorePatterns": ["out", "dist", "**/*.d.ts"]
|
||||
}
|
||||
|
||||
@@ -35,4 +35,5 @@ webview-ui/src/services/grpc-client.ts
|
||||
# Host bridge
|
||||
src/hosts/vscode/*/methods.ts
|
||||
src/hosts/vscode/*/index.ts
|
||||
src/hosts/vscode/client/host-grpc-client.ts
|
||||
src/hosts/vscode/host-grpc-service-config.ts
|
||||
|
||||
+3
-5
@@ -125,11 +125,9 @@ const baseConfig = {
|
||||
minify: production,
|
||||
sourcemap: !production,
|
||||
logLevel: "silent",
|
||||
define: production
|
||||
? {
|
||||
"process.env.IS_DEV": JSON.stringify(!production),
|
||||
}
|
||||
: undefined,
|
||||
define: {
|
||||
"process.env.IS_DEV": JSON.stringify(!production),
|
||||
},
|
||||
tsconfig: path.resolve(__dirname, "tsconfig.json"),
|
||||
plugins: [
|
||||
copyWasmFiles,
|
||||
|
||||
+1
-1
@@ -330,7 +330,7 @@
|
||||
"watch:esbuild": "node esbuild.js --watch",
|
||||
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
|
||||
"package": "npm run check-types && npm run build:webview && npm run lint && node esbuild.js --production",
|
||||
"protos": "node proto/build-proto.js && node scripts/generate-server-setup.mjs && node scripts/generate-host-bridge-client.mjs",
|
||||
"protos": "node proto/build-proto.js && node scripts/generate-server-setup.mjs",
|
||||
"postprotos": "prettier src/shared/proto src/core/controller src/hosts/ webview-ui/src/services src/generated --write --log-level warn",
|
||||
"compile-tests": "node ./scripts/build-tests.js",
|
||||
"watch-tests": "tsc -p . -w --outDir out",
|
||||
|
||||
+60
-38
@@ -177,9 +177,9 @@ export {
|
||||
${serviceExports.join(",\n\t")}
|
||||
}`
|
||||
|
||||
const filePath = path.join(ROOT_DIR, "webview-ui/src/services/grpc-client.ts")
|
||||
await writeFileWithMkdirs(filePath, content)
|
||||
log_verbose(chalk.green(`Generated gRPC client at ${filePath}`))
|
||||
const configPath = path.join(ROOT_DIR, "webview-ui", "src", "services", "grpc-client.ts")
|
||||
await fs.writeFile(configPath, content)
|
||||
log_verbose(chalk.green(`Generated gRPC client at ${configPath}`))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -248,7 +248,17 @@ async function generateMethodRegistrations() {
|
||||
const streamingMethodsMap = await parseProtoForStreamingMethods(protoFiles, SCRIPT_DIR)
|
||||
|
||||
for (const serviceDir of serviceDirs) {
|
||||
try {
|
||||
await fs.access(serviceDir)
|
||||
} catch (error) {
|
||||
log_verbose(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) || []
|
||||
|
||||
@@ -304,8 +314,7 @@ export function registerAllMethods(): void {
|
||||
methodsContent += `}`
|
||||
|
||||
// Write the methods.ts file
|
||||
const registryFile = path.join(serviceDir, "methods.ts")
|
||||
await writeFileWithMkdirs(registryFile, methodsContent)
|
||||
await fs.writeFile(registryFile, methodsContent)
|
||||
log_verbose(chalk.green(`Generated ${registryFile}`))
|
||||
|
||||
// Generate index.ts file
|
||||
@@ -334,8 +343,7 @@ export const isStreamingMethod = ${serviceName}Service.isStreamingMethod
|
||||
registerAllMethods()`
|
||||
|
||||
// Write the index.ts file
|
||||
const indexFile = path.join(serviceDir, "index.ts")
|
||||
await writeFileWithMkdirs(indexFile, indexContent)
|
||||
await fs.writeFile(indexFile, indexContent)
|
||||
log_verbose(chalk.green(`Generated ${indexFile}`))
|
||||
}
|
||||
|
||||
@@ -386,8 +394,8 @@ export interface ServiceHandlerConfig {
|
||||
export const serviceHandlers: Record<string, ServiceHandlerConfig> = {${serviceConfigs.join(",")}
|
||||
};`
|
||||
|
||||
const configPath = path.join(ROOT_DIR, "src/core/controller/grpc-service-config.ts")
|
||||
await writeFileWithMkdirs(configPath, content)
|
||||
const configPath = path.join(ROOT_DIR, "src", "core", "controller", "grpc-service-config.ts")
|
||||
await fs.writeFile(configPath, content)
|
||||
log_verbose(chalk.green(`Generated service configuration at ${configPath}`))
|
||||
}
|
||||
|
||||
@@ -454,7 +462,17 @@ async function generateHostMethodRegistrations() {
|
||||
const streamingMethodsMap = await parseProtoForStreamingMethods(hostProtoFiles, path.join(SCRIPT_DIR, "host"))
|
||||
|
||||
for (const serviceDir of hostServiceDirs) {
|
||||
try {
|
||||
await fs.access(serviceDir)
|
||||
} catch (error) {
|
||||
log_verbose(chalk.cyan(`Creating directory ${serviceDir} for new host 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 = hostServiceNameMap[serviceName]
|
||||
const streamingMethods = streamingMethodsMap.get(fullServiceName) || []
|
||||
|
||||
@@ -510,8 +528,7 @@ export function registerAllMethods(): void {
|
||||
methodsContent += `}`
|
||||
|
||||
// Write the methods.ts file
|
||||
const registryFile = path.join(serviceDir, "methods.ts")
|
||||
await writeFileWithMkdirs(registryFile, methodsContent)
|
||||
await fs.writeFile(registryFile, methodsContent)
|
||||
log_verbose(chalk.green(`Generated ${registryFile}`))
|
||||
|
||||
// Generate index.ts file
|
||||
@@ -540,8 +557,7 @@ export const isStreamingMethod = ${serviceName}Service.isStreamingMethod
|
||||
registerAllMethods()`
|
||||
|
||||
// Write the index.ts file
|
||||
const indexFile = path.join(serviceDir, "index.ts")
|
||||
await writeFileWithMkdirs(indexFile, indexContent)
|
||||
await fs.writeFile(indexFile, indexContent)
|
||||
log_verbose(chalk.green(`Generated ${indexFile}`))
|
||||
}
|
||||
|
||||
@@ -590,9 +606,10 @@ export interface HostServiceHandlerConfig {
|
||||
export const hostServiceHandlers: Record<string, HostServiceHandlerConfig> = {${serviceConfigs.join(",")}
|
||||
};`
|
||||
|
||||
const filePath = path.join(ROOT_DIR, "src/hosts/vscode/host-grpc-service-config.ts")
|
||||
await writeFileWithMkdirs(filePath, content)
|
||||
log_verbose(chalk.green(`Generated host service configuration at ${filePath}`))
|
||||
const configPath = path.join(ROOT_DIR, "src", "hosts", "vscode", "host-grpc-service-config.ts")
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true })
|
||||
await fs.writeFile(configPath, content)
|
||||
log_verbose(chalk.green(`Generated host service configuration at ${configPath}`))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -600,29 +617,44 @@ export const hostServiceHandlers: Record<string, HostServiceHandlerConfig> = {${
|
||||
*/
|
||||
async function generateHostGrpcClientConfig() {
|
||||
log_verbose(chalk.cyan("Generating host gRPC client configuration..."))
|
||||
const clients = []
|
||||
|
||||
const serviceImports = []
|
||||
const serviceClientCreations = []
|
||||
const serviceExports = []
|
||||
|
||||
// Process each service in the hostServiceNameMap
|
||||
for (const [_dirName, fullServiceName] of Object.entries(hostServiceNameMap)) {
|
||||
const serviceName = fullServiceName.replace(/.*\./, "")
|
||||
clients.push(`${serviceName}Client: createGrpcClient(${fullServiceName}Definition)`)
|
||||
for (const [dirName, _fullServiceName] of Object.entries(hostServiceNameMap)) {
|
||||
const capitalizedName = dirName.charAt(0).toUpperCase() + dirName.slice(1)
|
||||
|
||||
// Add import statement
|
||||
serviceImports.push(`import { ${capitalizedName}ServiceDefinition } from "@shared/proto/host/${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 "@hosts/vscode/client/host-grpc-client-base"
|
||||
import { HostBridgeClientProvider } from "@/hosts/host-bridge-client"
|
||||
import * as host from "@shared/proto/index.host"
|
||||
import { createGrpcClient } from "./host-grpc-client-base"
|
||||
${serviceImports.join("\n")}
|
||||
|
||||
export const vscodeHostBridgeClient: HostBridgeClientProvider = {
|
||||
${clients.join(",\n\t")}
|
||||
${serviceClientCreations.join("\n")}
|
||||
|
||||
export {
|
||||
${serviceExports.join(",\n\t")}
|
||||
}`
|
||||
|
||||
const filePath = path.join(ROOT_DIR, "src/generated/hosts/vscode/client/host-grpc-client.ts")
|
||||
await writeFileWithMkdirs(filePath, content)
|
||||
log_verbose(chalk.green(`Generated host gRPC client at ${filePath}`))
|
||||
const configPath = path.join(ROOT_DIR, "src", "hosts", "vscode", "client", "host-grpc-client.ts")
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true })
|
||||
await fs.writeFile(configPath, content)
|
||||
log_verbose(chalk.green(`Generated host gRPC client at ${configPath}`))
|
||||
}
|
||||
|
||||
async function cleanup() {
|
||||
@@ -632,10 +664,8 @@ async function cleanup() {
|
||||
for (const file of existingFiles) {
|
||||
await fs.unlink(path.join(TS_OUT_DIR, file))
|
||||
}
|
||||
await rmdir(path.join(ROOT_DIR, "src/generated"))
|
||||
|
||||
// Clean up generated files that were moved.
|
||||
await fs.rm(path.join(ROOT_DIR, "src/hosts/vscode/client/host-grpc-client.ts"), { force: true })
|
||||
await fs.rm(path.join(ROOT_DIR, "src/standalone/services/host-grpc-client.ts"), { force: true })
|
||||
await rmdir(path.join(ROOT_DIR, "src/standalone/services"))
|
||||
|
||||
@@ -645,14 +675,6 @@ async function cleanup() {
|
||||
await fs.rm(path.join(ROOT_DIR, "src/standalone/server-setup.ts"), { force: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* Write `contents` to `filePath`, creating any necessary directories in `filePath`.
|
||||
*/
|
||||
async function writeFileWithMkdirs(filePath, content) {
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
||||
await fs.writeFile(filePath, content)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an empty dir, do nothing if the directory doesn't exist or is not empty.
|
||||
*/
|
||||
|
||||
+1
-1
@@ -236,4 +236,4 @@ message ModelsApiConfiguration {
|
||||
optional string sap_ai_core_token_url = 71;
|
||||
optional string sap_ai_core_base_url = 72;
|
||||
optional string claude_code_path = 73;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,7 +259,4 @@ service UiService {
|
||||
|
||||
// Subscribe to focus chat input events with client ID
|
||||
rpc subscribeToFocusChatInput(StringRequest) returns (stream Empty);
|
||||
|
||||
// Subscribe to webview visibility change events
|
||||
rpc subscribeToDidBecomeVisible(EmptyRequest) returns (stream Empty);
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ const srcConfig = {
|
||||
format: "cjs",
|
||||
platform: "node",
|
||||
define: {
|
||||
"process.env.IS_DEV": "true",
|
||||
"process.env.IS_TEST": "true",
|
||||
},
|
||||
external: ["vscode"],
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import * as fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import * as grpc from "@grpc/grpc-js"
|
||||
import * as protoLoader from "@grpc/proto-loader"
|
||||
import chalk from "chalk"
|
||||
|
||||
const IMPL_FILE = path.resolve("src/generated/standalone/host-bridge-clients.ts")
|
||||
const INTERFACE_FILE = path.resolve("src/generated/hosts/host-bridge-client-types.ts")
|
||||
const DESCRIPTOR_SET = path.resolve("dist-standalone/proto/descriptor_set.pb")
|
||||
|
||||
const typeNameToFQN = new Map()
|
||||
|
||||
function addTypeNameToFqn(name, fqn) {
|
||||
if (typeNameToFQN.has(name)) {
|
||||
throw new Error(`Proto type ${name} redefined (${fqn}).`)
|
||||
}
|
||||
typeNameToFQN.set(name, fqn)
|
||||
}
|
||||
function getFqn(name) {
|
||||
if (!typeNameToFQN.has(name)) {
|
||||
throw Error(`No FQN for ${name}`)
|
||||
}
|
||||
return typeNameToFQN.get(name)
|
||||
}
|
||||
/**
|
||||
* Main function to generate the host bridge client
|
||||
*/
|
||||
async function main() {
|
||||
// Load service definitions from descriptor set
|
||||
const descriptorBuffer = await fs.readFile(DESCRIPTOR_SET)
|
||||
const packageDefinition = protoLoader.loadFileDescriptorSetFromBuffer(descriptorBuffer)
|
||||
const proto = grpc.loadPackageDefinition(packageDefinition)
|
||||
|
||||
// Extract host services and proto messages from the proto definition
|
||||
const hostServices = {}
|
||||
for (const [name, def] of Object.entries(proto.host)) {
|
||||
if (def && "service" in def) {
|
||||
hostServices[name] = def
|
||||
} else {
|
||||
addTypeNameToFqn(name, `proto.host.${name}`)
|
||||
}
|
||||
}
|
||||
for (const [name, def] of Object.entries(proto.cline)) {
|
||||
if (def && !("service" in def)) {
|
||||
addTypeNameToFqn(name, `proto.cline.${name}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate interfaces file
|
||||
await generateInterfacesFile(hostServices)
|
||||
|
||||
// // Generate implementation file
|
||||
await generateImplementationFile(hostServices)
|
||||
|
||||
console.log(`Generated host bridge client files at:`)
|
||||
console.log(`- ${INTERFACE_FILE}`)
|
||||
console.log(`- ${IMPL_FILE}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the client interfaces file.
|
||||
*/
|
||||
async function generateInterfacesFile(hostServices) {
|
||||
const clientInterfaces = []
|
||||
for (const [name, def] of Object.entries(hostServices)) {
|
||||
const clientInterface = generateClientInterface(name, def)
|
||||
clientInterfaces.push(clientInterface)
|
||||
}
|
||||
const content = `// GENERATED CODE -- DO NOT EDIT!
|
||||
// Generated by scripts/generate-host-bridge-client.mjs
|
||||
import * as proto from "@shared/proto/index"
|
||||
import { StreamingCallbacks } from "@hosts/host-bridge-client"
|
||||
|
||||
${clientInterfaces.join("\n\n")}
|
||||
`
|
||||
// Write output file
|
||||
await fs.mkdir(path.dirname(INTERFACE_FILE), { recursive: true })
|
||||
await fs.writeFile(INTERFACE_FILE, content)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a client interface for a service.
|
||||
*/
|
||||
function generateClientInterface(serviceName, serviceDefinition) {
|
||||
// Get the methods from the service definition
|
||||
const methods = Object.entries(serviceDefinition.service)
|
||||
.map(([methodName, methodDef]) => {
|
||||
const requestType = getFqn(methodDef.requestType.type.name)
|
||||
const responseType = getFqn(methodDef.responseType.type.name)
|
||||
|
||||
if (!methodDef.responseStream) {
|
||||
// Generate unary method signature.
|
||||
return ` ${methodName}(request: ${requestType}): Promise<${responseType}>;`
|
||||
}
|
||||
// Generate streaming method signature.
|
||||
return ` ${methodName}(request: ${requestType}, callbacks: StreamingCallbacks<${responseType}>): () => void;`
|
||||
})
|
||||
.join("\n\n")
|
||||
|
||||
// Generate the interface
|
||||
return `/**
|
||||
* Interface for ${serviceName} client.
|
||||
*/
|
||||
export interface ${serviceName}ClientInterface {
|
||||
|
||||
${methods}
|
||||
}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the client implementations file.
|
||||
*/
|
||||
async function generateImplementationFile(hostServices) {
|
||||
// Generate imports
|
||||
const imports = []
|
||||
// Add imports for the interfaces
|
||||
for (const [name, _def] of Object.entries(hostServices)) {
|
||||
imports.push(`import { ${name}ClientInterface } from "@generated/hosts/host-bridge-client-types"`)
|
||||
}
|
||||
const clientImplementations = []
|
||||
for (const [name, def] of Object.entries(hostServices)) {
|
||||
clientImplementations.push(generateClientImplementation(name, def))
|
||||
}
|
||||
|
||||
const content = `// GENERATED CODE -- DO NOT EDIT!
|
||||
// Generated by scripts/generate-host-bridge-client.mjs
|
||||
import { asyncIteratorToCallbacks } from "@/standalone/utils"
|
||||
import * as niceGrpc from "@generated/nice-grpc/index"
|
||||
import { StreamingCallbacks } from "@hosts/host-bridge-client"
|
||||
import * as proto from "@shared/proto/index"
|
||||
import { Channel, createClient } from "nice-grpc"
|
||||
|
||||
${imports.join("\n")}
|
||||
|
||||
${clientImplementations.join("\n\n")}
|
||||
`
|
||||
|
||||
// Write output file
|
||||
await fs.mkdir(path.dirname(IMPL_FILE), { recursive: true })
|
||||
await fs.writeFile(IMPL_FILE, content)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a client implementation class for a service
|
||||
*/
|
||||
function generateClientImplementation(serviceName, serviceDefinition) {
|
||||
// Get the methods from the service definition
|
||||
const methods = Object.entries(serviceDefinition.service)
|
||||
.map(([methodName, methodDef]) => {
|
||||
// Get fully qualified type names
|
||||
const requestType = getFqn(methodDef.requestType.type.name)
|
||||
const responseType = getFqn(methodDef.responseType.type.name)
|
||||
const isStreamingResponse = methodDef.responseStream
|
||||
|
||||
if (!isStreamingResponse) {
|
||||
return ` ${methodName}(request: ${requestType}): Promise<${responseType}> {
|
||||
return this.client.${methodName}(request)
|
||||
}`
|
||||
} else {
|
||||
// Generate streaming method
|
||||
return ` ${methodName}(request: ${requestType}, callbacks: StreamingCallbacks<${responseType}>): () => void {
|
||||
const abortController = new AbortController()
|
||||
const stream: AsyncIterable<${responseType}> = this.client.${methodName}(request, {signal: abortController.signal})
|
||||
asyncIteratorToCallbacks(stream, callbacks)
|
||||
return () => {abortController.abort()}
|
||||
}`
|
||||
}
|
||||
})
|
||||
.join("\n\n")
|
||||
|
||||
// Generate the class
|
||||
return `/**
|
||||
* Type-safe client implementation for ${serviceName}.
|
||||
*/
|
||||
export class ${serviceName}ClientImpl implements ${serviceName}ClientInterface {
|
||||
private client: niceGrpc.host.${serviceName}Client
|
||||
|
||||
constructor(channel: Channel) {
|
||||
this.client = createClient(niceGrpc.host.${serviceName}Definition, channel)
|
||||
}
|
||||
|
||||
${methods}
|
||||
}`
|
||||
}
|
||||
|
||||
// Run the main function
|
||||
main().catch((error) => {
|
||||
console.error(chalk.red("Error:"), error)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -12,7 +12,6 @@ git grep -h 'vscode\.' $DIR |
|
||||
grep -Ev '//.*vscode' | # remove commented out code
|
||||
sed 's|.*vscode\.|vscode.|'| # remove everything before vscode.
|
||||
sed 's/[^a-zA-Z0-9_.].*$//' | # remove everything after last identifier
|
||||
grep -E '\.[a-z][^.]+$' | # remove types (last part of identifier should be lowercase)
|
||||
sort | uniq > $SDK_DEST
|
||||
}
|
||||
echo Wrote uses of the vscode SDK to $(realpath $SDK_DEST)
|
||||
|
||||
@@ -184,35 +184,34 @@ describe("AwsBedrockHandler", () => {
|
||||
})
|
||||
})
|
||||
|
||||
const mockOptions: ApiHandlerOptions = {
|
||||
apiModelId: "anthropic.claude-3-7-sonnet-20250219-v1:0",
|
||||
awsRegion: "us-east-1",
|
||||
awsAccessKey: "test-key",
|
||||
awsSecretKey: "test-secret",
|
||||
awsSessionToken: "",
|
||||
awsUseProfile: false,
|
||||
awsProfile: "",
|
||||
awsBedrockUsePromptCache: false,
|
||||
awsUseCrossRegionInference: false,
|
||||
awsBedrockEndpoint: "",
|
||||
awsBedrockCustomSelected: false,
|
||||
awsBedrockCustomModelBaseId: undefined,
|
||||
thinkingBudgetTokens: 1600,
|
||||
}
|
||||
|
||||
const mockModelInfo = {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 200000,
|
||||
supportsPromptCache: true,
|
||||
supportsImages: true,
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
cacheWritesPrice: 3.75,
|
||||
cacheReadsPrice: 0.3,
|
||||
}
|
||||
|
||||
describe("executeConverseStream", () => {
|
||||
let handler: AwsBedrockHandler
|
||||
const mockOptions: ApiHandlerOptions = {
|
||||
apiModelId: "anthropic.claude-3-7-sonnet-20250219-v1:0",
|
||||
awsRegion: "us-east-1",
|
||||
awsAccessKey: "test-key",
|
||||
awsSecretKey: "test-secret",
|
||||
awsSessionToken: "",
|
||||
awsUseProfile: false,
|
||||
awsProfile: "",
|
||||
awsBedrockUsePromptCache: false,
|
||||
awsUseCrossRegionInference: false,
|
||||
awsBedrockEndpoint: "",
|
||||
awsBedrockCustomSelected: false,
|
||||
awsBedrockCustomModelBaseId: undefined,
|
||||
thinkingBudgetTokens: 1600,
|
||||
}
|
||||
|
||||
const mockModelInfo = {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 200000,
|
||||
supportsPromptCache: true,
|
||||
supportsImages: true,
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
cacheWritesPrice: 3.75,
|
||||
cacheReadsPrice: 0.3,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
handler = new AwsBedrockHandler(mockOptions)
|
||||
@@ -592,102 +591,4 @@ describe("AwsBedrockHandler", () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("getModelId", () => {
|
||||
it("should return raw model ID for custom models", async () => {
|
||||
const customOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
awsBedrockCustomSelected: true,
|
||||
apiModelId:
|
||||
"arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
|
||||
}
|
||||
const customHandler = new AwsBedrockHandler(customOptions)
|
||||
|
||||
const modelId = await customHandler.getModelId()
|
||||
modelId.should.equal(
|
||||
"arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
|
||||
)
|
||||
})
|
||||
|
||||
it("should not encode custom model IDs with slashes", async () => {
|
||||
const customOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
awsBedrockCustomSelected: true,
|
||||
apiModelId: "my-namespace/my-custom-model",
|
||||
}
|
||||
const customHandler = new AwsBedrockHandler(customOptions)
|
||||
|
||||
const modelId = await customHandler.getModelId()
|
||||
modelId.should.equal("my-namespace/my-custom-model")
|
||||
modelId.should.not.match(/%2F/)
|
||||
})
|
||||
|
||||
it("should apply cross-region prefix for non-custom models when enabled", async () => {
|
||||
const crossRegionOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
awsUseCrossRegionInference: true,
|
||||
awsRegion: "us-west-2",
|
||||
}
|
||||
const crossRegionHandler = new AwsBedrockHandler(crossRegionOptions)
|
||||
|
||||
const modelId = await crossRegionHandler.getModelId()
|
||||
modelId.should.equal("us.anthropic.claude-3-7-sonnet-20250219-v1:0")
|
||||
})
|
||||
|
||||
it("should apply EU cross-region prefix", async () => {
|
||||
const euOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
awsUseCrossRegionInference: true,
|
||||
awsRegion: "eu-central-1",
|
||||
}
|
||||
const euHandler = new AwsBedrockHandler(euOptions)
|
||||
|
||||
const modelId = await euHandler.getModelId()
|
||||
modelId.should.equal("eu.anthropic.claude-3-7-sonnet-20250219-v1:0")
|
||||
})
|
||||
|
||||
it("should apply APAC cross-region prefix", async () => {
|
||||
const apacOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
awsUseCrossRegionInference: true,
|
||||
awsRegion: "ap-northeast-1",
|
||||
}
|
||||
const apacHandler = new AwsBedrockHandler(apacOptions)
|
||||
|
||||
const modelId = await apacHandler.getModelId()
|
||||
modelId.should.equal("apac.anthropic.claude-3-7-sonnet-20250219-v1:0")
|
||||
})
|
||||
|
||||
it("should not apply cross-region prefix for custom models even when enabled", async () => {
|
||||
const customCrossRegionOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
awsBedrockCustomSelected: true,
|
||||
apiModelId: "arn:aws:bedrock:us-west-2:123456789012:custom-model/my-model",
|
||||
awsUseCrossRegionInference: true,
|
||||
}
|
||||
const customCrossRegionHandler = new AwsBedrockHandler(customCrossRegionOptions)
|
||||
|
||||
const modelId = await customCrossRegionHandler.getModelId()
|
||||
modelId.should.equal("arn:aws:bedrock:us-west-2:123456789012:custom-model/my-model")
|
||||
})
|
||||
|
||||
it("should handle UltraThink model ARN correctly", async () => {
|
||||
const ultraThinkOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
awsBedrockCustomSelected: true,
|
||||
apiModelId:
|
||||
"arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
|
||||
awsBedrockCustomModelBaseId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
|
||||
}
|
||||
const ultraThinkHandler = new AwsBedrockHandler(ultraThinkOptions)
|
||||
|
||||
const modelId = await ultraThinkHandler.getModelId()
|
||||
// Should return the raw ARN without any encoding
|
||||
modelId.should.equal(
|
||||
"arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
|
||||
)
|
||||
modelId.should.not.match(/%2F/)
|
||||
modelId.should.not.match(/%3A/)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -133,20 +133,11 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
|
||||
const customSelected = this.options.awsBedrockCustomSelected
|
||||
const baseModel = this.options.awsBedrockCustomModelBaseId
|
||||
|
||||
// Handle custom models
|
||||
if (customSelected && modelId) {
|
||||
// If base model is provided and valid, use its capabilities
|
||||
if (baseModel && baseModel in bedrockModels) {
|
||||
return {
|
||||
id: modelId,
|
||||
info: bedrockModels[baseModel],
|
||||
}
|
||||
}
|
||||
// For custom models without valid base model in bedrock model list, use default model's capabilities
|
||||
if (customSelected && modelId && baseModel && baseModel in bedrockModels) {
|
||||
// Use the user-input model ID but inherit capabilities from the base model
|
||||
return {
|
||||
id: modelId,
|
||||
info: bedrockModels[bedrockDefaultModelId],
|
||||
info: bedrockModels[baseModel],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,9 +214,12 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
|
||||
/**
|
||||
* Gets the appropriate model ID, accounting for cross-region inference if enabled.
|
||||
* For custom models, returns the raw model ID without any encoding.
|
||||
* If the model ID is an ARN that contains a slash, you will get the URL encoded ARN.
|
||||
*/
|
||||
async getModelId(): Promise<string> {
|
||||
if (this.options.awsBedrockCustomSelected && this.getModel().id.includes("/")) {
|
||||
return encodeURIComponent(this.getModel().id)
|
||||
}
|
||||
if (!this.options.awsBedrockCustomSelected && this.options.awsUseCrossRegionInference) {
|
||||
const regionPrefix = this.getRegion().slice(0, 3)
|
||||
switch (regionPrefix) {
|
||||
|
||||
@@ -185,7 +185,7 @@ export class GeminiHandler implements ApiHandler {
|
||||
})
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: promptTokens - cacheReadTokens,
|
||||
inputTokens: promptTokens,
|
||||
outputTokens,
|
||||
thoughtsTokenCount,
|
||||
cacheReadTokens,
|
||||
|
||||
@@ -9,7 +9,7 @@ export async function checkpointRestore(controller: Controller, request: Checkpo
|
||||
|
||||
if (request.number) {
|
||||
// wait for messages to be loaded
|
||||
await pWaitFor(() => controller.task?.taskState.isInitialized === true, {
|
||||
await pWaitFor(() => controller.task?.isInitialized === true, {
|
||||
timeout: 3_000,
|
||||
}).catch(() => {
|
||||
console.error("Failed to init new cline instance")
|
||||
|
||||
@@ -421,8 +421,8 @@ export class Controller {
|
||||
await updateWorkspaceState(this.context, "lmStudioModelId", newModelId)
|
||||
break
|
||||
case "litellm":
|
||||
await updateWorkspaceState(this.context, "liteLlmModelId", newModelId)
|
||||
await updateWorkspaceState(this.context, "liteLlmModelInfo", newModelInfo)
|
||||
await updateWorkspaceState(this.context, "previousModeModelId", apiConfiguration.liteLlmModelId)
|
||||
await updateWorkspaceState(this.context, "previousModeModelInfo", apiConfiguration.liteLlmModelInfo)
|
||||
break
|
||||
case "requesty":
|
||||
await updateWorkspaceState(this.context, "requestyModelId", newModelId)
|
||||
@@ -445,8 +445,8 @@ export class Controller {
|
||||
|
||||
if (this.task) {
|
||||
this.task.chatSettings = chatSettings
|
||||
if (this.task.taskState.isAwaitingPlanResponse && didSwitchToActMode) {
|
||||
this.task.taskState.didRespondToPlanAskBySwitchingMode = true
|
||||
if (this.task.isAwaitingPlanResponse && didSwitchToActMode) {
|
||||
this.task.didRespondToPlanAskBySwitchingMode = true
|
||||
// Use chatContent if provided, otherwise use default message
|
||||
await this.task.handleWebviewAskResponse(
|
||||
"messageResponse",
|
||||
@@ -471,9 +471,9 @@ export class Controller {
|
||||
await pWaitFor(
|
||||
() =>
|
||||
this.task === undefined ||
|
||||
this.task.taskState.isStreaming === false ||
|
||||
this.task.taskState.didFinishAbortingStream ||
|
||||
this.task.taskState.isWaitingForFirstChunk, // if only first chunk is processed, then there's no need to wait for graceful abort (closes edits, browser, etc)
|
||||
this.task.isStreaming === false ||
|
||||
this.task.didFinishAbortingStream ||
|
||||
this.task.isWaitingForFirstChunk, // if only first chunk is processed, then there's no need to wait for graceful abort (closes edits, browser, etc)
|
||||
{
|
||||
timeout: 3_000,
|
||||
},
|
||||
@@ -482,7 +482,7 @@ export class Controller {
|
||||
})
|
||||
if (this.task) {
|
||||
// 'abandoned' will prevent this cline instance from affecting future cline instance gui. this may happen if its hanging on a streaming request
|
||||
this.task.taskState.abandoned = true
|
||||
this.task.abandoned = true
|
||||
}
|
||||
await this.initTask(undefined, undefined, undefined, historyItem) // clears task again, so we need to abortTask manually above
|
||||
// await this.postStateToWebview() // new Cline instance will post state when it's ready. having this here sent an empty messages array to webview leading to virtuoso having to reload the entire list
|
||||
@@ -1006,7 +1006,7 @@ export class Controller {
|
||||
apiConfiguration,
|
||||
uriScheme: vscode.env.uriScheme,
|
||||
currentTaskItem: this.task?.taskId ? (taskHistory || []).find((item) => item.id === this.task?.taskId) : undefined,
|
||||
checkpointTrackerErrorMessage: this.task?.taskState.checkpointTrackerErrorMessage,
|
||||
checkpointTrackerErrorMessage: this.task?.checkpointTrackerErrorMessage,
|
||||
clineMessages: this.task?.messageStateHandler.getClineMessages() || [],
|
||||
taskHistory: (taskHistory || [])
|
||||
.filter((item) => item.ts && item.task)
|
||||
|
||||
@@ -41,6 +41,11 @@ export async function refreshRequestyModels(controller: Controller, _: EmptyRequ
|
||||
models[model.id] = modelInfo
|
||||
}
|
||||
console.log("Requesty models fetched", models)
|
||||
|
||||
controller.postMessageToWebview({
|
||||
type: "requestyModels",
|
||||
requestyModels: models,
|
||||
})
|
||||
} else {
|
||||
console.error("Invalid response from Requesty API")
|
||||
}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import { Controller } from "../index"
|
||||
import { Empty } from "@shared/proto/common"
|
||||
import { EmptyRequest } from "@shared/proto/common"
|
||||
import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler"
|
||||
|
||||
// Keep track of active didBecomeVisible subscriptions by controller ID
|
||||
const activeDidBecomeVisibleSubscriptions = new Map<string, StreamingResponseHandler>()
|
||||
|
||||
/**
|
||||
* Subscribe to didBecomeVisible events
|
||||
* @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 subscribeToDidBecomeVisible(
|
||||
controller: Controller,
|
||||
request: EmptyRequest,
|
||||
responseStream: StreamingResponseHandler,
|
||||
requestId?: string,
|
||||
): Promise<void> {
|
||||
const controllerId = controller.id
|
||||
console.log(`[DEBUG] set up didBecomeVisible subscription for controller ${controllerId}`)
|
||||
|
||||
// Add this subscription to the active subscriptions with the controller ID
|
||||
activeDidBecomeVisibleSubscriptions.set(controllerId, responseStream)
|
||||
|
||||
// Register cleanup when the connection is closed
|
||||
const cleanup = () => {
|
||||
activeDidBecomeVisibleSubscriptions.delete(controllerId)
|
||||
}
|
||||
|
||||
// Register the cleanup function with the request registry if we have a requestId
|
||||
if (requestId) {
|
||||
getRequestRegistry().registerRequest(requestId, cleanup, { type: "didBecomeVisible_subscription" }, responseStream)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a didBecomeVisible event to a specific controller's subscription
|
||||
* @param controllerId The ID of the controller to send the event to
|
||||
*/
|
||||
export async function sendDidBecomeVisibleEvent(controllerId: string): Promise<void> {
|
||||
// Get the subscription for this specific controller
|
||||
const responseStream = activeDidBecomeVisibleSubscriptions.get(controllerId)
|
||||
|
||||
if (!responseStream) {
|
||||
console.log(`[DEBUG] No active subscription for controller ${controllerId}`)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const event: Empty = Empty.create({})
|
||||
await responseStream(
|
||||
event,
|
||||
false, // Not the last message
|
||||
)
|
||||
} catch (error) {
|
||||
console.error(`Error sending didBecomeVisible event to controller ${controllerId}:`, error)
|
||||
// Remove the subscription if there was an error
|
||||
activeDidBecomeVisibleSubscriptions.delete(controllerId)
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { AssistantMessageContent } from "@core/assistant-message"
|
||||
import { StreamingJsonReplacer } from "@core/assistant-message/diff-json"
|
||||
import { ClineAskResponse } from "@shared/WebviewMessage"
|
||||
|
||||
export class TaskState {
|
||||
// Streaming flags
|
||||
isStreaming = false
|
||||
isWaitingForFirstChunk = false
|
||||
didCompleteReadingStream = false
|
||||
|
||||
// Content processing
|
||||
currentStreamingContentIndex = 0
|
||||
assistantMessageContent: AssistantMessageContent[] = []
|
||||
userMessageContent: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[] = []
|
||||
userMessageContentReady = false
|
||||
|
||||
// Presentation locks
|
||||
presentAssistantMessageLocked = false
|
||||
presentAssistantMessageHasPendingUpdates = false
|
||||
|
||||
// Claude 4 experimental JSON streaming
|
||||
streamingJsonReplacer?: StreamingJsonReplacer
|
||||
lastProcessedJsonLength: number = 0
|
||||
|
||||
// Ask/Response handling
|
||||
askResponse?: ClineAskResponse
|
||||
askResponseText?: string
|
||||
askResponseImages?: string[]
|
||||
askResponseFiles?: string[]
|
||||
lastMessageTs?: number
|
||||
|
||||
// Plan mode specific state
|
||||
isAwaitingPlanResponse = false
|
||||
didRespondToPlanAskBySwitchingMode = false
|
||||
|
||||
// Tool execution flags
|
||||
didRejectTool = false
|
||||
didAlreadyUseTool = false
|
||||
didEditFile: boolean = false
|
||||
|
||||
// Consecutive request tracking
|
||||
consecutiveAutoApprovedRequestsCount: number = 0
|
||||
|
||||
// Error tracking
|
||||
consecutiveMistakeCount: number = 0
|
||||
didAutomaticallyRetryFailedApiRequest = false
|
||||
checkpointTrackerErrorMessage?: string
|
||||
|
||||
// Task Initialization
|
||||
isInitialized = false
|
||||
|
||||
// Task Abort / Cancellation
|
||||
abort: boolean = false
|
||||
didFinishAbortingStream = false
|
||||
abandoned = false
|
||||
}
|
||||
+327
-327
File diff suppressed because it is too large
Load Diff
@@ -1,70 +0,0 @@
|
||||
import { showSystemNotification } from "@/integrations/notifications"
|
||||
import { ClineApiReqCancelReason, ClineApiReqInfo } from "@/shared/ExtensionMessage"
|
||||
import { serializeError } from "serialize-error"
|
||||
import { MessageStateHandler } from "./message-state"
|
||||
import { calculateApiCostAnthropic } from "@/utils/cost"
|
||||
import { ApiHandler } from "@/api"
|
||||
|
||||
export function formatErrorWithStatusCode(error: any): string {
|
||||
const statusCode = error.status || error.statusCode || (error.response && error.response.status)
|
||||
const message = error.message ?? JSON.stringify(serializeError(error), null, 2)
|
||||
|
||||
// Only prepend the statusCode if it's not already part of the message
|
||||
return statusCode && !message.includes(statusCode.toString()) ? `${statusCode} - ${message}` : message
|
||||
}
|
||||
|
||||
export const showNotificationForApprovalIfAutoApprovalEnabled = (
|
||||
message: string,
|
||||
autoApprovalSettingsEnabled: boolean,
|
||||
notificationsEnabled: boolean,
|
||||
) => {
|
||||
if (autoApprovalSettingsEnabled && notificationsEnabled) {
|
||||
showSystemNotification({
|
||||
subtitle: "Approval Required",
|
||||
message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type UpdateApiReqMsgParams = {
|
||||
messageStateHandler: MessageStateHandler
|
||||
lastApiReqIndex: number
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
cacheWriteTokens: number
|
||||
cacheReadTokens: number
|
||||
totalCost?: number
|
||||
api: ApiHandler
|
||||
cancelReason?: ClineApiReqCancelReason
|
||||
streamingFailedMessage?: string
|
||||
}
|
||||
|
||||
// update api_req_started. we can't use api_req_finished anymore since it's a unique case where it could come after a streaming message (ie in the middle of being updated or executed)
|
||||
// fortunately api_req_finished was always parsed out for the gui anyways, so it remains solely for legacy purposes to keep track of prices in tasks from history
|
||||
// (it's worth removing a few months from now)
|
||||
export const updateApiReqMsg = async (params: UpdateApiReqMsgParams) => {
|
||||
const clineMessages = params.messageStateHandler.getClineMessages()
|
||||
const currentApiReqInfo: ClineApiReqInfo = JSON.parse(clineMessages[params.lastApiReqIndex].text || "{}")
|
||||
delete currentApiReqInfo.retryStatus // Clear retry status when request is finalized
|
||||
|
||||
await params.messageStateHandler.updateClineMessage(params.lastApiReqIndex, {
|
||||
text: JSON.stringify({
|
||||
...currentApiReqInfo, // Spread the modified info (with retryStatus removed)
|
||||
tokensIn: params.inputTokens,
|
||||
tokensOut: params.outputTokens,
|
||||
cacheWrites: params.cacheWriteTokens,
|
||||
cacheReads: params.cacheReadTokens,
|
||||
cost:
|
||||
params.totalCost ??
|
||||
calculateApiCostAnthropic(
|
||||
params.api.getModel().info,
|
||||
params.inputTokens,
|
||||
params.outputTokens,
|
||||
params.cacheWriteTokens,
|
||||
params.cacheReadTokens,
|
||||
),
|
||||
cancelReason: params.cancelReason,
|
||||
streamingFailedMessage: params.streamingFailedMessage,
|
||||
} satisfies ClineApiReqInfo),
|
||||
})
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import path from "node:path"
|
||||
import { WebviewProviderType } from "@/shared/webview/types"
|
||||
import { sendThemeEvent } from "@core/controller/ui/subscribeToTheme"
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
import { sendDidBecomeVisibleEvent } from "../controller/ui/subscribeToDidBecomeVisible"
|
||||
|
||||
/*
|
||||
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
||||
@@ -114,9 +113,12 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
|
||||
// WebviewView and WebviewPanel have all the same properties except for this visibility listener
|
||||
// panel
|
||||
webviewView.onDidChangeViewState(
|
||||
async () => {
|
||||
() => {
|
||||
if (this.view?.visible) {
|
||||
await sendDidBecomeVisibleEvent(this.controller.id)
|
||||
this.controller.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "didBecomeVisible",
|
||||
})
|
||||
}
|
||||
},
|
||||
null,
|
||||
@@ -125,9 +127,12 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
|
||||
} else if ("onDidChangeVisibility" in webviewView) {
|
||||
// sidebar
|
||||
webviewView.onDidChangeVisibility(
|
||||
async () => {
|
||||
() => {
|
||||
if (this.view?.visible) {
|
||||
await sendDidBecomeVisibleEvent(this.controller.id)
|
||||
this.controller.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "didBecomeVisible",
|
||||
})
|
||||
}
|
||||
},
|
||||
null,
|
||||
|
||||
+1
-7
@@ -26,9 +26,6 @@ import { migratePlanActGlobalToWorkspaceStorage, migrateCustomInstructionsToGlob
|
||||
|
||||
import { sendFocusChatInputEvent } from "./core/controller/ui/subscribeToFocusChatInput"
|
||||
import { FileContextTracker } from "./core/context/context-tracking/FileContextTracker"
|
||||
import { maybeInitializeHostBridgeClient } from "./hosts/host-bridge-client"
|
||||
import { vscodeHostBridgeClient } from "@generated/hosts/vscode/client/host-grpc-client"
|
||||
|
||||
/*
|
||||
Built using https://github.com/microsoft/vscode-webview-ui-toolkit
|
||||
|
||||
@@ -50,8 +47,6 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
Logger.initialize(outputChannel)
|
||||
Logger.log("Cline extension activated")
|
||||
|
||||
maybeInitializeHostBridgeClient(vscodeHostBridgeClient)
|
||||
|
||||
// Migrate global storage values to workspace storage (one-time cleanup)
|
||||
await migratePlanActGlobalToWorkspaceStorage(context)
|
||||
|
||||
@@ -644,8 +639,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
//
|
||||
// This is a workaround to reload the extension when the source code changes
|
||||
// since vscode doesn't support hot reload for extensions
|
||||
const IS_DEV = process.env.IS_DEV
|
||||
const DEV_WORKSPACE_FOLDER = process.env.DEV_WORKSPACE_FOLDER
|
||||
const { IS_DEV, DEV_WORKSPACE_FOLDER } = process.env
|
||||
|
||||
// This method is called when your extension is deactivated
|
||||
export async function deactivate() {
|
||||
|
||||
@@ -1,38 +1,7 @@
|
||||
import { UriServiceClientInterface, WatchServiceClientInterface } from "@generated/hosts/host-bridge-client-types"
|
||||
import * as VscodeClient from "./vscode/client/host-grpc-client"
|
||||
import * as ExternalClient from "@/standalone/host-bridge-client-manager"
|
||||
const isHostBridgeExternal = process.env.HOST_BRIDGE_ADDRESS !== undefined && process.env.HOST_BRIDGE_ADDRESS !== "vscode"
|
||||
const Client = isHostBridgeExternal ? ExternalClient : VscodeClient
|
||||
|
||||
/**
|
||||
* Callback interface for streaming requests
|
||||
*/
|
||||
export interface StreamingCallbacks<T = any> {
|
||||
onResponse: (response: T) => void
|
||||
onError?: (error: Error) => void
|
||||
onComplete?: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for host bridge client providers
|
||||
*/
|
||||
export interface HostBridgeClientProvider {
|
||||
UriServiceClient: UriServiceClientInterface
|
||||
WatchServiceClient: WatchServiceClientInterface
|
||||
}
|
||||
|
||||
let isSetup = false
|
||||
|
||||
// Export the clients directly - they'll be set during initialization
|
||||
export let UriServiceClient: UriServiceClientInterface
|
||||
export let WatchServiceClient: WatchServiceClientInterface
|
||||
|
||||
export function initializeHostBridgeClient(provider: HostBridgeClientProvider): void {
|
||||
UriServiceClient = provider.UriServiceClient
|
||||
WatchServiceClient = provider.WatchServiceClient
|
||||
isSetup = true
|
||||
}
|
||||
|
||||
export function maybeInitializeHostBridgeClient(provider: HostBridgeClientProvider): void {
|
||||
if (isSetup) {
|
||||
console.log("Host bridge client already initialized, not re-initializing.")
|
||||
return
|
||||
}
|
||||
initializeHostBridgeClient(provider)
|
||||
}
|
||||
export const UriServiceClient = Client.UriServiceClient
|
||||
export const WatchServiceClient = Client.WatchServiceClient
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
import { GrpcHandler } from "../host-grpc-handler"
|
||||
import { StreamingCallbacks } from "@hosts/host-bridge-client"
|
||||
import { GrpcHandler, StreamingCallbacks } from "../host-grpc-handler"
|
||||
|
||||
// Generic type for any protobuf service definition
|
||||
export type ProtoService = {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { StreamingCallbacks } from "@hosts/host-bridge-client"
|
||||
import { HostServiceHandlerConfig, hostServiceHandlers } from "./host-grpc-service-config"
|
||||
import { GrpcRequestRegistry } from "@core/controller/grpc-request-registry"
|
||||
|
||||
@@ -10,6 +9,15 @@ export type StreamingResponseHandler = (response: any, isLast?: boolean, sequenc
|
||||
// Registry to track active gRPC requests and their cleanup functions
|
||||
const requestRegistry = new GrpcRequestRegistry()
|
||||
|
||||
/**
|
||||
* Callback interface for streaming requests
|
||||
*/
|
||||
export interface StreamingCallbacks<T = any> {
|
||||
onResponse: (response: T) => void
|
||||
onError?: (error: Error) => void
|
||||
onComplete?: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles gRPC requests for the host bridge.
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
|
||||
import { StdioClientTransport, getDefaultEnvironment } from "@modelcontextprotocol/sdk/client/stdio.js"
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
|
||||
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
|
||||
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
|
||||
import ReconnectingEventSource from "reconnecting-eventsource"
|
||||
@@ -153,6 +153,7 @@ export class McpHub {
|
||||
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) {
|
||||
@@ -215,8 +216,8 @@ export class McpHub {
|
||||
cwd: config.cwd,
|
||||
env: {
|
||||
// ...(config.env ? await injectEnv(config.env) : {}), // Commented out as injectEnv is not found
|
||||
...getDefaultEnvironment(),
|
||||
...(config.env || {}), // Use config.env directly or an empty object
|
||||
...(process.env.PATH ? { PATH: process.env.PATH } : {}),
|
||||
},
|
||||
stderr: "pipe",
|
||||
})
|
||||
|
||||
@@ -17,13 +17,15 @@ export interface ExtensionMessage {
|
||||
| "action"
|
||||
| "state"
|
||||
| "selectedImages"
|
||||
| "openAiModels"
|
||||
| "requestyModels"
|
||||
| "mcpDownloadDetails"
|
||||
| "userCreditsBalance"
|
||||
| "userCreditsUsage"
|
||||
| "userCreditsPayments"
|
||||
| "grpc_response" // New type for gRPC responses
|
||||
text?: string
|
||||
action?: "accountLogoutClicked"
|
||||
action?: "didBecomeVisible" | "accountLogoutClicked"
|
||||
state?: ExtensionState
|
||||
images?: string[]
|
||||
files?: string[]
|
||||
@@ -31,6 +33,7 @@ export interface ExtensionMessage {
|
||||
lmStudioModels?: string[]
|
||||
vsCodeLmModels?: { vendor?: string; family?: string; version?: string; id?: string }[]
|
||||
openAiModels?: string[]
|
||||
requestyModels?: Record<string, ModelInfo>
|
||||
mcpServers?: McpServer[]
|
||||
customToken?: string
|
||||
mcpMarketplaceCatalog?: McpMarketplaceCatalog
|
||||
|
||||
@@ -1,26 +1,51 @@
|
||||
import { Channel, createChannel } from "nice-grpc"
|
||||
import { UriServiceClientImpl, WatchServiceClientImpl } from "@generated/standalone/host-bridge-clients"
|
||||
import { UriServiceClientInterface, WatchServiceClientInterface } from "@generated/hosts/host-bridge-client-types"
|
||||
import { HostBridgeClientProvider } from "@/hosts/host-bridge-client"
|
||||
import { Channel, createChannel, createClient } from "nice-grpc"
|
||||
import * as host from "@generated/nice-grpc/index.host"
|
||||
|
||||
/**
|
||||
* Singleton class to hold the gRPC clients for the host bridge. The clients should be re-used to avoid
|
||||
* creating a new TCP connection every time a rpc is made.
|
||||
*/
|
||||
export class ExternalHostBridgeClientManager implements HostBridgeClientProvider {
|
||||
class HostBridgeClientManager {
|
||||
private static instance: HostBridgeClientManager | null
|
||||
private channel: Channel
|
||||
UriServiceClient: UriServiceClientInterface
|
||||
WatchServiceClient: WatchServiceClientInterface
|
||||
uriClient: host.UriServiceClient
|
||||
//watchClient: host.WatchServiceClient
|
||||
|
||||
constructor() {
|
||||
private constructor() {
|
||||
const address = process.env.HOST_BRIDGE_ADDRESS || "localhost:50052"
|
||||
this.channel = createChannel(address)
|
||||
this.uriClient = createClient(host.UriServiceDefinition, this.channel)
|
||||
//this.watchClient =
|
||||
}
|
||||
|
||||
this.UriServiceClient = new UriServiceClientImpl(this.channel)
|
||||
this.WatchServiceClient = new WatchServiceClientImpl(this.channel)
|
||||
public static getInstance(): HostBridgeClientManager {
|
||||
if (!HostBridgeClientManager.instance) {
|
||||
HostBridgeClientManager.instance = new HostBridgeClientManager()
|
||||
}
|
||||
return HostBridgeClientManager.instance
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
this.channel.close()
|
||||
HostBridgeClientManager.instance = null
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(sjf) Replace this with nice-grpc client.
|
||||
const StubWatchServiceClient = {
|
||||
subscribeToFile: function (
|
||||
_r: host.SubscribeToFileRequest,
|
||||
_h: {
|
||||
onResponse?: (response: { type: host.FileChangeEvent_ChangeType }) => void | Promise<void>
|
||||
onError?: (error: any) => void
|
||||
onComplete?: () => void
|
||||
},
|
||||
) {
|
||||
throw Error("Unimplemented")
|
||||
},
|
||||
}
|
||||
|
||||
const clientManager = HostBridgeClientManager.getInstance()
|
||||
|
||||
export const UriServiceClient = clientManager.uriClient
|
||||
export const WatchServiceClient = StubWatchServiceClient
|
||||
|
||||
@@ -7,14 +7,14 @@ import { Controller } from "../core/controller"
|
||||
import { extensionContext, outputChannel, postMessage } from "./vscode-context"
|
||||
import { getPackageDefinition, log } from "./utils"
|
||||
import { GrpcHandler, GrpcStreamingResponseHandler } from "./grpc-types"
|
||||
import { addProtobusServices } from "@generated/standalone/server-setup"
|
||||
import { addProtobusServices } from "@/generated/standalone/server-setup"
|
||||
import { StreamingResponseHandler } from "@/core/controller/grpc-handler"
|
||||
import { initializeHostBridgeClient, StreamingCallbacks, UriServiceClient, WatchServiceClient } from "@/hosts/host-bridge-client"
|
||||
import { ExternalHostBridgeClientManager } from "./host-bridge-client-manager"
|
||||
import { UriServiceClient } from "@/hosts/host-bridge-client"
|
||||
import { StringRequest } from "@/shared/proto/common"
|
||||
|
||||
async function main() {
|
||||
log("Starting service...")
|
||||
initializeHostBridgeClient(new ExternalHostBridgeClientManager())
|
||||
|
||||
activate(extensionContext)
|
||||
const controller = new Controller(extensionContext, outputChannel, postMessage)
|
||||
const server = new grpc.Server()
|
||||
|
||||
+2
-26
@@ -1,7 +1,7 @@
|
||||
import * as fs from "fs"
|
||||
import * as grpc from "@grpc/grpc-js"
|
||||
import * as protoLoader from "@grpc/proto-loader"
|
||||
import * as health from "grpc-health-check"
|
||||
import { StreamingCallbacks } from "@hosts/host-bridge-client"
|
||||
|
||||
const log = (...args: unknown[]) => {
|
||||
const timestamp = new Date().toISOString()
|
||||
@@ -16,28 +16,4 @@ function getPackageDefinition() {
|
||||
const packageDefinition = { ...clineDef, ...healthDef }
|
||||
return packageDefinition
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an AsyncIterable to a callback-based API
|
||||
* @param stream The AsyncIterable stream to process
|
||||
* @param callbacks The callbacks to invoke for stream events
|
||||
*/
|
||||
async function asyncIteratorToCallbacks<T>(stream: AsyncIterable<T>, callbacks: StreamingCallbacks<T>): Promise<void> {
|
||||
try {
|
||||
// Process each item in the stream
|
||||
for await (const response of stream) {
|
||||
callbacks.onResponse && callbacks.onResponse(response)
|
||||
}
|
||||
// Stream completed successfully
|
||||
callbacks.onComplete && callbacks.onComplete()
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err : new Error(String(err))
|
||||
if (callbacks.onError) {
|
||||
callbacks.onError(error)
|
||||
} else {
|
||||
log(`Host bridge RPC error: ${error}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { getPackageDefinition, log, asyncIteratorToCallbacks }
|
||||
export { getPackageDefinition, log }
|
||||
|
||||
@@ -27,14 +27,7 @@
|
||||
"prefer-const": "off",
|
||||
"no-extra-semi": "off",
|
||||
"eslint-rules/no-protobuf-object-literals": "error",
|
||||
"eslint-rules/no-grpc-client-object-literals": "error",
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
{
|
||||
"selector": "VariableDeclarator[id.type=\"ObjectPattern\"][init.object.name=\"process\"][init.property.name=\"env\"]",
|
||||
"message": "Use process.env.VARIABLE_NAME directly instead of destructuring"
|
||||
}
|
||||
]
|
||||
"eslint-rules/no-grpc-client-object-literals": "error"
|
||||
},
|
||||
"ignorePatterns": ["build"]
|
||||
}
|
||||
|
||||
@@ -675,6 +675,27 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
|
||||
const shouldDisableFilesAndImages = selectedImages.length + selectedFiles.length >= MAX_IMAGES_AND_FILES_PER_MESSAGE
|
||||
|
||||
const handleMessage = useCallback(
|
||||
(e: MessageEvent) => {
|
||||
const message: ExtensionMessage = e.data
|
||||
switch (message.type) {
|
||||
case "action":
|
||||
switch (message.action!) {
|
||||
case "didBecomeVisible":
|
||||
if (!isHidden && !sendingDisabled && !enableButtons) {
|
||||
textAreaRef.current?.focus()
|
||||
}
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
// textAreaRef.current is not explicitly required here since react guarantees that ref will be stable across re-renders, and we're not using its value but its reference.
|
||||
},
|
||||
[isHidden, sendingDisabled, enableButtons, handleSendMessage, handlePrimaryButtonClick, handleSecondaryButtonClick],
|
||||
)
|
||||
|
||||
useEvent("message", handleMessage)
|
||||
|
||||
// Listen for local focusChatInput event
|
||||
useEffect(() => {
|
||||
const handleFocusChatInput = () => {
|
||||
|
||||
@@ -16,7 +16,7 @@ import DeleteTaskButton from "./buttons/DeleteTaskButton"
|
||||
import CopyTaskButton from "./buttons/CopyTaskButton"
|
||||
import OpenDiskTaskHistoryButton from "./buttons/OpenDiskTaskHistoryButton"
|
||||
|
||||
const IS_DEV = process.env.IS_DEV
|
||||
const { IS_DEV } = process.env
|
||||
|
||||
interface TaskHeaderProps {
|
||||
task: ClineMessage
|
||||
@@ -286,11 +286,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
||||
${totalCost?.toFixed(4)}
|
||||
</div>
|
||||
)}
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
onClick={onClose}
|
||||
style={{ marginLeft: 6, flexShrink: 0 }}
|
||||
aria-label="Close task">
|
||||
<VSCodeButton appearance="icon" onClick={onClose} style={{ marginLeft: 6, flexShrink: 0 }}>
|
||||
<span className="codicon codicon-close"></span>
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,6 @@ const DeleteTaskButton: React.FC<{
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
onClick={() => taskId && TaskServiceClient.deleteTasksWithIds(StringArrayRequest.create({ value: [taskId] }))}
|
||||
aria-label="Delete task"
|
||||
style={{ padding: "0px 0px" }}>
|
||||
<div
|
||||
style={{
|
||||
|
||||
@@ -2,7 +2,6 @@ import { LINKS } from "@/constants"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { McpServiceClient } from "@/services/grpc-client"
|
||||
import { convertProtoMcpServersToMcpServers } from "@shared/proto-conversions/mcp/mcp-server-conversion"
|
||||
import { McpServers } from "@shared/proto/mcp"
|
||||
import { EmptyRequest } from "@shared/proto/common"
|
||||
import { AddRemoteMcpServerRequest } from "@shared/proto/mcp"
|
||||
import { VSCodeButton, VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
@@ -41,7 +40,7 @@ const AddRemoteServerForm = ({ onServerAdded }: { onServerAdded: () => void }) =
|
||||
setShowConnectingMessage(true)
|
||||
|
||||
try {
|
||||
const servers: McpServers = await McpServiceClient.addRemoteMcpServer(
|
||||
const servers = await McpServiceClient.addRemoteMcpServer(
|
||||
AddRemoteMcpServerRequest.create({
|
||||
serverName: serverName.trim(),
|
||||
serverUrl: serverUrl.trim(),
|
||||
@@ -50,7 +49,7 @@ const AddRemoteServerForm = ({ onServerAdded }: { onServerAdded: () => void }) =
|
||||
|
||||
setIsSubmitting(false)
|
||||
|
||||
const mcpServers = convertProtoMcpServersToMcpServers(servers.mcpServers)
|
||||
const mcpServers = convertProtoMcpServersToMcpServers(servers)
|
||||
setMcpServers(mcpServers)
|
||||
|
||||
setServerName("")
|
||||
|
||||
@@ -5,7 +5,7 @@ import React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from
|
||||
import { useRemark } from "react-remark"
|
||||
import { useMount } from "react-use"
|
||||
import styled from "styled-components"
|
||||
import { requestyDefaultModelId, requestyDefaultModelInfo } from "../../../../src/shared/api"
|
||||
import { requestyDefaultModelId } from "../../../../src/shared/api"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { ModelsServiceClient } from "../../services/grpc-client"
|
||||
import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
|
||||
@@ -18,7 +18,7 @@ export interface RequestyModelPickerProps {
|
||||
}
|
||||
|
||||
const RequestyModelPicker: React.FC<RequestyModelPickerProps> = ({ isPopup }) => {
|
||||
const { apiConfiguration, setApiConfiguration, requestyModels, setRequestyModels } = useExtensionState()
|
||||
const { apiConfiguration, setApiConfiguration, requestyModels } = useExtensionState()
|
||||
const [searchTerm, setSearchTerm] = useState(apiConfiguration?.requestyModelId || requestyDefaultModelId)
|
||||
const [isDropdownVisible, setIsDropdownVisible] = useState(false)
|
||||
const [selectedIndex, setSelectedIndex] = useState(-1)
|
||||
@@ -44,16 +44,9 @@ const RequestyModelPicker: React.FC<RequestyModelPickerProps> = ({ isPopup }) =>
|
||||
}, [apiConfiguration])
|
||||
|
||||
useMount(() => {
|
||||
ModelsServiceClient.refreshRequestyModels(EmptyRequest.create({}))
|
||||
.then((response) => {
|
||||
setRequestyModels({
|
||||
[requestyDefaultModelId]: requestyDefaultModelInfo,
|
||||
...response.models,
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to refresh Requesty models:", err)
|
||||
})
|
||||
ModelsServiceClient.refreshRequestyModels(EmptyRequest.create({})).catch((err) => {
|
||||
console.error("Failed to refresh Requesty models:", err)
|
||||
})
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -23,7 +23,7 @@ import SectionHeader from "./SectionHeader"
|
||||
import TerminalSettingsSection from "./TerminalSettingsSection"
|
||||
import { convertApiConfigurationToProtoApiConfiguration } from "@shared/proto-conversions/state/settings-conversion"
|
||||
import { convertChatSettingsToProtoChatSettings } from "@shared/proto-conversions/state/chat-settings-conversion"
|
||||
const IS_DEV = process.env.IS_DEV
|
||||
const { IS_DEV } = process.env
|
||||
|
||||
// Styles for the tab system
|
||||
const settingsTabsContainer = "flex flex-1 overflow-hidden [&.narrow_.tab-label]:hidden"
|
||||
|
||||
@@ -68,7 +68,6 @@ interface ExtensionStateContextType extends ExtensionState {
|
||||
setDefaultTerminalProfile: (value: string) => void
|
||||
setChatSettings: (value: ChatSettings) => void
|
||||
setMcpServers: (value: McpServer[]) => void
|
||||
setRequestyModels: (value: Record<string, ModelInfo>) => void
|
||||
setGlobalClineRulesToggles: (toggles: Record<string, boolean>) => void
|
||||
setLocalClineRulesToggles: (toggles: Record<string, boolean>) => void
|
||||
setLocalCursorRulesToggles: (toggles: Record<string, boolean>) => void
|
||||
@@ -217,6 +216,26 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
})
|
||||
const [mcpServers, setMcpServers] = useState<McpServer[]>([])
|
||||
const [mcpMarketplaceCatalog, setMcpMarketplaceCatalog] = useState<McpMarketplaceCatalog>({ items: [] })
|
||||
const handleMessage = useCallback((event: MessageEvent) => {
|
||||
const message: ExtensionMessage = event.data
|
||||
switch (message.type) {
|
||||
case "openAiModels": {
|
||||
const updatedModels = message.openAiModels ?? []
|
||||
setOpenAiModels(updatedModels)
|
||||
break
|
||||
}
|
||||
case "requestyModels": {
|
||||
const updatedModels = message.requestyModels ?? {}
|
||||
setRequestyModels({
|
||||
[requestyDefaultModelId]: requestyDefaultModelInfo,
|
||||
...updatedModels,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEvent("message", handleMessage)
|
||||
|
||||
// References to store subscription cancellation functions
|
||||
const stateSubscriptionRef = useRef<(() => void) | null>(null)
|
||||
@@ -246,7 +265,6 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
}
|
||||
}, [])
|
||||
const mcpServersSubscriptionRef = useRef<(() => void) | null>(null)
|
||||
const didBecomeVisibleUnsubscribeRef = useRef<(() => void) | null>(null)
|
||||
|
||||
// Subscribe to state updates and UI events using the gRPC streaming API
|
||||
useEffect(() => {
|
||||
@@ -377,18 +395,6 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
onComplete: () => {},
|
||||
})
|
||||
|
||||
// Subscribe to didBecomeVisible events
|
||||
didBecomeVisibleUnsubscribeRef.current = UiServiceClient.subscribeToDidBecomeVisible(EmptyRequest.create({}), {
|
||||
onResponse: () => {
|
||||
console.log("[DEBUG] Received didBecomeVisible event from gRPC stream")
|
||||
window.dispatchEvent(new CustomEvent("focusChatInput"))
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Error in didBecomeVisible subscription:", error)
|
||||
},
|
||||
onComplete: () => {},
|
||||
})
|
||||
|
||||
// Subscribe to MCP servers updates
|
||||
mcpServersSubscriptionRef.current = McpServiceClient.subscribeToMcpServers(EmptyRequest.create(), {
|
||||
onResponse: (response) => {
|
||||
@@ -643,10 +649,6 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
mcpServersSubscriptionRef.current()
|
||||
mcpServersSubscriptionRef.current = null
|
||||
}
|
||||
if (didBecomeVisibleUnsubscribeRef.current) {
|
||||
didBecomeVisibleUnsubscribeRef.current()
|
||||
didBecomeVisibleUnsubscribeRef.current = null
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
@@ -764,7 +766,6 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
defaultTerminalProfile: value,
|
||||
})),
|
||||
setMcpServers: (mcpServers: McpServer[]) => setMcpServers(mcpServers),
|
||||
setRequestyModels: (models: Record<string, ModelInfo>) => setRequestyModels(models),
|
||||
setMcpMarketplaceCatalog: (catalog: McpMarketplaceCatalog) => setMcpMarketplaceCatalog(catalog),
|
||||
setAvailableTerminalProfiles,
|
||||
setShowMcp,
|
||||
|
||||
Reference in New Issue
Block a user