mirror of
https://github.com/cline/cline.git
synced 2026-09-07 22:16:30 +08:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20bc8a2c0b | ||
|
|
6057557550 |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fixed race condition where clineAsk was undefined, leading to task restoration and other downstream issues
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Integrate Claude Code
|
||||
+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",
|
||||
"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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Executable
+192
@@ -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)
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,7 +79,6 @@ export type GlobalStateKey =
|
||||
| "sapAiCoreClientId"
|
||||
| "sapAiCoreClientSecret"
|
||||
| "sapAiCoreModelId"
|
||||
| "claudeCodePath"
|
||||
|
||||
export type LocalStateKey =
|
||||
| "localClineRulesToggles"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 || [],
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user