Compare commits

..
43 changed files with 571 additions and 1085 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
fix: respect setting litellm models for plan and act
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Migrate didBecomeVisible to protobus
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Prevent reading IS_DEV from the users environment
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
fix(bedrock): remove custom Model encode
-5
View File
@@ -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
View File
@@ -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"]
}
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
}
-3
View File
@@ -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);
}
+1
View File
@@ -34,6 +34,7 @@ const srcConfig = {
format: "cjs",
platform: "node",
define: {
"process.env.IS_DEV": "true",
"process.env.IS_TEST": "true",
},
external: ["vscode"],
-192
View File
@@ -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)
})
-1
View File
@@ -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)
+26 -125
View File
@@ -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/)
})
})
})
+7 -13
View File
@@ -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) {
+1 -1
View File
@@ -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")
+9 -9
View File
@@ -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)
}
}
-57
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
-70
View File
@@ -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 -5
View File
@@ -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
View File
@@ -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() {
+6 -37
View File
@@ -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 = {
+9 -1
View File
@@ -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.
*/
+3 -2
View File
@@ -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",
})
+4 -1
View File
@@ -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
+35 -10
View File
@@ -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
+4 -4
View File
@@ -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
View File
@@ -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 }
+1 -8
View File
@@ -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,