mirror of
https://github.com/cline/cline.git
synced 2026-09-06 20:41:02 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f146f5f179 |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Exclude clinerules from checkpoints
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Update developer reset to allow for resetting workspace settings.
|
||||
@@ -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
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Clear the chat input when the user changes mode within a task
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fixed race condition where clineAsk was undefined, leading to task restoration and other downstream issues
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix where clineMessages were not saving checkpoint commitHash on some messages
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Integrate Claude Code
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Better debounce on checkmark control menu
|
||||
+3
-2
@@ -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
|
||||
|
||||
Vendored
+2
-3
@@ -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"
|
||||
}
|
||||
|
||||
Generated
-100
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}.`)
|
||||
|
||||
@@ -26,7 +26,6 @@ import { XAIHandler } from "./providers/xai"
|
||||
import { SambanovaHandler } from "./providers/sambanova"
|
||||
import { CerebrasHandler } from "./providers/cerebras"
|
||||
import { SapAiCoreHandler } from "./providers/sapaicore"
|
||||
import { ClaudeCodeHandler } from "./providers/claude-code"
|
||||
|
||||
export interface ApiHandler {
|
||||
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream
|
||||
@@ -91,8 +90,6 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
|
||||
return new CerebrasHandler(options)
|
||||
case "sapaicore":
|
||||
return new SapAiCoreHandler(options)
|
||||
case "claude-code":
|
||||
return new ClaudeCodeHandler(options)
|
||||
default:
|
||||
return new AnthropicHandler(options)
|
||||
}
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
import type { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { claudeCodeDefaultModelId, ClaudeCodeModelId, claudeCodeModels, type ApiHandlerOptions } from "@/shared/api"
|
||||
import { type ApiHandler } from ".."
|
||||
import { ApiStreamUsageChunk, type ApiStream } from "../transform/stream"
|
||||
import { withRetry } from "../retry"
|
||||
import { runClaudeCode } from "@/integrations/claude-code/run"
|
||||
import { ClaudeCodeMessage } from "@/integrations/claude-code/types"
|
||||
|
||||
export class ClaudeCodeHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
}
|
||||
|
||||
@withRetry({
|
||||
maxRetries: 4,
|
||||
baseDelay: 2000,
|
||||
maxDelay: 15000,
|
||||
})
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const claudeProcess = runClaudeCode({
|
||||
systemPrompt,
|
||||
messages,
|
||||
path: this.options.claudeCodePath,
|
||||
modelId: this.getModel().id,
|
||||
})
|
||||
|
||||
const dataQueue: string[] = []
|
||||
let processError = null
|
||||
let errorOutput = ""
|
||||
let exitCode: number | null = null
|
||||
|
||||
claudeProcess.stdout.on("data", (data) => {
|
||||
const output = data.toString()
|
||||
const lines = output.split("\n").filter((line: string) => line.trim() !== "")
|
||||
|
||||
for (const line of lines) {
|
||||
dataQueue.push(line)
|
||||
}
|
||||
})
|
||||
|
||||
claudeProcess.stderr.on("data", (data) => {
|
||||
errorOutput += data.toString()
|
||||
})
|
||||
|
||||
claudeProcess.on("close", (code) => {
|
||||
exitCode = code
|
||||
})
|
||||
|
||||
claudeProcess.on("error", (error) => {
|
||||
processError = error
|
||||
})
|
||||
|
||||
// Usage is included with assistant messages,
|
||||
// but cost is included in the result chunk
|
||||
let usage: ApiStreamUsageChunk = {
|
||||
type: "usage",
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
cacheWriteTokens: 0,
|
||||
}
|
||||
|
||||
while (exitCode !== 0 || dataQueue.length > 0) {
|
||||
if (dataQueue.length === 0) {
|
||||
await new Promise((resolve) => setImmediate(resolve))
|
||||
}
|
||||
|
||||
if (exitCode !== null && exitCode !== 0) {
|
||||
throw new Error(
|
||||
`Claude Code process exited with code ${exitCode}.${errorOutput ? ` Error output: ${errorOutput.trim()}` : ""}`,
|
||||
)
|
||||
}
|
||||
|
||||
const data = dataQueue.shift()
|
||||
if (!data) {
|
||||
continue
|
||||
}
|
||||
|
||||
const chunk = this.attemptParseChunk(data)
|
||||
|
||||
if (!chunk) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: data || "",
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (chunk.type === "system" && chunk.subtype === "init") {
|
||||
continue
|
||||
}
|
||||
|
||||
if (chunk.type === "assistant" && "message" in chunk) {
|
||||
const message = chunk.message
|
||||
|
||||
if (message.stop_reason !== null && message.stop_reason !== "tool_use") {
|
||||
const errorMessage = message.content[0]?.text || `Claude Code stopped with reason: ${message.stop_reason}`
|
||||
|
||||
if (errorMessage.includes("Invalid model name")) {
|
||||
throw new Error(
|
||||
errorMessage +
|
||||
`\n\nAPI keys and subscription plans allow different models. Make sure the selected model is included in your plan.`,
|
||||
)
|
||||
}
|
||||
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
for (const content of message.content) {
|
||||
if (content.type === "text") {
|
||||
yield {
|
||||
type: "text",
|
||||
text: content.text,
|
||||
}
|
||||
} else {
|
||||
console.warn("Unsupported content type:", content.type)
|
||||
}
|
||||
}
|
||||
|
||||
usage.inputTokens += message.usage.input_tokens
|
||||
usage.outputTokens += message.usage.output_tokens
|
||||
usage.cacheReadTokens = (usage.cacheReadTokens || 0) + (message.usage.cache_read_input_tokens || 0)
|
||||
usage.cacheWriteTokens = (usage.cacheWriteTokens || 0) + (message.usage.cache_creation_input_tokens || 0)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (chunk.type === "result" && "result" in chunk) {
|
||||
usage.totalCost = chunk.cost_usd || 0
|
||||
|
||||
yield usage
|
||||
}
|
||||
|
||||
if (processError) {
|
||||
throw processError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getModel() {
|
||||
const modelId = this.options.apiModelId
|
||||
if (modelId && modelId in claudeCodeModels) {
|
||||
const id = modelId as ClaudeCodeModelId
|
||||
return { id, info: claudeCodeModels[id] }
|
||||
}
|
||||
|
||||
return {
|
||||
id: claudeCodeDefaultModelId,
|
||||
info: claudeCodeModels[claudeCodeDefaultModelId],
|
||||
}
|
||||
}
|
||||
|
||||
// TOOD: Validate instead of parsing
|
||||
private attemptParseChunk(data: string): ClaudeCodeMessage | null {
|
||||
try {
|
||||
return JSON.parse(data)
|
||||
} catch (error) {
|
||||
console.error("Error parsing chunk:", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -79,7 +79,6 @@ export type GlobalStateKey =
|
||||
| "sapAiCoreClientId"
|
||||
| "sapAiCoreClientSecret"
|
||||
| "sapAiCoreModelId"
|
||||
| "claudeCodePath"
|
||||
|
||||
export type LocalStateKey =
|
||||
| "localClineRulesToggles"
|
||||
|
||||
@@ -246,7 +246,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup,
|
||||
sapAiCoreModelId,
|
||||
claudeCodePath,
|
||||
] = await Promise.all([
|
||||
getGlobalState(context, "isNewUser") as Promise<boolean | undefined>,
|
||||
getSecret(context, "apiKey") as Promise<string | undefined>,
|
||||
@@ -319,7 +318,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
getGlobalState(context, "sapAiCoreTokenUrl") as Promise<string | undefined>,
|
||||
getGlobalState(context, "sapAiResourceGroup") as Promise<string | undefined>,
|
||||
getGlobalState(context, "sapAiCoreModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "claudeCodePath") as Promise<string | undefined>,
|
||||
])
|
||||
|
||||
const localClineRulesToggles = (await getWorkspaceState(context, "localClineRulesToggles")) as ClineRulesToggles
|
||||
@@ -439,7 +437,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
apiKey,
|
||||
openRouterApiKey,
|
||||
clineApiKey,
|
||||
claudeCodePath,
|
||||
awsAccessKey,
|
||||
awsSecretKey,
|
||||
awsSessionToken,
|
||||
@@ -621,7 +618,6 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup,
|
||||
sapAiCoreModelId,
|
||||
claudeCodePath,
|
||||
} = apiConfiguration
|
||||
// Workspace state updates
|
||||
await updateWorkspaceState(context, "apiProvider", apiProvider)
|
||||
@@ -676,7 +672,6 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
await updateGlobalState(context, "sapAiCoreTokenUrl", sapAiCoreTokenUrl)
|
||||
await updateGlobalState(context, "sapAiResourceGroup", sapAiResourceGroup)
|
||||
await updateGlobalState(context, "sapAiCoreModelId", sapAiCoreModelId)
|
||||
await updateGlobalState(context, "claudeCodePath", claudeCodePath)
|
||||
|
||||
// Secret updates
|
||||
await storeSecret(context, "apiKey", apiKey)
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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 }
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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/",
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import * as vscode from "vscode"
|
||||
import Anthropic from "@anthropic-ai/sdk"
|
||||
import { execa } from "execa"
|
||||
|
||||
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
|
||||
|
||||
export function runClaudeCode({
|
||||
systemPrompt,
|
||||
messages,
|
||||
path,
|
||||
modelId,
|
||||
}: {
|
||||
systemPrompt: string
|
||||
messages: Anthropic.Messages.MessageParam[]
|
||||
path?: string
|
||||
modelId?: string
|
||||
}) {
|
||||
const claudePath = path || "claude"
|
||||
|
||||
// TODO: Is it worh using sessions? Where do we store the session ID?
|
||||
const args = [
|
||||
"-p",
|
||||
JSON.stringify(messages),
|
||||
"--system-prompt",
|
||||
systemPrompt,
|
||||
"--verbose",
|
||||
"--output-format",
|
||||
"stream-json",
|
||||
// Cline will handle recursive calls
|
||||
"--max-turns",
|
||||
"1",
|
||||
]
|
||||
|
||||
if (modelId) {
|
||||
args.push("--model", modelId)
|
||||
}
|
||||
|
||||
return execa(claudePath, args, {
|
||||
stdin: "ignore",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
env: process.env,
|
||||
cwd,
|
||||
})
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
type InitMessage = {
|
||||
type: "system"
|
||||
subtype: "init"
|
||||
session_id: string
|
||||
tools: string[]
|
||||
mcp_servers: string[]
|
||||
}
|
||||
|
||||
type ClaudeCodeContent = {
|
||||
type: "text"
|
||||
text: string
|
||||
}
|
||||
|
||||
type AssistantMessage = {
|
||||
type: "assistant"
|
||||
message: {
|
||||
id: string
|
||||
type: "message"
|
||||
role: "assistant"
|
||||
model: string
|
||||
content: ClaudeCodeContent[]
|
||||
stop_reason: null
|
||||
stop_sequence: null
|
||||
usage: {
|
||||
input_tokens: number
|
||||
cache_creation_input_tokens?: number
|
||||
cache_read_input_tokens?: number
|
||||
output_tokens: number
|
||||
service_tier: "standard"
|
||||
}
|
||||
}
|
||||
session_id: string
|
||||
}
|
||||
|
||||
type ErrorMessage = {
|
||||
type: "error"
|
||||
}
|
||||
|
||||
type ResultMessage = {
|
||||
type: "result"
|
||||
subtype: "success"
|
||||
cost_usd: number
|
||||
is_error: boolean
|
||||
duration_ms: number
|
||||
duration_api_ms: number
|
||||
num_turns: number
|
||||
result: string
|
||||
total_cost: number
|
||||
session_id: string
|
||||
}
|
||||
|
||||
export type ClaudeCodeMessage = InitMessage | AssistantMessage | ErrorMessage | ResultMessage
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user