Compare commits

..
2 Commits
21 changed files with 256 additions and 357 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Fixed race condition where clineAsk was undefined, leading to task restoration and other downstream issues
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Integrate Claude Code
+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",
"protos": "node proto/build-proto.js && node scripts/generate-server-setup.mjs && node scripts/generate-host-bridge-client.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",
+1 -4
View File
@@ -21,10 +21,7 @@ const NICE_JS_OUT_DIR = path.join(ROOT_DIR, "src/generated/nice-grpc")
const DESCRIPTOR_OUT_DIR = path.join(ROOT_DIR, "dist-standalone/proto")
const isWindows = process.platform === "win32"
const TS_PROTO_PLUGIN = isWindows
? path.join(ROOT_DIR, "node_modules", ".bin", "protoc-gen-ts_proto.cmd") // Use the .bin directory path for Windows
: require.resolve("ts-proto/protoc-gen-ts_proto")
const TS_PROTO_PLUGIN = require.resolve("ts-proto/protoc-gen-ts_proto") + (isWindows ? ".cmd" : "")
const TS_PROTO_OPTIONS = [
"env=node",
"esModuleInterop=true",
-2
View File
@@ -122,7 +122,6 @@ enum ApiProvider {
SAMBANOVA = 22;
CEREBRAS = 23;
SAPAICORE = 24;
CLAUDE_CODE = 25;
}
// Model info for OpenAI-compatible models
@@ -235,5 +234,4 @@ message ModelsApiConfiguration {
optional string sap_ai_resource_group = 70;
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
@@ -217,7 +217,4 @@ message ApiConfiguration {
optional string sap_ai_core_base_url = 74;
optional string sap_ai_core_token_url = 75;
optional string sap_ai_resource_group = 76;
// Claude Code specific
optional string claude_code_path = 77;
}
+192
View File
@@ -0,0 +1,192 @@
#!/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/vscode/host-grpc-handler"
${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/vscode/host-grpc-handler"
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)
})
-3
View File
@@ -26,7 +26,6 @@ import { XAIHandler } from "./providers/xai"
import { SambanovaHandler } from "./providers/sambanova"
import { CerebrasHandler } from "./providers/cerebras"
import { SapAiCoreHandler } from "./providers/sapaicore"
import { ClaudeCodeHandler } from "./providers/claude-code"
export interface ApiHandler {
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream
@@ -91,8 +90,6 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
return new CerebrasHandler(options)
case "sapaicore":
return new SapAiCoreHandler(options)
case "claude-code":
return new ClaudeCodeHandler(options)
default:
return new AnthropicHandler(options)
}
-165
View File
@@ -1,165 +0,0 @@
import type { Anthropic } from "@anthropic-ai/sdk"
import { claudeCodeDefaultModelId, ClaudeCodeModelId, claudeCodeModels, type ApiHandlerOptions } from "@/shared/api"
import { type ApiHandler } from ".."
import { ApiStreamUsageChunk, type ApiStream } from "../transform/stream"
import { withRetry } from "../retry"
import { runClaudeCode } from "@/integrations/claude-code/run"
import { ClaudeCodeMessage } from "@/integrations/claude-code/types"
export class ClaudeCodeHandler implements ApiHandler {
private options: ApiHandlerOptions
constructor(options: ApiHandlerOptions) {
this.options = options
}
@withRetry({
maxRetries: 4,
baseDelay: 2000,
maxDelay: 15000,
})
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const claudeProcess = runClaudeCode({
systemPrompt,
messages,
path: this.options.claudeCodePath,
modelId: this.getModel().id,
})
const dataQueue: string[] = []
let processError = null
let errorOutput = ""
let exitCode: number | null = null
claudeProcess.stdout.on("data", (data) => {
const output = data.toString()
const lines = output.split("\n").filter((line: string) => line.trim() !== "")
for (const line of lines) {
dataQueue.push(line)
}
})
claudeProcess.stderr.on("data", (data) => {
errorOutput += data.toString()
})
claudeProcess.on("close", (code) => {
exitCode = code
})
claudeProcess.on("error", (error) => {
processError = error
})
// Usage is included with assistant messages,
// but cost is included in the result chunk
let usage: ApiStreamUsageChunk = {
type: "usage",
inputTokens: 0,
outputTokens: 0,
cacheReadTokens: 0,
cacheWriteTokens: 0,
}
while (exitCode !== 0 || dataQueue.length > 0) {
if (dataQueue.length === 0) {
await new Promise((resolve) => setImmediate(resolve))
}
if (exitCode !== null && exitCode !== 0) {
throw new Error(
`Claude Code process exited with code ${exitCode}.${errorOutput ? ` Error output: ${errorOutput.trim()}` : ""}`,
)
}
const data = dataQueue.shift()
if (!data) {
continue
}
const chunk = this.attemptParseChunk(data)
if (!chunk) {
yield {
type: "text",
text: data || "",
}
continue
}
if (chunk.type === "system" && chunk.subtype === "init") {
continue
}
if (chunk.type === "assistant" && "message" in chunk) {
const message = chunk.message
if (message.stop_reason !== null && message.stop_reason !== "tool_use") {
const errorMessage = message.content[0]?.text || `Claude Code stopped with reason: ${message.stop_reason}`
if (errorMessage.includes("Invalid model name")) {
throw new Error(
errorMessage +
`\n\nAPI keys and subscription plans allow different models. Make sure the selected model is included in your plan.`,
)
}
throw new Error(errorMessage)
}
for (const content of message.content) {
if (content.type === "text") {
yield {
type: "text",
text: content.text,
}
} else {
console.warn("Unsupported content type:", content.type)
}
}
usage.inputTokens += message.usage.input_tokens
usage.outputTokens += message.usage.output_tokens
usage.cacheReadTokens = (usage.cacheReadTokens || 0) + (message.usage.cache_read_input_tokens || 0)
usage.cacheWriteTokens = (usage.cacheWriteTokens || 0) + (message.usage.cache_creation_input_tokens || 0)
continue
}
if (chunk.type === "result" && "result" in chunk) {
usage.totalCost = chunk.cost_usd || 0
yield usage
}
if (processError) {
throw processError
}
}
}
getModel() {
const modelId = this.options.apiModelId
if (modelId && modelId in claudeCodeModels) {
const id = modelId as ClaudeCodeModelId
return { id, info: claudeCodeModels[id] }
}
return {
id: claudeCodeDefaultModelId,
info: claudeCodeModels[claudeCodeDefaultModelId],
}
}
// TOOD: Validate instead of parsing
private attemptParseChunk(data: string): ClaudeCodeMessage | null {
try {
return JSON.parse(data)
} catch (error) {
console.error("Error parsing chunk:", error)
return null
}
}
}
-1
View File
@@ -79,7 +79,6 @@ export type GlobalStateKey =
| "sapAiCoreClientId"
| "sapAiCoreClientSecret"
| "sapAiCoreModelId"
| "claudeCodePath"
export type LocalStateKey =
| "localClineRulesToggles"
-5
View File
@@ -246,7 +246,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
sapAiCoreTokenUrl,
sapAiResourceGroup,
sapAiCoreModelId,
claudeCodePath,
] = await Promise.all([
getGlobalState(context, "isNewUser") as Promise<boolean | undefined>,
getSecret(context, "apiKey") as Promise<string | undefined>,
@@ -319,7 +318,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
getGlobalState(context, "sapAiCoreTokenUrl") as Promise<string | undefined>,
getGlobalState(context, "sapAiResourceGroup") as Promise<string | undefined>,
getGlobalState(context, "sapAiCoreModelId") as Promise<string | undefined>,
getGlobalState(context, "claudeCodePath") as Promise<string | undefined>,
])
const localClineRulesToggles = (await getWorkspaceState(context, "localClineRulesToggles")) as ClineRulesToggles
@@ -439,7 +437,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
apiKey,
openRouterApiKey,
clineApiKey,
claudeCodePath,
awsAccessKey,
awsSecretKey,
awsSessionToken,
@@ -621,7 +618,6 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
sapAiCoreTokenUrl,
sapAiResourceGroup,
sapAiCoreModelId,
claudeCodePath,
} = apiConfiguration
// Workspace state updates
await updateWorkspaceState(context, "apiProvider", apiProvider)
@@ -676,7 +672,6 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
await updateGlobalState(context, "sapAiCoreTokenUrl", sapAiCoreTokenUrl)
await updateGlobalState(context, "sapAiResourceGroup", sapAiResourceGroup)
await updateGlobalState(context, "sapAiCoreModelId", sapAiCoreModelId)
await updateGlobalState(context, "claudeCodePath", claudeCodePath)
// Secret updates
await storeSecret(context, "apiKey", apiKey)
+3 -2
View File
@@ -1,7 +1,8 @@
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
export const UriServiceClient = Client.UriServiceClient
export const WatchServiceClient = Client.WatchServiceClient
export const UriServiceClient: UriServiceClientInterface = Client.UriServiceClient
export const WatchServiceClient: WatchServiceClientInterface = Client.WatchServiceClient
-45
View File
@@ -1,45 +0,0 @@
import * as vscode from "vscode"
import Anthropic from "@anthropic-ai/sdk"
import { execa } from "execa"
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
export function runClaudeCode({
systemPrompt,
messages,
path,
modelId,
}: {
systemPrompt: string
messages: Anthropic.Messages.MessageParam[]
path?: string
modelId?: string
}) {
const claudePath = path || "claude"
// TODO: Is it worh using sessions? Where do we store the session ID?
const args = [
"-p",
JSON.stringify(messages),
"--system-prompt",
systemPrompt,
"--verbose",
"--output-format",
"stream-json",
// Cline will handle recursive calls
"--max-turns",
"1",
]
if (modelId) {
args.push("--model", modelId)
}
return execa(claudePath, args, {
stdin: "ignore",
stdout: "pipe",
stderr: "pipe",
env: process.env,
cwd,
})
}
-52
View File
@@ -1,52 +0,0 @@
type InitMessage = {
type: "system"
subtype: "init"
session_id: string
tools: string[]
mcp_servers: string[]
}
type ClaudeCodeContent = {
type: "text"
text: string
}
type AssistantMessage = {
type: "assistant"
message: {
id: string
type: "message"
role: "assistant"
model: string
content: ClaudeCodeContent[]
stop_reason: null
stop_sequence: null
usage: {
input_tokens: number
cache_creation_input_tokens?: number
cache_read_input_tokens?: number
output_tokens: number
service_tier: "standard"
}
}
session_id: string
}
type ErrorMessage = {
type: "error"
}
type ResultMessage = {
type: "result"
subtype: "success"
cost_usd: number
is_error: boolean
duration_ms: number
duration_api_ms: number
num_turns: number
result: string
total_cost: number
session_id: string
}
export type ClaudeCodeMessage = InitMessage | AssistantMessage | ErrorMessage | ResultMessage
-13
View File
@@ -2,7 +2,6 @@ import type { LanguageModelChatSelector } from "../api/providers/types"
export type ApiProvider =
| "anthropic"
| "claude-code"
| "openrouter"
| "bedrock"
| "vertex"
@@ -55,7 +54,6 @@ export interface ApiHandlerOptions {
awsBedrockEndpoint?: string
awsBedrockCustomSelected?: boolean
awsBedrockCustomModelBaseId?: BedrockModelId
claudeCodePath?: string
vertexProjectId?: string
vertexRegion?: string
openAiBaseUrl?: string
@@ -226,17 +224,6 @@ export const anthropicModels = {
},
} as const satisfies Record<string, ModelInfo> // as const assertion makes the object deeply readonly
// Claude Code
export type ClaudeCodeModelId = keyof typeof claudeCodeModels
export const claudeCodeDefaultModelId: ClaudeCodeModelId = "claude-sonnet-4-20250514"
export const claudeCodeModels = {
"claude-sonnet-4-20250514": anthropicModels["claude-sonnet-4-20250514"],
"claude-opus-4-20250514": anthropicModels["claude-opus-4-20250514"],
"claude-3-7-sonnet-20250219": anthropicModels["claude-3-7-sonnet-20250219"],
"claude-3-5-sonnet-20241022": anthropicModels["claude-3-5-sonnet-20241022"],
"claude-3-5-haiku-20241022": anthropicModels["claude-3-5-haiku-20241022"],
} as const satisfies Record<string, ModelInfo>
// AWS Bedrock
// https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference.html
export type BedrockModelId = keyof typeof bedrockModels
@@ -236,8 +236,6 @@ function convertApiProviderToProto(provider: string | undefined): ProtoApiProvid
return ProtoApiProvider.CEREBRAS
case "sapaicore":
return ProtoApiProvider.SAPAICORE
case "claude-code":
return ProtoApiProvider.CLAUDE_CODE
default:
return ProtoApiProvider.ANTHROPIC
}
@@ -296,8 +294,6 @@ function convertProtoToApiProvider(provider: ProtoApiProvider): ApiProvider {
return "cerebras"
case ProtoApiProvider.SAPAICORE:
return "sapaicore"
case ProtoApiProvider.CLAUDE_CODE:
return "claude-code"
default:
return "anthropic"
}
@@ -378,7 +374,6 @@ export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoA
sapAiResourceGroup: config.sapAiResourceGroup,
sapAiCoreTokenUrl: config.sapAiCoreTokenUrl,
sapAiCoreBaseUrl: config.sapAiCoreBaseUrl,
claudeCodePath: config.claudeCodePath,
}
}
@@ -457,6 +452,5 @@ export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguratio
sapAiResourceGroup: protoConfig.sapAiResourceGroup,
sapAiCoreTokenUrl: protoConfig.sapAiCoreTokenUrl,
sapAiCoreBaseUrl: protoConfig.sapAiCoreBaseUrl,
claudeCodePath: protoConfig.claudeCodePath,
}
}
@@ -117,9 +117,6 @@ export function convertApiConfigurationToProtoApiConfiguration(config: ApiConfig
litellmModelInfo: config.liteLlmModelInfo ? JSON.stringify(config.liteLlmModelInfo) : undefined,
openaiHeaders: config.openAiHeaders ? JSON.stringify(config.openAiHeaders) : undefined,
// Claude Code specific
claudeCodePath: config.claudeCodePath,
// Arrays
favoritedModelIds: config.favoritedModelIds || [],
})
@@ -225,9 +222,6 @@ export function convertProtoApiConfigurationToApiConfiguration(protoConfig: Prot
sapAiCoreTokenUrl: protoConfig.sapAiCoreTokenUrl,
sapAiResourceGroup: protoConfig.sapAiResourceGroup,
// Claude Code specific
claudeCodePath: protoConfig.claudeCodePath,
// Arrays
favoritedModelIds: protoConfig.favoritedModelIds || [],
}
+5 -3
View File
@@ -1,5 +1,7 @@
import { Channel, createChannel, createClient } from "nice-grpc"
import * as host from "@generated/nice-grpc/index.host"
import { UriServiceClientInterface, WatchServiceClientInterface } from "@/generated/hosts/host-bridge-client-types"
import { FileChangeEvent } from "@/shared/proto/index.host"
/**
* Singleton class to hold the gRPC clients for the host bridge. The clients should be re-used to avoid
@@ -36,7 +38,7 @@ const StubWatchServiceClient = {
subscribeToFile: function (
_r: host.SubscribeToFileRequest,
_h: {
onResponse?: (response: { type: host.FileChangeEvent_ChangeType }) => void | Promise<void>
onResponse?: (response: FileChangeEvent) => void | Promise<void>
onError?: (error: any) => void
onComplete?: () => void
},
@@ -47,5 +49,5 @@ const StubWatchServiceClient = {
const clientManager = HostBridgeClientManager.getInstance()
export const UriServiceClient = clientManager.uriClient
export const WatchServiceClient = StubWatchServiceClient
export const UriServiceClient: UriServiceClientInterface = clientManager.uriClient
export const WatchServiceClient: WatchServiceClientInterface = StubWatchServiceClient
+26 -1
View File
@@ -2,6 +2,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/vscode/host-grpc-handler"
const log = (...args: unknown[]) => {
const timestamp = new Date().toISOString()
@@ -16,4 +17,28 @@ function getPackageDefinition() {
const packageDefinition = { ...clineDef, ...healthDef }
return packageDefinition
}
export { getPackageDefinition, log }
/**
* 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 }
+28 -8
View File
@@ -132,6 +132,8 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
const [selectedImages, setSelectedImages] = useState<string[]>([])
const [selectedFiles, setSelectedFiles] = useState<string[]>([])
// we need to hold on to the ask because useEffect > lastMessage will always let us know when an ask comes in and handle it, but by the time handleMessage is called, the last message might not be the ask anymore (it could be a say that followed)
const [clineAsk, setClineAsk] = useState<ClineAsk | undefined>(undefined)
const [enableButtons, setEnableButtons] = useState<boolean>(false)
const [primaryButtonText, setPrimaryButtonText] = useState<string | undefined>("Approve")
const [secondaryButtonText, setSecondaryButtonText] = useState<string | undefined>("Reject")
@@ -144,14 +146,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
const [isAtBottom, setIsAtBottom] = useState(false)
const [pendingScrollToMessage, setPendingScrollToMessage] = useState<number | null>(null)
// UI layout depends on the last 2 messages
// (since it relies on the content of these messages, we are deep comparing. i.e. the button state after hitting button sets enableButtons to false, and this effect otherwise would have to true again even if messages didn't change
const lastMessage = useMemo(() => messages.at(-1), [messages])
const secondLastMessage = useMemo(() => messages.at(-2), [messages])
// Derive clineAsk directly from lastMessage to avoid race conditions
const clineAsk = useMemo(() => (lastMessage?.type === "ask" ? lastMessage.ask : undefined), [lastMessage])
useEffect(() => {
const handleCopy = async (e: ClipboardEvent) => {
const targetElement = e.target as HTMLElement | null
@@ -238,6 +232,11 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
document.removeEventListener("copy", handleCopy)
}
}, [])
// UI layout depends on the last 2 messages
// (since it relies on the content of these messages, we are deep comparing. i.e. the button state after hitting button sets enableButtons to false, and this effect otherwise would have to true again even if messages didn't change
const lastMessage = useMemo(() => messages.at(-1), [messages])
const secondLastMessage = useMemo(() => messages.at(-2), [messages])
useDeepCompareEffect(() => {
// if last message is an ask, show user ask UI
// if user finished a task, then start a new task with a new conversation history since in this moment that the extension is waiting for user response, the user could close the extension and the conversation history would be lost.
@@ -249,36 +248,42 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
switch (lastMessage.ask) {
case "api_req_failed":
setSendingDisabled(true)
setClineAsk("api_req_failed")
setEnableButtons(true)
setPrimaryButtonText("Retry")
setSecondaryButtonText("Start New Task")
break
case "mistake_limit_reached":
setSendingDisabled(false)
setClineAsk("mistake_limit_reached")
setEnableButtons(true)
setPrimaryButtonText("Proceed Anyways")
setSecondaryButtonText("Start New Task")
break
case "auto_approval_max_req_reached":
setSendingDisabled(true)
setClineAsk("auto_approval_max_req_reached")
setEnableButtons(true)
setPrimaryButtonText("Proceed")
setSecondaryButtonText("Start New Task")
break
case "followup":
setSendingDisabled(isPartial)
setClineAsk("followup")
setEnableButtons(false)
// setPrimaryButtonText(undefined)
// setSecondaryButtonText(undefined)
break
case "plan_mode_respond":
setSendingDisabled(isPartial)
setClineAsk("plan_mode_respond")
setEnableButtons(false)
// setPrimaryButtonText(undefined)
// setSecondaryButtonText(undefined)
break
case "tool":
setSendingDisabled(isPartial)
setClineAsk("tool")
setEnableButtons(!isPartial)
const tool = JSON.parse(lastMessage.text || "{}") as ClineSayTool
switch (tool.tool) {
@@ -295,24 +300,28 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
break
case "browser_action_launch":
setSendingDisabled(isPartial)
setClineAsk("browser_action_launch")
setEnableButtons(!isPartial)
setPrimaryButtonText("Approve")
setSecondaryButtonText("Reject")
break
case "command":
setSendingDisabled(isPartial)
setClineAsk("command")
setEnableButtons(!isPartial)
setPrimaryButtonText("Run Command")
setSecondaryButtonText("Reject")
break
case "command_output":
setSendingDisabled(false)
setClineAsk("command_output")
setEnableButtons(true)
setPrimaryButtonText("Proceed While Running")
setSecondaryButtonText(undefined)
break
case "use_mcp_server":
setSendingDisabled(isPartial)
setClineAsk("use_mcp_server")
setEnableButtons(!isPartial)
setPrimaryButtonText("Approve")
setSecondaryButtonText("Reject")
@@ -320,12 +329,14 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
case "completion_result":
// extension waiting for feedback. but we can just present a new task button
setSendingDisabled(isPartial)
setClineAsk("completion_result")
setEnableButtons(!isPartial)
setPrimaryButtonText("Start New Task")
setSecondaryButtonText(undefined)
break
case "resume_task":
setSendingDisabled(false)
setClineAsk("resume_task")
setEnableButtons(true)
setPrimaryButtonText("Resume Task")
setSecondaryButtonText(undefined)
@@ -333,6 +344,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
break
case "resume_completed_task":
setSendingDisabled(false)
setClineAsk("resume_completed_task")
setEnableButtons(true)
setPrimaryButtonText("Start New Task")
setSecondaryButtonText(undefined)
@@ -340,18 +352,21 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
break
case "new_task":
setSendingDisabled(isPartial)
setClineAsk("new_task")
setEnableButtons(!isPartial)
setPrimaryButtonText("Start New Task with Context")
setSecondaryButtonText(undefined)
break
case "condense":
setSendingDisabled(isPartial)
setClineAsk("condense")
setEnableButtons(!isPartial)
setPrimaryButtonText("Condense Conversation")
setSecondaryButtonText(undefined)
break
case "report_bug":
setSendingDisabled(isPartial)
setClineAsk("report_bug")
setEnableButtons(!isPartial)
setPrimaryButtonText("Report GitHub issue")
setSecondaryButtonText(undefined)
@@ -368,6 +383,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
setSendingDisabled(true)
setSelectedImages([])
setSelectedFiles([])
setClineAsk(undefined)
setEnableButtons(false)
}
break
@@ -403,6 +419,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
useEffect(() => {
if (messages.length === 0) {
setSendingDisabled(false)
setClineAsk(undefined)
setEnableButtons(false)
setPrimaryButtonText("Approve")
setSecondaryButtonText("Reject")
@@ -487,6 +504,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
setSendingDisabled(true)
setSelectedImages([])
setSelectedFiles([])
setClineAsk(undefined)
setEnableButtons(false)
// setPrimaryButtonText(undefined)
// setSecondaryButtonText(undefined)
@@ -566,6 +584,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
break
}
setSendingDisabled(true)
setClineAsk(undefined)
setEnableButtons(false)
// setPrimaryButtonText(undefined)
// setSecondaryButtonText(undefined)
@@ -618,6 +637,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
break
}
setSendingDisabled(true)
setClineAsk(undefined)
setEnableButtons(false)
// setPrimaryButtonText(undefined)
// setSecondaryButtonText(undefined)
@@ -16,8 +16,6 @@ import {
bedrockModels,
cerebrasDefaultModelId,
cerebrasModels,
claudeCodeDefaultModelId,
claudeCodeModels,
deepSeekDefaultModelId,
deepSeekModels,
doubaoDefaultModelId,
@@ -358,7 +356,6 @@ const ApiOptions = ({
<VSCodeOption value="cline">Cline</VSCodeOption>
<VSCodeOption value="openrouter">OpenRouter</VSCodeOption>
<VSCodeOption value="anthropic">Anthropic</VSCodeOption>
<VSCodeOption value="claude-code">Claude Code</VSCodeOption>
<VSCodeOption value="bedrock">Amazon Bedrock</VSCodeOption>
<VSCodeOption value="openai">OpenAI Compatible</VSCodeOption>
<VSCodeOption value="vertex">GCP Vertex AI</VSCodeOption>
@@ -476,27 +473,6 @@ const ApiOptions = ({
</div>
)}
{selectedProvider === "claude-code" && (
<div>
<VSCodeTextField
value={apiConfiguration?.claudeCodePath || ""}
style={{ width: "100%", marginTop: 3 }}
type="text"
onInput={handleInputChange("claudeCodePath")}
placeholder="Default: claude"
/>
<p
style={{
fontSize: "12px",
marginTop: 3,
color: "var(--vscode-descriptionForeground)",
}}>
Path to the Claude Code CLI.
</p>
</div>
)}
{selectedProvider === "openai-native" && (
<div>
<VSCodeTextField
@@ -2277,7 +2253,6 @@ const ApiOptions = ({
<span style={{ fontWeight: 500 }}>Model</span>
</label>
{selectedProvider === "anthropic" && createDropdown(anthropicModels)}
{selectedProvider === "claude-code" && createDropdown(claudeCodeModels)}
{selectedProvider === "vertex" &&
createDropdown(apiConfiguration?.vertexRegion === "global" ? vertexGlobalModels : vertexModels)}
{selectedProvider === "gemini" && createDropdown(geminiModels)}
@@ -2629,8 +2604,6 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration):
switch (provider) {
case "anthropic":
return getProviderData(anthropicModels, anthropicDefaultModelId)
case "claude-code":
return getProviderData(claudeCodeModels, claudeCodeDefaultModelId)
case "bedrock":
if (apiConfiguration?.awsBedrockCustomSelected) {
const baseModelId = apiConfiguration.awsBedrockCustomModelBaseId