Compare commits

..

1 Commits

Author SHA1 Message Date
celestial-vault f146f5f179 catch error more broadly 2025-06-14 22:18:44 -07:00
46 changed files with 287 additions and 1230 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Exclude clinerules from checkpoints
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Update developer reset to allow for resetting workspace settings.
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Added file context warnings to reduce diff edit errors when resuming a task after it has been restored
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Clear the chat input when the user changes mode within a task
-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
---
Fix where clineMessages were not saving checkpoint commitHash on some messages
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Integrate Claude Code
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Better debounce on checkmark control menu
+3 -2
View File
@@ -21,9 +21,8 @@ coverage
*evals.env
# Generated files
# Generated proto files
src/generated/
# Core
src/core/controller/*/methods.ts
src/core/controller/*/index.ts
src/core/controller/grpc-service-config.ts
@@ -32,6 +31,8 @@ src/shared/proto/*.ts
src/shared/proto/host/*.ts
# Webview
webview-ui/src/services/grpc-client.ts
# Standalone
src/standalone/server-setup.ts
# Host bridge
src/hosts/vscode/*/methods.ts
src/hosts/vscode/*/index.ts
+2 -3
View File
@@ -42,7 +42,7 @@
{
"type": "node",
"request": "launch",
"name": "Run Standalone Service",
"name": "Run Standalone Extension",
"skipFiles": ["<node_internals>/**"],
"sourceMaps": true,
"resolveSourceMapLocations": ["${workspaceFolder}/**", "!**/node_modules/**"],
@@ -53,8 +53,7 @@
"GRPC_TRACE": "all",
"GRPC_VERBOSITY": "DEBUG",
"NODE_PATH": "${workspaceFolder}/dist-standalone/node_modules",
"CLINE_DIR": "${userHome}/.cline-standalone",
"HOST_BRIDGE_ADDRESS": "localhost:50052"
"CLINE_DIR": "${userHome}/.cline-standalone"
},
"program": "standalone.js"
}
-100
View File
@@ -53,7 +53,6 @@
"jschardet": "^3.1.4",
"mammoth": "^1.8.0",
"monaco-vscode-textmate-theme-converter": "^0.1.7",
"nice-grpc": "^2.1.12",
"ollama": "^0.5.13",
"open": "^10.1.2",
"open-graph-scraper": "^6.9.0",
@@ -3756,16 +3755,6 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@js-sdsl/ordered-map": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz",
"integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/js-sdsl"
}
},
"node_modules/@jsep-plugin/assignment": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz",
@@ -8704,12 +8693,6 @@
"node": ">=6.5"
}
},
"node_modules/abort-controller-x": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/abort-controller-x/-/abort-controller-x-0.4.3.tgz",
"integrity": "sha512-VtUwTNU8fpMwvWGn4xE93ywbogTYsuT+AUxAXOeelbXuQVIwNmC5YLeho9sH4vZ4ITW8414TTAOG1nW6uIVHCA==",
"license": "MIT"
},
"node_modules/accepts": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
@@ -18246,39 +18229,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/nice-grpc": {
"version": "2.1.12",
"resolved": "https://registry.npmjs.org/nice-grpc/-/nice-grpc-2.1.12.tgz",
"integrity": "sha512-J1n4Wg+D3IhRhGQb+iqh2OpiM0GzTve/kf2lnlW4S+xczmIEd0aHUDV1OsJ5a3q8GSTqJf+s4Rgg1M8uJltarw==",
"license": "MIT",
"dependencies": {
"@grpc/grpc-js": "^1.13.1",
"abort-controller-x": "^0.4.0",
"nice-grpc-common": "^2.0.2"
}
},
"node_modules/nice-grpc-common": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/nice-grpc-common/-/nice-grpc-common-2.0.2.tgz",
"integrity": "sha512-7RNWbls5kAL1QVUOXvBsv1uO0wPQK3lHv+cY1gwkTzirnG1Nop4cBJZubpgziNbaVc/bl9QJcyvsf/NQxa3rjQ==",
"license": "MIT",
"dependencies": {
"ts-error": "^1.0.6"
}
},
"node_modules/nice-grpc/node_modules/@grpc/grpc-js": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz",
"integrity": "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==",
"license": "Apache-2.0",
"dependencies": {
"@grpc/proto-loader": "^0.7.13",
"@js-sdsl/ordered-map": "^4.4.2"
},
"engines": {
"node": ">=12.10.0"
}
},
"node_modules/nice-try": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
@@ -22400,12 +22350,6 @@
"typescript": ">=4.2.0"
}
},
"node_modules/ts-error": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/ts-error/-/ts-error-1.0.6.tgz",
"integrity": "sha512-tLJxacIQUM82IR7JO1UUkKlYuUTmoY9HBJAmNWFzheSlDS5SPMcNIepejHJa4BpPQLAcbRhRf3GDJzyj6rbKvA==",
"license": "MIT"
},
"node_modules/ts-morph": {
"version": "25.0.1",
"resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-25.0.1.tgz",
@@ -26711,11 +26655,6 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"@js-sdsl/ordered-map": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz",
"integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="
},
"@jsep-plugin/assignment": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz",
@@ -30429,11 +30368,6 @@
"event-target-shim": "^5.0.0"
}
},
"abort-controller-x": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/abort-controller-x/-/abort-controller-x-0.4.3.tgz",
"integrity": "sha512-VtUwTNU8fpMwvWGn4xE93ywbogTYsuT+AUxAXOeelbXuQVIwNmC5YLeho9sH4vZ4ITW8414TTAOG1nW6uIVHCA=="
},
"accepts": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
@@ -36855,35 +36789,6 @@
}
}
},
"nice-grpc": {
"version": "2.1.12",
"resolved": "https://registry.npmjs.org/nice-grpc/-/nice-grpc-2.1.12.tgz",
"integrity": "sha512-J1n4Wg+D3IhRhGQb+iqh2OpiM0GzTve/kf2lnlW4S+xczmIEd0aHUDV1OsJ5a3q8GSTqJf+s4Rgg1M8uJltarw==",
"requires": {
"@grpc/grpc-js": "^1.13.1",
"abort-controller-x": "^0.4.0",
"nice-grpc-common": "^2.0.2"
},
"dependencies": {
"@grpc/grpc-js": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz",
"integrity": "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==",
"requires": {
"@grpc/proto-loader": "^0.7.13",
"@js-sdsl/ordered-map": "^4.4.2"
}
}
}
},
"nice-grpc-common": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/nice-grpc-common/-/nice-grpc-common-2.0.2.tgz",
"integrity": "sha512-7RNWbls5kAL1QVUOXvBsv1uO0wPQK3lHv+cY1gwkTzirnG1Nop4cBJZubpgziNbaVc/bl9QJcyvsf/NQxa3rjQ==",
"requires": {
"ts-error": "^1.0.6"
}
},
"nice-try": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
@@ -39757,11 +39662,6 @@
"dev": true,
"requires": {}
},
"ts-error": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/ts-error/-/ts-error-1.0.6.tgz",
"integrity": "sha512-tLJxacIQUM82IR7JO1UUkKlYuUTmoY9HBJAmNWFzheSlDS5SPMcNIepejHJa4BpPQLAcbRhRf3GDJzyj6rbKvA=="
},
"ts-morph": {
"version": "25.0.1",
"resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-25.0.1.tgz",
+1 -2
View File
@@ -331,7 +331,7 @@
"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",
"postprotos": "prettier src/shared/proto src/core/controller src/hosts/ webview-ui/src/services src/generated --write --log-level warn",
"postprotos": "prettier src/shared/proto src/core/controller src/hosts/ webview-ui/src/services src/standalone/server-setup.ts --write --log-level silent",
"compile-tests": "node ./scripts/build-tests.js",
"watch-tests": "tsc -p . -w --outDir out",
"pretest": "npm run compile-tests && npm run compile && npm run compile-standalone && npm run lint",
@@ -448,7 +448,6 @@
"jschardet": "^3.1.4",
"mammoth": "^1.8.0",
"monaco-vscode-textmate-theme-converter": "^0.1.7",
"nice-grpc": "^2.1.12",
"ollama": "^0.5.13",
"open": "^10.1.2",
"open-graph-scraper": "^6.9.0",
+16 -23
View File
@@ -15,10 +15,9 @@ const PROTOC = path.join(require.resolve("grpc-tools"), "../bin/protoc")
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url))
const ROOT_DIR = path.resolve(SCRIPT_DIR, "..")
const TS_OUT_DIR = path.join(ROOT_DIR, "src/shared/proto")
const GRPC_JS_OUT_DIR = path.join(ROOT_DIR, "src/generated/grpc-js")
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 TS_OUT_DIR = path.join(ROOT_DIR, "src", "shared", "proto")
const GRPC_JS_OUT_DIR = path.join(ROOT_DIR, "src", "generated", "grpc-js")
const DESCRIPTOR_OUT_DIR = path.join(ROOT_DIR, "dist-standalone", "proto")
const isWindows = process.platform === "win32"
const TS_PROTO_PLUGIN = isWindows
@@ -28,7 +27,6 @@ const TS_PROTO_PLUGIN = isWindows
const TS_PROTO_OPTIONS = [
"env=node",
"esModuleInterop=true",
"outputServices=generic-definitions", // output generic ServiceDefinitions
"outputIndex=true", // output an index file for each package which exports all protos in the package.
"useOptionals=messages", // Message fields are optional, scalars are not.
"useDate=false", // Timestamp fields will not be automatically converted to Date.
@@ -51,7 +49,7 @@ const serviceNameMap = {
ui: "cline.UiService",
// Add new services here - no other code changes needed!
}
const serviceDirs = Object.keys(serviceNameMap).map((serviceKey) => path.join(ROOT_DIR, "src/core/controller", serviceKey))
const serviceDirs = Object.keys(serviceNameMap).map((serviceKey) => path.join(ROOT_DIR, "src", "core", "controller", serviceKey))
// List of host gRPC services (IDE API bridge)
// These services are implemented in the IDE extension and called by the standalone Cline Core
@@ -60,7 +58,9 @@ const hostServiceNameMap = {
watch: "host.WatchService",
// Add new host services here
}
const hostServiceDirs = Object.keys(hostServiceNameMap).map((serviceKey) => path.join(ROOT_DIR, "src/hosts/vscode", serviceKey))
const hostServiceDirs = Object.keys(hostServiceNameMap).map((serviceKey) =>
path.join(ROOT_DIR, "src", "hosts", "vscode", serviceKey),
)
async function main() {
console.log(chalk.bold.blue("Starting Protocol Buffer code generation..."))
@@ -69,7 +69,7 @@ async function main() {
checkAppleSiliconCompatibility()
// Create output directories if they don't exist
for (const dir of [TS_OUT_DIR, GRPC_JS_OUT_DIR, NICE_JS_OUT_DIR, DESCRIPTOR_OUT_DIR]) {
for (const dir of [TS_OUT_DIR, GRPC_JS_OUT_DIR, DESCRIPTOR_OUT_DIR]) {
await fs.mkdir(dir, { recursive: true })
}
@@ -82,11 +82,8 @@ async function main() {
const protoFiles = await globby("**/*.proto", { cwd: SCRIPT_DIR, realpath: true })
console.log(chalk.cyan(`Processing ${protoFiles.length} proto files from`), SCRIPT_DIR)
tsProtoc(TS_OUT_DIR, protoFiles, TS_PROTO_OPTIONS)
// grpc-js is used to generate service impls for the ProtoBus service.
tsProtoc(GRPC_JS_OUT_DIR, protoFiles, ["outputServices=grpc-js,outputClientImpl=false", ...TS_PROTO_OPTIONS])
// nice-js is used for the Host Bridge client impls because it uses promises.
tsProtoc(NICE_JS_OUT_DIR, protoFiles, ["outputServices=nice-grpc,useExactTypes=false", ...TS_PROTO_OPTIONS])
tsProtoc(TS_OUT_DIR, protoFiles, ["outputServices=generic-definitions", ...TS_PROTO_OPTIONS])
tsProtoc(GRPC_JS_OUT_DIR, protoFiles, ["outputServices=grpc-js", ...TS_PROTO_OPTIONS])
const descriptorFile = path.join(DESCRIPTOR_OUT_DIR, "descriptor_set.pb")
const descriptorProtocCommand = [
@@ -119,18 +116,17 @@ async function main() {
async function tsProtoc(outDir, protoFiles, protoOptions) {
// Build the protoc command with proper path handling for cross-platform
const command = [
const tsProtocCommand = [
PROTOC,
`--proto_path="${SCRIPT_DIR}"`,
`--plugin=protoc-gen-ts_proto="${TS_PROTO_PLUGIN}"`,
`--ts_proto_out="${outDir}"`,
`--ts_proto_opt=${protoOptions.join(",")} `,
...protoFiles.map((s) => `"${s}"`),
...protoFiles,
].join(" ")
try {
log_verbose(chalk.cyan(`Generating TypeScript code in ${outDir} for:\n${protoFiles.join("\n")}...`))
log_verbose(command)
execSync(command, { stdio: "inherit" })
execSync(tsProtocCommand, { stdio: "inherit" })
} catch (error) {
console.error(chalk.red("Error generating TypeScript for proto files:"), error)
process.exit(1)
@@ -666,13 +662,10 @@ async function cleanup() {
}
// Clean up generated files that were moved.
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"))
await fs.rm(path.join(ROOT_DIR, "hosts/vscode"), { force: true, recursive: 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"))
await fs.rm(path.join(ROOT_DIR, "hosts", "vscode"), { force: true, recursive: true })
await rmdir(path.join(ROOT_DIR, "hosts"))
await fs.rm(path.join(ROOT_DIR, "src/standalone/server-setup.ts"), { force: true })
}
/**
+2 -8
View File
@@ -121,8 +121,6 @@ enum ApiProvider {
XAI = 21;
SAMBANOVA = 22;
CEREBRAS = 23;
SAPAICORE = 24;
CLAUDE_CODE = 25;
}
// Model info for OpenAI-compatible models
@@ -228,12 +226,8 @@ message ModelsApiConfiguration {
optional string sambanova_api_key = 63;
optional string cerebras_api_key = 64;
optional int32 request_timeout_ms = 65;
// From ApiConfiguration (additional fields)
optional ApiProvider api_provider = 66;
repeated string favorited_model_ids = 67;
optional string sap_ai_core_client_id = 68;
optional string sap_ai_core_client_secret = 69;
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;
}
+1 -9
View File
@@ -13,7 +13,7 @@ service StateService {
rpc getAvailableTerminalProfiles(EmptyRequest) returns (TerminalProfiles);
rpc subscribeToState(EmptyRequest) returns (stream State);
rpc toggleFavoriteModel(StringRequest) returns (Empty);
rpc resetState(ResetStateRequest) returns (Empty);
rpc resetState(EmptyRequest) returns (Empty);
rpc togglePlanActMode(TogglePlanActModeRequest) returns (Empty);
rpc updateAutoApprovalSettings(AutoApprovalSettingsRequest) returns (Empty);
rpc updateSettings(UpdateSettingsRequest) returns (Empty);
@@ -63,11 +63,6 @@ message ChatContent {
repeated string files = 3;
}
message ResetStateRequest {
Metadata metadata = 1;
optional bool global = 2;
}
message AutoApprovalSettingsRequest {
Metadata metadata = 1;
message Actions {
@@ -217,7 +212,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;
}
+8 -9
View File
@@ -1,11 +1,11 @@
import * as fs from "fs"
import * as grpc from "@grpc/grpc-js"
import * as protoLoader from "@grpc/proto-loader"
import * as fs from "fs"
import * as health from "grpc-health-check"
import path, { basename, dirname } from "path"
import { fileURLToPath } from "url"
import path from "path"
const OUT_FILE = path.resolve("src/generated/standalone/server-setup.ts")
const OUT_FILE = path.resolve("src/standalone/server-setup.ts")
const DESCRIPTOR_SET = path.resolve("dist-standalone/proto/descriptor_set.pb")
// Load service definitions.
@@ -31,7 +31,7 @@ function generateHandlersAndExports() {
handlerSetup.push(` // ${domain} Service`)
handlerSetup.push(` server.addService(cline.${name}Service, {`)
for (const [rpcName, rpc] of Object.entries(def.service)) {
imports.push(`import { ${rpcName} } from "@core/controller/${dir}/${rpcName}"`)
imports.push(`import { ${rpcName} } from "../core/controller/${dir}/${rpcName}"`)
const requestType = "cline." + rpc.requestType.type.name
if (rpc.requestStream) {
throw new Error("Request streaming is not supported")
@@ -60,12 +60,12 @@ const scriptName = path.basename(fileURLToPath(import.meta.url))
let output = `// GENERATED CODE -- DO NOT EDIT!
// Generated by ${scriptName}
import * as grpc from "@grpc/grpc-js"
import { cline } from "@generated/grpc-js"
import { Controller } from "@core/controller"
import { GrpcHandlerWrapper, GrpcStreamingResponseHandlerWrapper } from "@/standalone/grpc-types"
import { cline } from "../generated/grpc-js"
import { Controller } from "../core/controller"
import { GrpcHandlerWrapper, GrpcStreamingResponseHandlerWrapper } from "./grpc-types"
${imports}
export function addProtobusServices(
export function addServices(
server: grpc.Server,
controller: Controller,
wrapper: GrpcHandlerWrapper,
@@ -75,7 +75,6 @@ ${handlerSetup}
}
`
// Write output file
fs.mkdirSync(dirname(OUT_FILE), { recursive: true })
fs.writeFileSync(OUT_FILE, output)
console.log(`Generated service handlers in ${OUT_FILE}.`)
-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,10 +1,7 @@
import * as path from "path"
import * as vscode from "vscode"
import { getTaskMetadata, saveTaskMetadata } from "@core/storage/disk"
import { getWorkspaceState, updateWorkspaceState } from "@core/storage/state"
import { getGlobalState } from "@core/storage/state"
import type { FileMetadataEntry } from "./ContextTrackerTypes"
import type { ClineMessage } from "@shared/ExtensionMessage"
// This class is responsible for tracking file operations that may result in stale context.
// If a user modifies a file outside of Cline, the context may become stale and need to be updated.
@@ -14,12 +11,10 @@ import type { ClineMessage } from "@shared/ExtensionMessage"
// a diff edit because the file was modified since Cline last read it.
// FileContextTracker
/**
This class is responsible for tracking file operations.
If the full contents of a file are passed to Cline via a tool, mention, or edit, the file is marked as active.
If a file is modified outside of Cline, we detect and track this change to prevent stale context.
This is used when restoring a task (non-git "checkpoint" restore), and mid-task.
*/
//
// This class is responsible for tracking file operations.
// If the full contents of a file are pass to Cline via a tool, mention, or edit, the file is marked as active.
// If a file is modified outside of Cline, we detect and track this change to prevent stale context.
export class FileContextTracker {
private context: vscode.ExtensionContext
readonly taskId: string
@@ -34,9 +29,7 @@ export class FileContextTracker {
this.taskId = taskId
}
/**
* Gets the current working directory or returns undefined if it cannot be determined
*/
// Gets the current working directory or returns undefined if it cannot be determined
private getCwd(): string | undefined {
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
if (!cwd) {
@@ -45,9 +38,7 @@ export class FileContextTracker {
return cwd
}
/**
* File watchers are set up for each file that is tracked in the task metadata.
*/
// File watchers are set up for each file that is tracked in the task metadata.
async setupFileWatcher(filePath: string) {
// Only setup watcher if it doesn't already exist for this file
if (this.fileWatchers.has(filePath)) {
@@ -79,10 +70,8 @@ export class FileContextTracker {
this.fileWatchers.set(filePath, watcher)
}
/**
* Tracks a file operation in metadata and sets up a watcher for the file
* This is the main entry point for FileContextTracker and is called when a file is passed to Cline via a tool, mention, or edit.
*/
// Tracks a file operation in metadata and sets up a watcher for the file
// This is the main entry point for FileContextTracker and is called when a file is passed to Cline via a tool, mention, or edit.
async trackFileContext(filePath: string, operation: "read_tool" | "user_edited" | "cline_edited" | "file_mentioned") {
try {
const cwd = this.getCwd()
@@ -100,11 +89,9 @@ export class FileContextTracker {
}
}
/**
* Adds a file to the metadata tracker
* This handles the business logic of determining if the file is new, stale, or active.
* It also updates the metadata with the latest read/edit dates.
*/
// Adds a file to the metadata tracker
// This handles the business logic of determining if the file is new, stale, or active.
// It also updates the metadata with the latest read/edit dates.
async addFileToFileContextTracker(
context: vscode.ExtensionContext,
taskId: string,
@@ -167,149 +154,23 @@ export class FileContextTracker {
}
}
/**
* Returns (and then clears) the set of recently modified files
*/
// Returns (and then clears) the set of recently modified files
getAndClearRecentlyModifiedFiles(): string[] {
const files = Array.from(this.recentlyModifiedFiles)
this.recentlyModifiedFiles.clear()
return files
}
/**
* Marks a file as edited by Cline to prevent false positives in file watchers
*/
// Marks a file as edited by Cline to prevent false positives in file watchers
markFileAsEditedByCline(filePath: string): void {
this.recentlyEditedByCline.add(filePath)
}
/**
* Disposes all file watchers
*/
// Disposes all file watchers
dispose(): void {
for (const watcher of this.fileWatchers.values()) {
watcher.dispose()
}
this.fileWatchers.clear()
}
/**
* Detects files that were edited by Cline or users after a specific message timestamp
* This is used when restoring checkpoints to warn about potential file content mismatches
*/
async detectFilesEditedAfterMessage(messageTs: number, deletedMessages: ClineMessage[]): Promise<string[]> {
const editedFiles: string[] = []
try {
// Check task metadata for files that were edited by Cline or users after the message timestamp
const taskMetadata = await getTaskMetadata(this.context, this.taskId)
if (taskMetadata?.files_in_context) {
for (const fileEntry of taskMetadata.files_in_context) {
const clineEditedAfter = fileEntry.cline_edit_date && fileEntry.cline_edit_date > messageTs
const userEditedAfter = fileEntry.user_edit_date && fileEntry.user_edit_date > messageTs
if (clineEditedAfter || userEditedAfter) {
editedFiles.push(fileEntry.path)
}
}
}
} catch (error) {
console.error("Error checking file context metadata:", error)
}
// Also check deleted task messages for file operations
for (const message of deletedMessages) {
if (message.say === "tool" && message.text) {
try {
const toolData = JSON.parse(message.text)
if ((toolData.tool === "editedExistingFile" || toolData.tool === "newFileCreated") && toolData.path) {
if (!editedFiles.includes(toolData.path)) {
editedFiles.push(toolData.path)
}
}
} catch (error) {
console.error("Error checking task messages:", error)
}
}
}
return [...new Set(editedFiles)]
}
/**
* Stores pending file context warning in workspace state so it persists across task reinitialization
*/
async storePendingFileContextWarning(files: string[]): Promise<void> {
try {
const key = `pendingFileContextWarning_${this.taskId}`
await updateWorkspaceState(this.context, key, files)
} catch (error) {
console.error("Error storing pending file context warning:", error)
}
}
/**
* Retrieves pending file context warning from workspace state (without clearing it)
*/
async retrievePendingFileContextWarning(): Promise<string[] | undefined> {
try {
const key = `pendingFileContextWarning_${this.taskId}`
const files = (await getWorkspaceState(this.context, key)) as string[]
return files
} catch (error) {
console.error("Error retrieving pending file context warning:", error)
}
return undefined
}
/**
* Retrieves and clears pending file context warning from workspace state
*/
async retrieveAndClearPendingFileContextWarning(): Promise<string[] | undefined> {
try {
const files = await this.retrievePendingFileContextWarning()
if (files) {
await updateWorkspaceState(this.context, `pendingFileContextWarning_${this.taskId}`, undefined)
return files
}
} catch (error) {
console.error("Error retrieving pending file context warning:", error)
}
return undefined
}
/**
* Static method to clean up orphaned pending file context warnings at startup
* This removes warnings for tasks that may no longer exist
*/
static async cleanupOrphanedWarnings(context: vscode.ExtensionContext): Promise<void> {
const startTime = Date.now()
try {
const taskHistory = ((await getGlobalState(context, "taskHistory")) as Array<{ id: string }>) || []
const existingTaskIds = new Set(taskHistory.map((task) => task.id))
const allStateKeys = context.workspaceState.keys()
const pendingWarningKeys = allStateKeys.filter((key) => key.startsWith("pendingFileContextWarning_"))
const orphanedPendingContextTasks: string[] = []
for (const key of pendingWarningKeys) {
const taskId = key.replace("pendingFileContextWarning_", "")
if (!existingTaskIds.has(taskId)) {
orphanedPendingContextTasks.push(key)
}
}
if (orphanedPendingContextTasks.length > 0) {
for (const key of orphanedPendingContextTasks) {
await updateWorkspaceState(context, key, undefined)
}
}
const duration = Date.now() - startTime
console.log(
`FileContextTracker: Processed ${existingTaskIds.size} tasks, found ${pendingWarningKeys.length} pending warnings, ${orphanedPendingContextTasks.length} orphaned, deleted ${orphanedPendingContextTasks.length}, took ${duration}ms`,
)
} catch (error) {
console.error("Error cleaning up orphaned file context warnings:", error)
}
}
}
+10 -10
View File
@@ -64,17 +64,17 @@ export class GrpcRequestRegistry {
*/
public cancelRequest(requestId: string): boolean {
const requestInfo = this.activeRequests.get(requestId)
if (!requestInfo) {
return false
if (requestInfo) {
try {
requestInfo.cleanup()
console.log(`[DEBUG] Cleaned up request: ${requestId}`)
} catch (error) {
console.error(`Error cleaning up request ${requestId}:`, error)
}
this.activeRequests.delete(requestId)
return true
}
try {
requestInfo.cleanup()
console.log(`[DEBUG] Cleaned up request: ${requestId}`)
} catch (error) {
console.error(`Error cleaning up request ${requestId}:`, error)
}
this.activeRequests.delete(requestId)
return true
return false
}
/**
+6 -12
View File
@@ -1,25 +1,19 @@
import { Controller } from ".."
import { Empty } from "../../../shared/proto/common"
import { ResetStateRequest } from "../../../shared/proto/state"
import { resetGlobalState, resetWorkspaceState } from "../../../core/storage/state"
import { Empty, EmptyRequest } from "../../../shared/proto/common"
import { resetExtensionState } from "../../../core/storage/state"
import * as vscode from "vscode"
import { sendChatButtonClickedEvent } from "../ui/subscribeToChatButtonClicked"
/**
* Resets the extension state to its defaults
* @param controller The controller instance
* @param request The reset state request containing the global flag
* @param request An empty request (no parameters needed)
* @returns An empty response
*/
export async function resetState(controller: Controller, request: ResetStateRequest): Promise<Empty> {
export async function resetState(controller: Controller, request: EmptyRequest): Promise<Empty> {
try {
if (request.global) {
vscode.window.showInformationMessage("Resetting global state...")
await resetGlobalState(controller.context)
} else {
vscode.window.showInformationMessage("Resetting workspace state...")
await resetWorkspaceState(controller.context)
}
vscode.window.showInformationMessage("Resetting state...")
await resetExtensionState(controller.context)
if (controller.task) {
controller.task.abortTask()
+1 -15
View File
@@ -149,14 +149,13 @@ Otherwise, if you have not completed the task and do not need additional informa
cwd: string,
wasRecent: boolean | 0 | undefined,
responseText?: string,
hasPendingFileContextWarnings?: boolean,
): [string, string] => {
const taskResumptionMessage = `[TASK RESUMPTION] ${
mode === "plan"
? `This task was interrupted ${agoText}. The conversation may have been incomplete. Be aware that the project state may have changed since then. The current working directory is now '${cwd.toPosix()}'.\n\nNote: If you previously attempted a tool use that the user did not provide a result for, you should assume the tool use was not successful. However you are in PLAN MODE, so rather than continuing the task, you must respond to the user's message.`
: `This task was interrupted ${agoText}. It may or may not be complete, so please reassess the task context. Be aware that the project state may have changed since then. The current working directory is now '${cwd.toPosix()}'. If the task has not been completed, retry the last step before interruption and proceed with completing the task.\n\nNote: If you previously attempted a tool use that the user did not provide a result for, you should assume the tool use was not successful and assess whether you should retry. If the last tool was a browser_action, the browser has been closed and you must launch a new browser if needed.`
}${
wasRecent && !hasPendingFileContextWarnings
wasRecent
? "\n\nIMPORTANT: If the last tool use was a replace_in_file or write_to_file that was interrupted, the file was reverted back to its original state before the interrupted edit, and you do NOT need to re-read the file as you already have its up-to-date contents."
: ""
}`
@@ -241,19 +240,6 @@ Otherwise, if you have not completed the task and do not need additional informa
cursorRulesLocalDirectoryInstructions: (cwd: string, content: string) =>
`# .cursor/rules\n\nThe following is provided by a root-level .cursor/rules directory where the user has specified instructions for this working directory (${cwd.toPosix()})\n\n${content}`,
fileContextWarning: (editedFiles: string[]): string => {
const fileCount = editedFiles.length
const fileVerb = fileCount === 1 ? "file has" : "files have"
const fileDemonstrativePronoun = fileCount === 1 ? "this file" : "these files"
const filePersonalPronoun = fileCount === 1 ? "it" : "they"
return (
`<explicit_instructions>\nCRITICAL FILE STATE ALERT: ${fileCount} ${fileVerb} been externally modified since your last interaction. Your cached understanding of ${fileDemonstrativePronoun} is now stale and unreliable. Before making ANY modifications to ${fileDemonstrativePronoun}, you must execute read_file to obtain the current state, as ${filePersonalPronoun} may contain completely different content than what you expect:\n` +
`${editedFiles.map((file) => ` ${path.resolve(file).toPosix()}`).join("\n")}\n` +
`Failure to re-read before editing will result in replace_in_file edit errors, requiring subsequent attempts and wasting tokens. You DO NOT need to re-read these files after subsequent edits, unless instructed to do so.\n</explicit_instructions>`
)
},
}
// to avoid circular dependency
-1
View File
@@ -79,7 +79,6 @@ export type GlobalStateKey =
| "sapAiCoreClientId"
| "sapAiCoreClientSecret"
| "sapAiCoreModelId"
| "claudeCodePath"
export type LocalStateKey =
| "localClineRulesToggles"
+1 -13
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)
@@ -705,14 +700,7 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
await storeSecret(context, "sapAiCoreClientSecret", sapAiCoreClientSecret)
}
export async function resetWorkspaceState(context: vscode.ExtensionContext) {
for (const key of context.workspaceState.keys()) {
await context.workspaceState.update(key, undefined)
}
}
export async function resetGlobalState(context: vscode.ExtensionContext) {
// TODO: Reset all workspace states?
export async function resetExtensionState(context: vscode.ExtensionContext) {
for (const key of context.globalState.keys()) {
await context.globalState.update(key, undefined)
}
+16 -73
View File
@@ -429,18 +429,6 @@ export class Task {
vscode.window.showErrorMessage("Failed to restore offsetcheckpoint: " + errorMessage)
didWorkspaceRestoreFail = true
}
} else if (!offset && lastMessageWithHash.lastCheckpointHash && this.checkpointTracker) {
// Fallback: restore to most recent checkpoint when target message has no checkpoint hash
console.warn(`Message ${messageTs} has no checkpoint hash, falling back to previous checkpoint`)
try {
await this.checkpointTracker.resetHead(lastMessageWithHash.lastCheckpointHash)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error"
vscode.window.showErrorMessage("Failed to restore checkpoint: " + errorMessage)
didWorkspaceRestoreFail = true
}
} else {
vscode.window.showErrorMessage("Failed to restore checkpoint")
}
break
}
@@ -466,18 +454,6 @@ export class Task {
const deletedMessages = this.clineMessages.slice(messageIndex + 1)
const deletedApiReqsMetrics = getApiMetrics(combineApiRequests(combineCommandSequences(deletedMessages)))
// Detect files edited after this message timestamp for file context warning
// Only needed for task-only restores when a user edits a message or restores the task context, but not the files.
if (restoreType === "task") {
const filesEditedAfterMessage = await this.fileContextTracker.detectFilesEditedAfterMessage(
messageTs,
deletedMessages,
)
if (filesEditedAfterMessage.length > 0) {
await this.fileContextTracker.storePendingFileContextWarning(filesEditedAfterMessage)
}
}
const newClineMessages = this.clineMessages.slice(0, messageIndex + 1)
await this.overwriteClineMessages(newClineMessages) // calls saveClineMessages which saves historyItem
@@ -1155,17 +1131,12 @@ export class Task {
const wasRecent = lastClineMessage?.ts && Date.now() - lastClineMessage.ts < 30_000
// Check if there are pending file context warnings before calling taskResumption
const pendingContextWarning = await this.fileContextTracker.retrieveAndClearPendingFileContextWarning()
const hasPendingFileContextWarnings = pendingContextWarning && pendingContextWarning.length > 0
const [taskResumptionMessage, userResponseMessage] = formatResponse.taskResumption(
this.chatSettings?.mode === "plan" ? "plan" : "act",
agoText,
cwd,
wasRecent,
responseText,
hasPendingFileContextWarnings,
)
if (taskResumptionMessage !== "") {
@@ -1196,15 +1167,6 @@ export class Task {
}
}
// Inject file context warning if there were pending warnings from message editing
if (pendingContextWarning && pendingContextWarning.length > 0) {
const fileContextWarning = formatResponse.fileContextWarning(pendingContextWarning)
newUserContent.push({
type: "text",
text: fileContextWarning,
})
}
await this.overwriteApiConversationHistory(modifiedApiConversationHistory)
await this.initiateTaskLoop(newUserContent)
}
@@ -1274,43 +1236,23 @@ export class Task {
return
}
// Initialize checkpoint tracker if it doesn't exist
if (!this.checkpointTracker && !this.checkpointTrackerErrorMessage) {
try {
this.checkpointTracker = await CheckpointTracker.create(
// For non-attempt completion we just say checkpoints
await this.say("checkpoint_created")
this.checkpointTracker?.commit().then(async (commitHash) => {
const lastCheckpointMessage = findLast(this.clineMessages, (m) => m.say === "checkpoint_created")
if (lastCheckpointMessage) {
lastCheckpointMessage.lastCheckpointHash = commitHash
await saveClineMessagesAndUpdateHistory(
this.getContext(),
this.taskId,
this.context.globalStorageUri.fsPath,
this.enableCheckpoints,
this.clineMessages,
this.taskIsFavorited ?? false,
this.conversationHistoryDeletedRange,
this.checkpointTracker,
this.updateTaskHistory,
)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error"
console.error("Failed to initialize checkpoint tracker:", errorMessage)
this.checkpointTrackerErrorMessage = errorMessage
await this.postStateToWebview()
return
}
}
// Create a checkpoint commit and update clineMessages with a commitHash
if (this.checkpointTracker) {
const commitHash = await this.checkpointTracker.commit()
if (commitHash) {
await this.say("checkpoint_created")
const lastCheckpointMessage = findLast(this.clineMessages, (m) => m.say === "checkpoint_created")
if (lastCheckpointMessage) {
lastCheckpointMessage.lastCheckpointHash = commitHash
await saveClineMessagesAndUpdateHistory(
this.getContext(),
this.taskId,
this.clineMessages,
this.taskIsFavorited ?? false,
this.conversationHistoryDeletedRange,
this.checkpointTracker,
this.updateTaskHistory,
)
}
}
} // silently fails for now
}) // silently fails for now
//
} else {
@@ -1772,6 +1714,7 @@ export class Task {
clineIgnoreInstructions,
preferredLanguageInstructions,
)
console.log("[INSTRUCTIONS] User instructions:", userInstructions)
systemPrompt += userInstructions
}
const contextManagementMetadata = await this.contextManager.getNewContextMessagesAndMetadata(
@@ -4473,8 +4416,8 @@ export class Task {
// Now, if it's the first request AND checkpoints are enabled AND tracker was successfully initialized,
// then say "checkpoint_created" and perform the commit.
if (isFirstRequest && this.enableCheckpoints && this.checkpointTracker) {
const commitHash = await this.checkpointTracker.commit() // Actual commit
await this.say("checkpoint_created") // Now this is conditional
const commitHash = await this.checkpointTracker.commit() // Actual commit
const lastCheckpointMessage = findLast(this.clineMessages, (m) => m.say === "checkpoint_created")
if (lastCheckpointMessage) {
lastCheckpointMessage.lastCheckpointHash = commitHash
-4
View File
@@ -25,7 +25,6 @@ import { sendAccountButtonClickedEvent } from "./core/controller/ui/subscribeToA
import { migratePlanActGlobalToWorkspaceStorage, migrateCustomInstructionsToGlobalRules } from "./core/storage/state"
import { sendFocusChatInputEvent } from "./core/controller/ui/subscribeToFocusChatInput"
import { FileContextTracker } from "./core/context/context-tracking/FileContextTracker"
/*
Built using https://github.com/microsoft/vscode-webview-ui-toolkit
@@ -53,9 +52,6 @@ export async function activate(context: vscode.ExtensionContext) {
// Migrate custom instructions to global Cline rules (one-time cleanup)
await migrateCustomInstructionsToGlobalRules(context)
// Clean up orphaned file context warnings (startup cleanup)
await FileContextTracker.cleanupOrphanedWarnings(context)
// Version checking for autoupdate notification
const currentVersion = context.extension.packageJSON.version
const previousVersion = context.globalState.get<string>("clineVersion")
+23
View File
@@ -0,0 +1,23 @@
import { StringRequest } from "@/shared/proto/common"
import { Uri } from "@/shared/proto/host/uri"
import { FileChangeEvent_ChangeType, SubscribeToFileRequest } from "@/shared/proto/host/watch"
const UriServiceClient = {
parse: function (_: StringRequest): Uri {
throw Error("Unimplemented")
},
}
const WatchServiceClient = {
subscribeToFile: function (
_r: SubscribeToFileRequest,
_h: {
onResponse?: (response: { type: FileChangeEvent_ChangeType }) => void | Promise<void>
onError?: (error: any) => void
onComplete?: () => void
},
) {
throw Error("Unimplemented")
},
}
export { UriServiceClient, WatchServiceClient }
+2 -1
View File
@@ -1,5 +1,6 @@
import * as VscodeClient from "./vscode/client/host-grpc-client"
import * as ExternalClient from "@/standalone/host-bridge-client-manager"
import * as ExternalClient from "./external/client/host-bridge-client"
const isHostBridgeExternal = process.env.HOST_BRIDGE_ADDRESS !== undefined && process.env.HOST_BRIDGE_ADDRESS !== "vscode"
const Client = isHostBridgeExternal ? ExternalClient : VscodeClient
@@ -33,11 +33,10 @@ export function createGrpcClient<T extends ProtoService>(service: T): GrpcClient
const grpcHandler = new GrpcHandler()
Object.values(service.methods).forEach((method) => {
// Use lowercase method name as the key in the client object
const methodKey = method.name.charAt(0).toLowerCase() + method.name.slice(1)
// Streaming method implementation
if (method.responseStream) {
// Use lowercase method name as the key in the client object
const methodKey = method.name.charAt(0).toLowerCase() + method.name.slice(1)
client[methodKey as keyof GrpcClientType<T>] = ((
request: any,
options: StreamingCallbacks<InstanceType<typeof method.responseType>>,
@@ -76,6 +75,7 @@ export function createGrpcClient<T extends ProtoService>(service: T): GrpcClient
}) as any
} else {
// Unary method implementation
const methodKey = method.name.charAt(0).toLowerCase() + method.name.slice(1)
client[methodKey as keyof GrpcClientType<T>] = ((request: any) => {
return new Promise(async (resolve, reject) => {
const requestId = uuidv4()
@@ -84,12 +84,15 @@ export function createGrpcClient<T extends ProtoService>(service: T): GrpcClient
const response = await grpcHandler.handleRequest(service.fullName, methodKey, request, requestId)
console.log(`[DEBUG] gRPC host resp to ${service.fullName}.${methodKey} req:${requestId}`)
// Check if the response is a function (streaming)
// Check if the response is a function (streaming) or an object (unary)
if (typeof response === "function") {
// This shouldn't happen for unary requests
throw new Error("Received streaming response for unary request")
} else if (response && response.message) {
resolve(response.message)
} else {
throw new Error("gRPC response didn't have a message")
}
resolve(response)
} catch (e) {
console.log(`[DEBUG] gRPC host ERR to ${service.fullName}.${methodKey} req:${requestId} err:${e}`)
reject(e)
@@ -98,5 +101,6 @@ export function createGrpcClient<T extends ProtoService>(service: T): GrpcClient
}) as any
}
})
return client
}
+96 -77
View File
@@ -1,4 +1,5 @@
import { HostServiceHandlerConfig, hostServiceHandlers } from "./host-grpc-service-config"
import { v4 as uuidv4 } from "uuid"
import { hostServiceHandlers } from "./host-grpc-service-config"
import { GrpcRequestRegistry } from "@core/controller/grpc-request-registry"
/**
@@ -19,13 +20,13 @@ export interface StreamingCallbacks<T = any> {
}
/**
* Handles gRPC requests for the host bridge.
* Handles gRPC requests from the webview
*/
export class GrpcHandler {
constructor() {}
/**
* Handle a gRPC request for the host bridge.
* Handle a gRPC request from the webview
* @param service The service name
* @param method The method name
* @param message The request message
@@ -36,71 +37,90 @@ export class GrpcHandler {
async handleRequest<T = any>(
service: string,
method: string,
request: any,
message: any,
requestId: string,
streamingCallbacks?: StreamingCallbacks<T>,
): Promise<any | (() => void)> {
if (!streamingCallbacks) {
return this.handleUnaryRequest(service, method, request)
}
): Promise<
| {
message?: any
error?: string
request_id: string
}
| (() => void)
> {
// If streaming callbacks are provided, handle as a streaming request
let completionCalled = false
if (streamingCallbacks) {
let completionCalled = false
// Create a response handler that will call the client's callbacks
const responseHandler: StreamingResponseHandler = async (response, isLast = false, sequenceNumber) => {
try {
// Call the client's onResponse callback with the response
streamingCallbacks.onResponse(response)
// Create a response handler that will call the client's callbacks
const responseHandler: StreamingResponseHandler = async (response, isLast = false, sequenceNumber) => {
try {
// Call the client's onResponse callback with the response
streamingCallbacks.onResponse(response)
// If this is the last response, call the onComplete callback
if (isLast && streamingCallbacks.onComplete && !completionCalled) {
completionCalled = true
streamingCallbacks.onComplete()
// If this is the last response, call the onComplete callback
if (isLast && streamingCallbacks.onComplete && !completionCalled) {
completionCalled = true
streamingCallbacks.onComplete()
}
} catch (error) {
// If there's an error in the callback, call the onError callback
if (streamingCallbacks.onError) {
streamingCallbacks.onError(error instanceof Error ? error : new Error(String(error)))
}
}
}
// Register the response handler with the registry
requestRegistry.registerRequest(
requestId,
() => {
console.log(`[DEBUG] Cleaning up streaming request: ${requestId}`)
if (streamingCallbacks.onComplete && !completionCalled) {
completionCalled = true
streamingCallbacks.onComplete()
}
},
{ type: "streaming_request", service, method },
responseHandler,
)
// Call the streaming handler directly
console.log(`[DEBUG] Streaming gRPC host call to ${service}.${method} req:${requestId}`)
try {
await this.handleStreamingRequest(service, method, message, requestId)
} catch (error) {
// If there's an error in the callback, call the onError callback
if (streamingCallbacks.onError) {
streamingCallbacks.onError(error instanceof Error ? error : new Error(String(error)))
}
}
}
// Register the response handler with the registry
requestRegistry.registerRequest(
requestId,
() => {
console.log(`[DEBUG] Cleaning up streaming request: ${requestId}`)
if (streamingCallbacks.onComplete && !completionCalled) {
completionCalled = true
streamingCallbacks.onComplete()
}
},
{ type: "streaming_request", service, method },
responseHandler,
)
// Call the streaming handler directly
console.log(`[DEBUG] Streaming gRPC host call to ${service}.${method} req:${requestId}`)
try {
await this.handleStreamingRequest(service, method, request, requestId)
} catch (error) {
if (streamingCallbacks.onError) {
streamingCallbacks.onError(error instanceof Error ? error : new Error(String(error)))
// Return a function to cancel the stream
return () => {
console.log(`[DEBUG] Cancelling streaming request: ${requestId}`)
this.cancelRequest(requestId)
}
}
// Return a function to cancel the stream
return () => {
console.log(`[DEBUG] Cancelling streaming request: ${requestId}`)
this.cancelRequest(requestId)
}
}
// Handle as a unary request
try {
// Get the service handler from the config
const serviceConfig = hostServiceHandlers[service]
if (!serviceConfig) {
throw new Error(`Unknown service: ${service}`)
}
private async handleUnaryRequest(service: string, method: string, request: any): Promise<any> {
const serviceConfig = this.getServiceHandlerConfig(service)
const response = await serviceConfig.requestHandler(method, request)
return response
// Handle unary request
return {
message: await serviceConfig.requestHandler(method, message),
request_id: requestId,
}
} catch (error) {
return {
error: error instanceof Error ? error.message : String(error),
request_id: requestId,
}
}
}
/**
@@ -109,25 +129,27 @@ export class GrpcHandler {
* @returns True if the request was found and cancelled, false otherwise
*/
public async cancelRequest(requestId: string): Promise<boolean> {
const requestInfo = requestRegistry.getRequestInfo(requestId)
if (!requestInfo) {
return false
const cancelled = requestRegistry.cancelRequest(requestId)
if (cancelled) {
// Get the registered response handler from the registry
const requestInfo = requestRegistry.getRequestInfo(requestId)
if (requestInfo && requestInfo.responseStream) {
try {
// Send cancellation confirmation using the registered response handler
await requestInfo.responseStream(
{ cancelled: true },
true, // Mark as last message
)
} catch (e) {
console.error(`Error sending cancellation response for ${requestId}:`, e)
}
}
} else {
console.log(`[DEBUG] Request not found for cancellation: ${requestId}`)
}
const cancelled = requestRegistry.cancelRequest(requestId)
if (!cancelled) {
console.log(`[DEBUG] Request not found for cancellation: ${requestId}`)
return false
}
if (requestInfo.responseStream) {
try {
// Send cancellation confirmation using the registered response handler
await requestInfo.responseStream({ cancelled: true }, true /* isLast */)
} catch (e) {
console.error(`Error sending cancellation response for ${requestId}:`, e)
}
}
return true
return cancelled
}
/**
@@ -138,7 +160,11 @@ export class GrpcHandler {
* @param requestId The request ID for response correlation
*/
private async handleStreamingRequest(service: string, method: string, message: any, requestId: string): Promise<void> {
const serviceConfig = this.getServiceHandlerConfig(service)
// Get the service handler from the config
const serviceConfig = hostServiceHandlers[service]
if (!serviceConfig) {
throw new Error(`Unknown service: ${service}`)
}
// Check if the service supports streaming
if (!serviceConfig.streamingHandler) {
@@ -160,13 +186,6 @@ export class GrpcHandler {
// Don't send a final message here - the stream should stay open for future updates
// The stream will be closed when the client disconnects or when the service explicitly ends it
}
private getServiceHandlerConfig(serviceName: string): HostServiceHandlerConfig {
if (!(serviceName in hostServiceHandlers)) {
throw new Error(`Unknown service: ${serviceName}`)
}
return hostServiceHandlers[serviceName]
}
}
/**
@@ -83,7 +83,6 @@ function getBuildArtifactPatterns(): string[] {
".sass-cache/",
".vs/",
".vscode/",
".clinerules/",
"Pods/",
"__pycache__/",
"bin/",
-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
+2 -20
View File
@@ -156,16 +156,12 @@ export class TerminalManager {
}
runCommand(terminalInfo: TerminalInfo, command: string): TerminalProcessResultPromise {
console.log(`[TerminalManager] Running command on terminal ${terminalInfo.id}: "${command}"`)
console.log(`[TerminalManager] Terminal ${terminalInfo.id} busy state before: ${terminalInfo.busy}`)
terminalInfo.busy = true
terminalInfo.lastCommand = command
const process = new TerminalProcess()
this.processes.set(terminalInfo.id, process)
process.once("completed", () => {
console.log(`[TerminalManager] Terminal ${terminalInfo.id} completed, setting busy to false`)
terminalInfo.busy = false
})
@@ -229,12 +225,8 @@ export class TerminalManager {
this.defaultTerminalProfile !== "default" ? getShellForProfile(this.defaultTerminalProfile) : undefined
// Find available terminal from our pool first (created for this task)
console.log(`[TerminalManager] Looking for terminal in cwd: ${cwd}`)
console.log(`[TerminalManager] Available terminals: ${terminals.length}`)
const matchingTerminal = terminals.find((t) => {
if (t.busy) {
console.log(`[TerminalManager] Terminal ${t.id} is busy, skipping`)
return false
}
// Check if shell path matches current configuration
@@ -243,15 +235,11 @@ export class TerminalManager {
}
const terminalCwd = t.terminal.shellIntegration?.cwd // one of cline's commands could have changed the cwd of the terminal
if (!terminalCwd) {
console.log(`[TerminalManager] Terminal ${t.id} has no cwd, skipping`)
return false
}
const matches = arePathsEqual(vscode.Uri.file(cwd).fsPath, terminalCwd.fsPath)
console.log(`[TerminalManager] Terminal ${t.id} cwd: ${terminalCwd.fsPath}, matches: ${matches}`)
return matches
return arePathsEqual(vscode.Uri.file(cwd).fsPath, terminalCwd.fsPath)
})
if (matchingTerminal) {
console.log(`[TerminalManager] Found matching terminal ${matchingTerminal.id} in correct cwd`)
this.terminalIds.add(matchingTerminal.id)
return matchingTerminal
}
@@ -267,13 +255,7 @@ export class TerminalManager {
})
// Navigate back to the desired directory
const cdProcess = this.runCommand(availableTerminal, `cd "${cwd}"`)
// Wait for the cd command to complete before proceeding
await cdProcess
// Add a small delay to ensure terminal is ready after cd
await new Promise((resolve) => setTimeout(resolve, 100))
await this.runCommand(availableTerminal, `cd "${cwd}"`)
// Either resolve immediately if CWD already updated or wait for event/timeout
if (this.isCwdMatchingExpected(availableTerminal)) {
+4 -177
View File
@@ -23,84 +23,20 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
private lastRetrievedIndex: number = 0
isHot: boolean = false
private hotTimer: NodeJS.Timeout | null = null
private command: string = ""
private gracePeriodTimer: NodeJS.Timeout | null = null
private hasEmittedCompleted: boolean = false
// constructor() {
// super()
async run(terminal: vscode.Terminal, command: string) {
// Clear any existing grace period timer from previous commands
if (this.gracePeriodTimer) {
clearTimeout(this.gracePeriodTimer)
this.gracePeriodTimer = null
console.log(`[TerminalProcess] Cleared existing grace period timer before starting new command`)
}
// Clear any existing hot timer
if (this.hotTimer) {
clearTimeout(this.hotTimer)
this.hotTimer = null
}
// Reset state for new command
this.hasEmittedCompleted = false
this.buffer = ""
this.fullOutput = ""
this.lastRetrievedIndex = 0
this.isListening = true
this.isHot = false
this.command = command
console.log(`[TerminalProcess] Starting command: "${command}"`)
console.log(`[TerminalProcess] Shell integration available: ${!!terminal.shellIntegration?.executeCommand}`)
console.log(`[TerminalProcess] Terminal ID: ${terminal.name}`)
if (terminal.shellIntegration && terminal.shellIntegration.executeCommand) {
let execution
let stream
try {
execution = terminal.shellIntegration.executeCommand(command)
stream = execution.read()
} catch (error) {
console.error(`[TerminalProcess] Failed to execute command: ${error}`)
this.emit("error", error as Error)
return
}
const execution = terminal.shellIntegration.executeCommand(command)
const stream = execution.read()
// todo: need to handle errors
let isFirstChunk = true
let didOutputNonCommand = false
let didEmitEmptyLine = false
let receivedFirstChunk = false
// Set up a timeout to emit empty line if no output is received within 3 seconds
// This ensures the "proceed while running" button appears even for commands with no/delayed output
const firstChunkTimeout = setTimeout(() => {
if (!receivedFirstChunk && !didEmitEmptyLine) {
console.log(`[TerminalProcess] First chunk timeout fired - no output received within 3s for: "${command}"`)
this.emit("line", "") // empty line to show proceed button
didEmitEmptyLine = true
// Also emit a message indicating the command might be running without output
this.emit("line", "[Command is running but producing no output]")
}
}, 3000) // 3 second timeout
for await (let data of stream) {
// Clear the timeout since we received output
if (!receivedFirstChunk) {
clearTimeout(firstChunkTimeout)
receivedFirstChunk = true
console.log(`[TerminalProcess] First chunk received for command: "${command}"`)
}
// Log raw data length
console.log(`[TerminalProcess] Raw data chunk received: ${data.length} chars`)
if (!data || data.trim() === "") {
console.log(`[TerminalProcess] WARNING: Received empty or whitespace-only chunk`)
}
// 1. Process chunk and remove artifacts
if (isFirstChunk) {
/*
@@ -240,79 +176,14 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
this.emitRemainingBufferIfListening()
// Clean up the first chunk timeout if it's still active
if (!receivedFirstChunk) {
clearTimeout(firstChunkTimeout)
console.log(`[TerminalProcess] WARNING: Stream ended without receiving any chunks for command: "${command}"`)
// If we never received any chunks and haven't emitted anything yet, emit now
if (!didEmitEmptyLine) {
console.log(`[TerminalProcess] Emitting fallback empty line for no-output command`)
this.emit("line", "") // empty line to show proceed button
this.emit("line", "[Command completed with no output]")
didEmitEmptyLine = true
}
}
// for now we don't want this delaying requests since we don't send diagnostics automatically anymore (previous: "even though the command is finished, we still want to consider it 'hot' in case so that api request stalls to let diagnostics catch up")
if (this.hotTimer) {
clearTimeout(this.hotTimer)
}
this.isHot = false
console.log(`[TerminalProcess] Stream ended for command: "${command}"`)
console.log(`[TerminalProcess] Final output length: ${this.fullOutput.length} characters`)
// Check if this looks like a command that completed vs one that's still running
const quickCommands = ["cd ", "pwd", "ls ", "echo ", "mkdir ", "touch ", "rm ", "cp ", "mv "]
const isQuickCommand = quickCommands.some((cmd) => command.startsWith(cmd) || command.includes(" && " + cmd))
// Check if output suggests a long-running process
const longRunningIndicators = [
"listening on",
"server running",
"started on",
"watching for",
"compiled successfully",
"webpack",
"vite",
"nodemon",
"dev server",
"press ctrl",
"to quit",
"to exit",
"to stop",
]
const hasLongRunningOutput = longRunningIndicators.some((indicator) =>
this.fullOutput.toLowerCase().includes(indicator),
)
// Check if this is likely a command that starts a server or long-running process
const longRunningCommands = ["npm run", "npm start", "yarn", "node ", "python ", "serve", "dev", "watch"]
const isLongRunningCommand = longRunningCommands.some((cmd) => command.includes(cmd))
if (this.fullOutput.length === 0) {
console.log(`[TerminalProcess] WARNING: Process completed but no output was captured`)
// Ensure we emit at least one line for UI feedback
if (!didEmitEmptyLine) {
this.emit("line", "[Command completed silently]")
}
}
// Only skip grace period for truly quick commands that have no output or are known to complete instantly
if ((this.fullOutput.length === 0 || isQuickCommand) && !isLongRunningCommand && !hasLongRunningOutput) {
console.log(`[TerminalProcess] Command appears to have completed immediately, skipping grace period`)
this.emit("completed")
this.emit("continue")
} else {
console.log(
`[TerminalProcess] Command may still be running (longRunningCommand: ${isLongRunningCommand}, longRunningOutput: ${hasLongRunningOutput})`,
)
console.log(`[TerminalProcess] Starting grace period to detect true completion...`)
// Start grace period - wait 2.5 seconds to see if more output comes
// This prevents premature "proceed while running" for commands that clearly finished
this.startGracePeriod()
}
this.emit("completed")
this.emit("continue")
} else {
terminal.sendText(command, true)
// For terminals without shell integration, we can't know when the command completes
@@ -331,24 +202,14 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
private emitIfEol(chunk: string) {
this.buffer += chunk
let lineEndIndex: number
let lineCount = 0
while ((lineEndIndex = this.buffer.indexOf("\n")) !== -1) {
let line = this.buffer.slice(0, lineEndIndex).trimEnd() // removes trailing \r
// Remove \r if present (for Windows-style line endings)
// if (line.endsWith("\r")) {
// line = line.slice(0, -1)
// }
if (!line || line.trim() === "") {
console.log(`[TerminalProcess] Emitting empty line`)
} else {
console.log(`[TerminalProcess] Emitting line: ${line.substring(0, 100)}${line.length > 100 ? "..." : ""}`)
}
this.emit("line", line)
this.buffer = this.buffer.slice(lineEndIndex + 1)
lineCount++
}
if (lineCount === 0 && chunk.length > 0) {
console.log(`[TerminalProcess] Buffering partial line, buffer size: ${this.buffer.length}`)
}
}
@@ -363,41 +224,7 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
}
}
private startGracePeriod() {
// Clear any existing grace period timer
if (this.gracePeriodTimer) {
clearTimeout(this.gracePeriodTimer)
}
// Emit completed event for UI to show "proceed while running" button
console.log(`[TerminalProcess] Emitting completed event for UI (grace period active)`)
this.emit("completed")
// Wait 2.5 seconds to see if the command is truly finished
this.gracePeriodTimer = setTimeout(() => {
// Double-check the timer hasn't been cleared
if (this.gracePeriodTimer && !this.hasEmittedCompleted) {
console.log(`[TerminalProcess] Grace period completed - command appears truly finished: "${this.command}"`)
console.log(`[TerminalProcess] Auto-continuing without user intervention`)
this.hasEmittedCompleted = true
this.gracePeriodTimer = null
// Only emit continue after the grace period, not immediately
this.emit("continue")
}
}, 2500) // 2.5 second grace period
}
continue() {
console.log(`[TerminalProcess] Manual continue() called for: "${this.command}"`)
// Clear grace period since user manually continued
if (this.gracePeriodTimer) {
console.log(`[TerminalProcess] Clearing grace period timer due to manual continue`)
clearTimeout(this.gracePeriodTimer)
this.gracePeriodTimer = null
}
this.hasEmittedCompleted = true
this.emitRemainingBufferIfListening()
this.isListening = false
this.removeAllListeners("line")
-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
@@ -234,10 +234,6 @@ function convertApiProviderToProto(provider: string | undefined): ProtoApiProvid
return ProtoApiProvider.SAMBANOVA
case "cerebras":
return ProtoApiProvider.CEREBRAS
case "sapaicore":
return ProtoApiProvider.SAPAICORE
case "claude-code":
return ProtoApiProvider.CLAUDE_CODE
default:
return ProtoApiProvider.ANTHROPIC
}
@@ -294,10 +290,6 @@ function convertProtoToApiProvider(provider: ProtoApiProvider): ApiProvider {
return "sambanova"
case ProtoApiProvider.CEREBRAS:
return "cerebras"
case ProtoApiProvider.SAPAICORE:
return "sapaicore"
case ProtoApiProvider.CLAUDE_CODE:
return "claude-code"
default:
return "anthropic"
}
@@ -373,12 +365,6 @@ export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoA
requestTimeoutMs: config.requestTimeoutMs,
apiProvider: config.apiProvider ? convertApiProviderToProto(config.apiProvider) : undefined,
favoritedModelIds: config.favoritedModelIds || [],
sapAiCoreClientId: config.sapAiCoreClientId,
sapAiCoreClientSecret: config.sapAiCoreClientSecret,
sapAiResourceGroup: config.sapAiResourceGroup,
sapAiCoreTokenUrl: config.sapAiCoreTokenUrl,
sapAiCoreBaseUrl: config.sapAiCoreBaseUrl,
claudeCodePath: config.claudeCodePath,
}
}
@@ -452,11 +438,5 @@ export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguratio
requestTimeoutMs: protoConfig.requestTimeoutMs,
apiProvider: protoConfig.apiProvider !== undefined ? convertProtoToApiProvider(protoConfig.apiProvider) : undefined,
favoritedModelIds: protoConfig.favoritedModelIds.length > 0 ? protoConfig.favoritedModelIds : undefined,
sapAiCoreClientId: protoConfig.sapAiCoreClientId,
sapAiCoreClientSecret: protoConfig.sapAiCoreClientSecret,
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,51 +0,0 @@
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.
*/
class HostBridgeClientManager {
private static instance: HostBridgeClientManager | null
private channel: Channel
uriClient: host.UriServiceClient
//watchClient: host.WatchServiceClient
private constructor() {
const address = process.env.HOST_BRIDGE_ADDRESS || "localhost:50052"
this.channel = createChannel(address)
this.uriClient = createClient(host.UriServiceDefinition, this.channel)
//this.watchClient =
}
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
+3 -5
View File
@@ -7,12 +7,10 @@ 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 { addServices } from "./server-setup"
import { StreamingResponseHandler } from "@/core/controller/grpc-handler"
import { UriServiceClient } from "@/hosts/host-bridge-client"
import { StringRequest } from "@/shared/proto/common"
async function main() {
function main() {
log("Starting service...")
activate(extensionContext)
@@ -24,7 +22,7 @@ async function main() {
healthImpl.addToServer(server)
// Add all the handlers for the ProtoBus services to the server.
addProtobusServices(server, controller, wrapHandler, wrapStreamingResponseHandler)
addServices(server, controller, wrapHandler, wrapStreamingResponseHandler)
// Set up reflection.
const reflection = new ReflectionService(getPackageDefinition())
+2 -3
View File
@@ -24,13 +24,12 @@
"@/*": ["src/*"],
"@api/*": ["src/api/*"],
"@core/*": ["src/core/*"],
"@generated/*": ["src/generated/*"],
"@hosts/*": ["src/hosts/*"],
"@integrations/*": ["src/integrations/*"],
"@packages/*": ["src/packages/*"],
"@services/*": ["src/services/*"],
"@shared/*": ["src/shared/*"],
"@utils/*": ["src/utils/*"]
"@utils/*": ["src/utils/*"],
"@packages/*": ["src/packages/*"]
}
},
"include": ["src/**/*", "scripts/**/*"],
@@ -81,7 +81,6 @@ interface ChatTextAreaProps {
shouldDisableFilesAndImages: boolean
onHeightChange?: (height: number) => void
onFocusChange?: (isFocused: boolean) => void
isTaskView: boolean
}
interface GitCommit {
@@ -269,7 +268,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
shouldDisableFilesAndImages,
onHeightChange,
onFocusChange,
isTaskView,
},
ref,
) => {
@@ -1017,9 +1015,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
)
// Focus the textarea after mode toggle with slight delay
setTimeout(() => {
if (isTaskView) {
setInputValue("")
}
textAreaRef.current?.focus()
}, 100)
}, changeModeDelay)
+28 -9
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)
@@ -1286,7 +1306,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
scrollToBottomAuto()
}
}}
isTaskView={!!task}
/>
</div>
)
@@ -5,7 +5,7 @@ import { CheckpointRestoreRequest } from "@shared/proto/checkpoints"
import { Int64Request } from "@shared/proto/common"
import { ClineCheckpointRestore } from "@shared/WebviewMessage"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { useCallback, useEffect, useRef, useState } from "react"
import { useEffect, useRef, useState } from "react"
import { createPortal } from "react-dom"
import styled from "styled-components"
import { useExtensionState } from "@/context/ExtensionStateContext"
@@ -21,43 +21,11 @@ export const CheckmarkControl = ({ messageTs, isCheckpointCheckedOut }: Checkmar
const [restoreWorkspaceDisabled, setRestoreWorkspaceDisabled] = useState(false)
const [restoreBothDisabled, setRestoreBothDisabled] = useState(false)
const [showRestoreConfirm, setShowRestoreConfirm] = useState(false)
const [hasMouseEntered, setHasMouseEntered] = useState(false)
const containerRef = useRef<HTMLDivElement>(null)
const tooltipRef = useRef<HTMLDivElement>(null)
const { onRelinquishControl } = useExtensionState()
// Debounce
const closeMenuTimeoutRef = useRef<NodeJS.Timeout | null>(null)
const scheduleCloseRestore = useCallback(() => {
if (closeMenuTimeoutRef.current) {
clearTimeout(closeMenuTimeoutRef.current)
}
closeMenuTimeoutRef.current = setTimeout(() => {
setShowRestoreConfirm(false)
}, 350)
}, [])
const cancelCloseRestore = useCallback(() => {
if (closeMenuTimeoutRef.current) {
clearTimeout(closeMenuTimeoutRef.current)
closeMenuTimeoutRef.current = null
}
}, [])
// Debounce cleanup
useEffect(() => {
return () => {
if (closeMenuTimeoutRef.current) {
clearTimeout(closeMenuTimeoutRef.current)
closeMenuTimeoutRef.current = null
}
}
}, [showRestoreConfirm])
// Clear "Restore Files" button when checkpoint is no longer checked out
useEffect(() => {
if (!isCheckpointCheckedOut && restoreWorkspaceDisabled) {
setRestoreWorkspaceDisabled(false)
}
}, [isCheckpointCheckedOut, restoreWorkspaceDisabled])
const { refs, floatingStyles, update, placement } = useFloating({
placement: "bottom-end",
middleware: [
@@ -144,27 +112,38 @@ export const CheckmarkControl = ({ messageTs, isCheckpointCheckedOut }: Checkmar
}
const handleMouseEnter = () => {
cancelCloseRestore()
setHasMouseEntered(true)
}
const handleMouseLeave = () => {
scheduleCloseRestore()
if (hasMouseEntered) {
setShowRestoreConfirm(false)
setHasMouseEntered(false)
}
}
const handleControlsMouseEnter = () => {
cancelCloseRestore()
}
const handleControlsMouseLeave = (e: React.MouseEvent) => {
const tooltipElement = tooltipRef.current
const handleControlsMouseLeave = () => {
scheduleCloseRestore()
if (tooltipElement && showRestoreConfirm) {
const tooltipRect = tooltipElement.getBoundingClientRect()
if (
e.clientY >= tooltipRect.top &&
e.clientY <= tooltipRect.bottom &&
e.clientX >= tooltipRect.left &&
e.clientX <= tooltipRect.right
) {
return
}
}
setShowRestoreConfirm(false)
setHasMouseEntered(false)
}
return (
<Container
isMenuOpen={showRestoreConfirm}
$isCheckedOut={isCheckpointCheckedOut}
onMouseEnter={handleControlsMouseEnter}
onMouseLeave={handleControlsMouseLeave}>
<Container isMenuOpen={showRestoreConfirm} $isCheckedOut={isCheckpointCheckedOut} onMouseLeave={handleControlsMouseLeave}>
<i
className="codicon codicon-bookmark"
style={{
@@ -217,9 +196,9 @@ export const CheckmarkControl = ({ messageTs, isCheckpointCheckedOut }: Checkmar
<RestoreOption>
<VSCodeButton
onClick={handleRestoreWorkspace}
disabled={restoreWorkspaceDisabled || isCheckpointCheckedOut}
disabled={restoreWorkspaceDisabled}
style={{
cursor: restoreWorkspaceDisabled || isCheckpointCheckedOut ? "wait" : "pointer",
cursor: restoreWorkspaceDisabled ? "wait" : "pointer",
width: "100%",
marginBottom: "10px",
}}>
@@ -339,9 +318,9 @@ const CustomButton = styled.button<{ disabled?: boolean; isActive?: boolean; $is
props.isActive || props.disabled
? "none"
: `linear-gradient(to right, ${props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)"} 50%, transparent 50%),
linear-gradient(to bottom, ${props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)"} 50%, transparent 50%),
linear-gradient(to right, ${props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)"} 50%, transparent 50%),
linear-gradient(to bottom, ${props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)"} 50%, transparent 50%)`};
linear-gradient(to bottom, ${props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)"} 50%, transparent 50%),
linear-gradient(to right, ${props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)"} 50%, transparent 50%),
linear-gradient(to bottom, ${props.$isCheckedOut ? "var(--vscode-textLink-foreground)" : "var(--vscode-descriptionForeground)"} 50%, transparent 50%)`};
background-size: ${(props) => (props.isActive || props.disabled ? "auto" : `4px 1px, 1px 4px, 4px 1px, 1px 4px`)};
background-repeat: repeat-x, repeat-y, repeat-x, repeat-y;
background-position:
@@ -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
@@ -7,7 +7,7 @@ import { validateApiConfiguration, validateModelId } from "@/utils/validate"
import { vscode } from "@/utils/vscode"
import { ExtensionMessage } from "@shared/ExtensionMessage"
import { EmptyRequest, StringRequest } from "@shared/proto/common"
import { PlanActMode, ResetStateRequest, TogglePlanActModeRequest, UpdateSettingsRequest } from "@shared/proto/state"
import { PlanActMode, TogglePlanActModeRequest, UpdateSettingsRequest } from "@shared/proto/state"
import { VSCodeButton, VSCodeCheckbox, VSCodeLink, VSCodeTextArea } from "@vscode/webview-ui-toolkit/react"
import { CheckCheck, FlaskConical, Info, LucideIcon, Settings, SquareMousePointer, SquareTerminal, Webhook } from "lucide-react"
import { memo, useCallback, useEffect, useRef, useState } from "react"
@@ -406,13 +406,9 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
useEvent("message", handleMessage)
const handleResetState = async (resetGlobalState?: boolean) => {
const handleResetState = async () => {
try {
await StateServiceClient.resetState(
ResetStateRequest.create({
global: resetGlobalState,
}),
)
await StateServiceClient.resetState(EmptyRequest.create({}))
} catch (error) {
console.error("Failed to reset state:", error)
}
@@ -713,16 +709,10 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
{renderSectionHeader("debug")}
<Section>
<VSCodeButton
onClick={() => handleResetState()}
onClick={handleResetState}
className="mt-[5px] w-auto"
style={{ backgroundColor: "var(--vscode-errorForeground)", color: "black" }}>
Reset Workspace State
</VSCodeButton>
<VSCodeButton
onClick={() => handleResetState(true)}
className="mt-[5px] w-auto"
style={{ backgroundColor: "var(--vscode-errorForeground)", color: "black" }}>
Reset Global State
Reset State
</VSCodeButton>
<p className="text-xs mt-[5px] text-[var(--vscode-descriptionForeground)]">
This will reset all global state and secret storage in the extension.