mirror of
https://github.com/cline/cline.git
synced 2026-09-02 15:52:29 +08:00
Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2e11e07411 | |||
| 32bdc3884e | |||
| 4f35a5975a | |||
| 17fd4ceb7f | |||
| a231ee4d5c | |||
| c389507c7b | |||
| 531a272a63 | |||
| 6b9ac49d0a | |||
| 6d42a3fe0c | |||
| d291c11850 | |||
| 57fa7678c1 | |||
| 5c9c37a075 | |||
| edf18b318d | |||
| d08c394f68 | |||
| c0ca9889ff | |||
| fc19697fad | |||
| f64e8ddbf7 | |||
| 70d1dce34f | |||
| 950142fdfb | |||
| 41765fc29a | |||
| e5f8c048ce | |||
| 83de7bb2f1 | |||
| 6c842c145f | |||
| 09adaca575 | |||
| 0b1a237290 | |||
| cb1bda9b5c | |||
| 5dfd16359d | |||
| 64a8096f96 | |||
| e5a1d11179 | |||
| 5af6e8d5ed | |||
| 6ecadfc7a1 | |||
| eb3158c83d | |||
| 625b701a68 | |||
| dd59acf0fb | |||
| 335d0ffa05 | |||
| 3f341d9cf7 | |||
| 0adb1046e4 | |||
| 324564af72 | |||
| 291287c8e4 | |||
| d69fb10cfd | |||
| 0f6eab2bfe | |||
| 016ac1eade | |||
| 2606ed7abf | |||
| 3ed4d55a36 | |||
| a3b4ad53dc | |||
| bd98955f57 | |||
| 297cfbb895 | |||
| b4c95fbeab | |||
| 5e8102e588 | |||
| 60f2c3a22b | |||
| 7472b1cb4a | |||
| 794d8bccd3 | |||
| 0db3cfe34d | |||
| 5c878b5b84 | |||
| e3bc5f0143 | |||
| f2dbab814e |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Reset account view on sign out
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Adding telemetry for terminal command issue
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Sync taskHistory across Cline instances so that it will update for all Cline instances when one instance creates, adds to, or deletes a task.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Adding a grpc recording class with its builder
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fixed minor bug where entering slash commands would result in the following word being removed from the chat text area
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix issue where editor panel gets reset to chat view on each chat input focus event
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Exclude dependency folders for deep-planning prompt
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Sambanova provider: add DeepSeek-V3.1
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
fetch deployment id in design-time instead of runtime
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
add free grok-coder-free model to cline provider
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Adding a new standalone core api server script for local and integration testing purposes
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Setup Raw Structure for implementing multi-workspace support with WorkspaceRoot
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Reasoning Effort Minimal on OpenAI
|
||||
@@ -1,64 +1,69 @@
|
||||
name: 🐛 Bug Report
|
||||
description: File a bug report
|
||||
labels: ["bug"]
|
||||
labels: ['bug']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Important:** All bug reports must be reproducible using Claude 4 Sonnet. Cline uses complex prompts so less capable models may not work as expected.
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: Also tell us, what did you expect to happen?
|
||||
placeholder: Tell us what you see!
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: How do you trigger this bug? Please walk us through it step by step.
|
||||
value: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant API REQUEST output
|
||||
description: Please copy and paste any relevant output. This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
||||
- type: input
|
||||
id: provider-model
|
||||
attributes:
|
||||
label: Provider/Model
|
||||
description: What provider and model were you using when the issue occurred?
|
||||
placeholder: "e.g., cline:anthropic/claude-3.7-sonnet, gemini:gemini-2.5-pro-exp-03-25"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: system-info
|
||||
attributes:
|
||||
label: System Information
|
||||
description: What operating system and hardware are you using?
|
||||
placeholder: |
|
||||
Operating System: Windows 11, macOS Sonoma, Ubuntu 22.04, etc.
|
||||
Hardware: CPU, GPU, RAM specifications if relevant
|
||||
e.g.,
|
||||
OS: Windows 11
|
||||
CPU: Intel Core i7-11700K
|
||||
GPU: NVIDIA GeForce RTX 3070
|
||||
RAM: 32GB DDR4
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: cline-version
|
||||
attributes:
|
||||
label: Cline Version
|
||||
description: What version of Cline are you using? (You can find this at the bottom of the Settings view)
|
||||
placeholder: "e.g., 1.2.3"
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Important:** All bug reports must be reproducible using Claude Sonnet 4. Cline uses complex prompts so less capable models may not work as expected.
|
||||
- type: dropdown
|
||||
id: plugin-type
|
||||
attributes:
|
||||
label: Plugin Type
|
||||
description: Which plugin are you reporting a bug for?
|
||||
options:
|
||||
- VSCode Extension
|
||||
- JetBrains Plugin
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: cline-version
|
||||
attributes:
|
||||
label: Cline Version
|
||||
description: What version of Cline are you using? (You can find this at the bottom of the Settings view)
|
||||
placeholder: 'e.g., 1.2.3'
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: Also tell us, what did you expect to happen?
|
||||
placeholder: Tell us what you see!
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: How do you trigger this bug? Please walk us through it step by step.
|
||||
value: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: provider-model
|
||||
attributes:
|
||||
label: Provider/Model
|
||||
description: What provider and model were you using when the issue occurred?
|
||||
placeholder: 'e.g., cline:anthropic/claude-sonnet-4, gemini:gemini-2.5-pro-exp-03-25'
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: system-info
|
||||
attributes:
|
||||
label: System Information
|
||||
description: What operating system and hardware are you using?
|
||||
placeholder: |
|
||||
Operating System: Windows 11, macOS Sonoma, Ubuntu 22.04, etc.
|
||||
Hardware: CPU, GPU, RAM specifications if relevant
|
||||
e.g.,
|
||||
OS: Windows 11
|
||||
CPU: Intel Core i7-11700K
|
||||
GPU: NVIDIA GeForce RTX 3070
|
||||
RAM: 32GB DDR4
|
||||
validations:
|
||||
required: false
|
||||
|
||||
Vendored
+51
-7
@@ -91,7 +91,7 @@
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Run cline-core service",
|
||||
"name": "Debug Test Standalone Core Api Server (test:sca-server)",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
@@ -100,18 +100,62 @@
|
||||
"${workspaceFolder}/**",
|
||||
"!**/node_modules/**"
|
||||
],
|
||||
"cwd": "${workspaceFolder}/dist-standalone",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/dist/**/*.js",
|
||||
"${workspaceFolder}/dist-standalone/**/*.js"
|
||||
],
|
||||
"preLaunchTask": "compile-standalone",
|
||||
"runtimeExecutable": "npx",
|
||||
"runtimeArgs": [
|
||||
"tsx"
|
||||
],
|
||||
"program": "scripts/test-standalone-core-api-server.ts",
|
||||
"env": {
|
||||
// Turns on grpc debug log.
|
||||
//"GRPC_TRACE": "all",
|
||||
//"GRPC_VERBOSITY": "DEBUG",
|
||||
"NODE_PATH": "${workspaceFolder}/dist-standalone/node_modules"
|
||||
"PROTOBUS_PORT": "26040",
|
||||
"HOSTBRIDGE_PORT": "26041",
|
||||
"WORKSPACE_DIR": "${workspaceFolder}",
|
||||
"E2E_TEST": "true",
|
||||
"CLINE_ENVIRONMENT": "local"
|
||||
},
|
||||
"program": "cline-core.js"
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Debug Current Test File",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"sourceMaps": true,
|
||||
"resolveSourceMapLocations": [
|
||||
"${workspaceFolder}/**",
|
||||
"!**/node_modules/**"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"runtimeExecutable": "npx",
|
||||
"runtimeArgs": [
|
||||
"mocha"
|
||||
],
|
||||
"args": [
|
||||
"--require",
|
||||
"ts-node/register",
|
||||
"--require",
|
||||
"source-map-support/register",
|
||||
"--require",
|
||||
"./src/test/requires.ts",
|
||||
"--exit",
|
||||
"${file}"
|
||||
],
|
||||
"env": {
|
||||
"TS_NODE_PROJECT": "./tsconfig.unit-test.json",
|
||||
"NODE_ENV": "test",
|
||||
"IS_DEV": "true",
|
||||
"CLINE_ENVIRONMENT": "local"
|
||||
},
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "openOnSessionStart"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+1
-12
@@ -21,16 +21,5 @@
|
||||
},
|
||||
// Enable Lint and format using Biome
|
||||
"biome.enabled": true,
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
}
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# Changelog
|
||||
|
||||
## [3.27.1]
|
||||
|
||||
- Add new Kimi K2 model to groq and moonshot providers
|
||||
|
||||
## [3.27.0]
|
||||
|
||||
- Fix `grok-code-fast-1` model information
|
||||
|
||||
+7
-1
@@ -135,7 +135,12 @@
|
||||
"**",
|
||||
"!**/hosts/vscode/**",
|
||||
"!**/test/**",
|
||||
"!src/extension.ts"
|
||||
"!**/*.test.ts",
|
||||
"!src/dev/**",
|
||||
"!src/extension.ts",
|
||||
"!src/integrations/git/commit-message-generator.ts",
|
||||
"!src/integrations/terminal/**",
|
||||
"!src/core/controller/ui/openWalkthrough.ts"
|
||||
],
|
||||
"plugins": [
|
||||
"src/dev/grit/vscode-api.grit"
|
||||
@@ -148,6 +153,7 @@
|
||||
"!src/core/storage/FileContextTracker.ts",
|
||||
"!src/core/context/context-tracking/FileContextTracker.ts",
|
||||
"!src/common.ts",
|
||||
"!src/services/logging/distinctId.ts",
|
||||
"!src/core/storage/utils/state-helpers.ts",
|
||||
"!src/extension.ts"
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "Installing Cline for JetBrains"
|
||||
description: "Get Cline running in your favorite JetBrains IDE with the same powerful AI assistance you know from VSCode."
|
||||
title: "Installing Cline for JetBrains (Early Access)"
|
||||
description: "Get early access to Cline in your favorite JetBrains IDE with the same powerful AI assistance you know from VSCode."
|
||||
---
|
||||
|
||||
<Frame>
|
||||
@@ -20,16 +20,16 @@ Cline for JetBrains works almost identically to Cline in VSCode. All the core fe
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
<Note>Cline for JetBrains is currently in alpha. While all core features are functional, you may encounter occasional issues.</Note>
|
||||
<Note>Cline for JetBrains is in early access. All core features are functional, with ongoing improvements based on user feedback.</Note>
|
||||
|
||||
## Installation
|
||||
|
||||
Since Cline for JetBrains is currently in alpha, it's not yet available on the JetBrains Marketplace. You'll need to install it manually from a downloaded file:
|
||||
As part of our early access program, Cline for JetBrains is available through direct download before its official marketplace release. You'll need to install it manually from a downloaded file:
|
||||
|
||||
### Manual Installation from Disk
|
||||
|
||||
1. **Download the Plugin:**
|
||||
- Go to [https://plugins.jetbrains.com/plugin/28247-cline/versions/stable](https://plugins.jetbrains.com/plugin/28247-cline/versions/stable)
|
||||
- Go to [https://plugins.jetbrains.com/plugin/28247-cline/versions/eap](https://plugins.jetbrains.com/plugin/28247-cline/versions/eap)
|
||||
- Click **Download** to get the `.zip` file
|
||||
|
||||
<Frame>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// HubSpot Tracking Code for Cline Documentation
|
||||
(function() {
|
||||
// Check if HubSpot script is already loaded to prevent duplicates
|
||||
if (!document.getElementById('hs-script-loader')) {
|
||||
var script = document.createElement('script');
|
||||
script.type = 'text/javascript';
|
||||
script.id = 'hs-script-loader';
|
||||
script.async = true;
|
||||
script.src = 'https://js-na2.hs-scripts.com/243656267.js';
|
||||
|
||||
// Append the script to the document head
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
})();
|
||||
+2
-2
@@ -160,9 +160,9 @@ const standaloneConfig = {
|
||||
...baseConfig,
|
||||
entryPoints: ["src/standalone/cline-core.ts"],
|
||||
outfile: `${destDir}/cline-core.js`,
|
||||
// These gRPC protos need to load files from the module directory at runtime,
|
||||
// These modules need to load files from the module directory at runtime,
|
||||
// so they cannot be bundled.
|
||||
external: ["vscode", "@grpc/reflection", "grpc-health-check"],
|
||||
external: ["vscode", "@grpc/reflection", "grpc-health-check", "better-sqlite3"],
|
||||
}
|
||||
|
||||
// E2E build script configuration
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { ToolUseName, ToolParamName } from "../../src/core/assistant-message"
|
||||
import { ToolParamName } from "../../src/core/assistant-message"
|
||||
import { ClineDefaultTool } from "../../src/shared/tools"
|
||||
|
||||
export interface InputMessage {
|
||||
role: "user" | "assistant"
|
||||
@@ -88,7 +89,7 @@ export interface TestResult {
|
||||
}
|
||||
|
||||
export interface ExtractedToolCall {
|
||||
name: ToolUseName
|
||||
name: ClineDefaultTool
|
||||
input: Partial<Record<ToolParamName, string>>
|
||||
}
|
||||
|
||||
|
||||
Generated
+22
-30
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-dev",
|
||||
"version": "3.27.0",
|
||||
"version": "3.27.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-dev",
|
||||
"version": "3.27.0",
|
||||
"version": "3.27.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.37.0",
|
||||
@@ -86,6 +86,7 @@
|
||||
"@biomejs/biome": "^2.1.4",
|
||||
"@bufbuild/buf": "^1.54.0",
|
||||
"@changesets/cli": "^2.27.12",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/chai": "^5.0.1",
|
||||
"@types/clone-deep": "^4.0.4",
|
||||
"@types/diff": "^5.2.1",
|
||||
@@ -109,6 +110,7 @@
|
||||
"lint-staged": "^16.1.0",
|
||||
"minimatch": "^3.0.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prebuild-install": "^7.1.3",
|
||||
"protoc-gen-ts": "^0.8.7",
|
||||
"proxyquire": "^2.1.3",
|
||||
"rimraf": "^6.0.1",
|
||||
@@ -5090,6 +5092,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/better-sqlite3": {
|
||||
"version": "7.6.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz",
|
||||
"integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/chai": {
|
||||
"version": "5.0.1",
|
||||
"dev": true,
|
||||
@@ -7113,7 +7125,6 @@
|
||||
"version": "6.0.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"mimic-response": "^3.1.0"
|
||||
},
|
||||
@@ -7128,7 +7139,6 @@
|
||||
"version": "3.1.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -7151,7 +7161,6 @@
|
||||
"version": "0.6.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
@@ -8105,7 +8114,6 @@
|
||||
"version": "2.0.3",
|
||||
"dev": true,
|
||||
"license": "(MIT OR WTFPL)",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
@@ -8963,8 +8971,7 @@
|
||||
"node_modules/github-from-package": {
|
||||
"version": "0.0.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "10.4.3",
|
||||
@@ -9394,8 +9401,7 @@
|
||||
"node_modules/ini": {
|
||||
"version": "1.3.8",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"optional": true
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/internal-slot": {
|
||||
"version": "1.1.0",
|
||||
@@ -10975,8 +10981,7 @@
|
||||
"node_modules/mkdirp-classic": {
|
||||
"version": "0.5.3",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mocha": {
|
||||
"version": "10.6.0",
|
||||
@@ -11204,8 +11209,7 @@
|
||||
"node_modules/napi-build-utils": {
|
||||
"version": "2.0.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "1.0.0",
|
||||
@@ -11269,7 +11273,6 @@
|
||||
"version": "3.75.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"semver": "^7.3.5"
|
||||
},
|
||||
@@ -12411,9 +12414,10 @@
|
||||
},
|
||||
"node_modules/prebuild-install": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||||
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.0",
|
||||
"expand-template": "^2.0.3",
|
||||
@@ -12439,7 +12443,6 @@
|
||||
"version": "4.1.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"buffer": "^5.5.0",
|
||||
"inherits": "^2.0.4",
|
||||
@@ -12464,7 +12467,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
@@ -12473,14 +12475,12 @@
|
||||
"node_modules/prebuild-install/node_modules/chownr": {
|
||||
"version": "1.1.4",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"optional": true
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/prebuild-install/node_modules/detect-libc": {
|
||||
"version": "2.0.4",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -12489,7 +12489,6 @@
|
||||
"version": "3.6.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
@@ -12503,7 +12502,6 @@
|
||||
"version": "2.1.3",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"chownr": "^1.1.1",
|
||||
"mkdirp-classic": "^0.5.2",
|
||||
@@ -12515,7 +12513,6 @@
|
||||
"version": "2.2.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"bl": "^4.0.3",
|
||||
"end-of-stream": "^1.4.1",
|
||||
@@ -12745,7 +12742,6 @@
|
||||
"version": "1.2.8",
|
||||
"dev": true,
|
||||
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"deep-extend": "^0.6.0",
|
||||
"ini": "~1.3.0",
|
||||
@@ -12771,7 +12767,6 @@
|
||||
"version": "2.0.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -13652,8 +13647,7 @@
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/simple-get": {
|
||||
"version": "4.0.1",
|
||||
@@ -13673,7 +13667,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"decompress-response": "^6.0.0",
|
||||
"once": "^1.3.1",
|
||||
@@ -14567,7 +14560,6 @@
|
||||
"version": "0.6.0",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
},
|
||||
|
||||
+6
-3
@@ -2,7 +2,7 @@
|
||||
"name": "claude-dev",
|
||||
"displayName": "Cline",
|
||||
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
|
||||
"version": "3.27.0",
|
||||
"version": "3.27.1",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
@@ -355,6 +355,7 @@
|
||||
"test:integration": "vscode-test",
|
||||
"test:unit": "TS_NODE_PROJECT='./tsconfig.unit-test.json' mocha",
|
||||
"test:coverage": "vscode-test --coverage",
|
||||
"test:sca-server": "npx tsx watch scripts/test-standalone-core-api-server.ts",
|
||||
"e2e": "playwright test -c playwright.config.ts",
|
||||
"test:e2e": "playwright install && vsce package --no-dependencies --allow-package-secrets sendgrid --out dist/e2e.vsix && node src/test/e2e/utils/build.mjs && playwright test",
|
||||
"test:e2e:optimal": "vsce package --no-dependencies --allow-package-secrets sendgrid --out dist/e2e.vsix && node src/test/e2e/utils/build.mjs && playwright test",
|
||||
@@ -381,6 +382,7 @@
|
||||
"@biomejs/biome": "^2.1.4",
|
||||
"@bufbuild/buf": "^1.54.0",
|
||||
"@changesets/cli": "^2.27.12",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/chai": "^5.0.1",
|
||||
"@types/clone-deep": "^4.0.4",
|
||||
"@types/diff": "^5.2.1",
|
||||
@@ -404,6 +406,7 @@
|
||||
"lint-staged": "^16.1.0",
|
||||
"minimatch": "^3.0.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prebuild-install": "^7.1.3",
|
||||
"protoc-gen-ts": "^0.8.7",
|
||||
"proxyquire": "^2.1.3",
|
||||
"rimraf": "^6.0.1",
|
||||
@@ -419,8 +422,6 @@
|
||||
"@anthropic-ai/vertex-sdk": "^0.6.4",
|
||||
"@aws-sdk/client-bedrock-runtime": "^3.840.0",
|
||||
"@aws-sdk/credential-providers": "^3.840.0",
|
||||
"@sap-ai-sdk/ai-api": "^1.17.0",
|
||||
"@sap-ai-sdk/orchestration": "^1.17.0",
|
||||
"@bufbuild/protobuf": "^2.2.5",
|
||||
"@cerebras/cerebras_cloud_sdk": "^1.35.0",
|
||||
"@google-cloud/vertexai": "^1.9.3",
|
||||
@@ -436,6 +437,8 @@
|
||||
"@opentelemetry/sdk-trace-node": "^1.30.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.30.0",
|
||||
"@playwright/test": "^1.53.2",
|
||||
"@sap-ai-sdk/ai-api": "^1.17.0",
|
||||
"@sap-ai-sdk/orchestration": "^1.17.0",
|
||||
"@sentry/browser": "^9.12.0",
|
||||
"@streamparser/json": "^0.0.22",
|
||||
"@types/uuid": "^10.0.0",
|
||||
|
||||
@@ -2,6 +2,7 @@ syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
import "cline/common.proto";
|
||||
import "cline/state.proto";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
@@ -10,7 +11,6 @@ service BrowserService {
|
||||
rpc testBrowserConnection(StringRequest) returns (BrowserConnection);
|
||||
rpc discoverBrowser(EmptyRequest) returns (BrowserConnection);
|
||||
rpc getDetectedChromePath(EmptyRequest) returns (ChromePath);
|
||||
rpc updateBrowserSettings(UpdateBrowserSettingsRequest) returns (Boolean);
|
||||
rpc relaunchChromeDebugMode(EmptyRequest) returns (String);
|
||||
}
|
||||
|
||||
@@ -31,11 +31,6 @@ message ChromePath {
|
||||
bool is_bundled = 2;
|
||||
}
|
||||
|
||||
message Viewport {
|
||||
int32 width = 1;
|
||||
int32 height = 2;
|
||||
}
|
||||
|
||||
message BrowserSettings {
|
||||
Viewport viewport = 1;
|
||||
optional string remote_browser_host = 2;
|
||||
|
||||
@@ -8,19 +8,16 @@ message Metadata {
|
||||
}
|
||||
|
||||
message EmptyRequest {
|
||||
Metadata metadata = 1;
|
||||
}
|
||||
|
||||
message Empty {
|
||||
}
|
||||
|
||||
message StringRequest {
|
||||
Metadata metadata = 1;
|
||||
string value = 2;
|
||||
}
|
||||
|
||||
message StringArrayRequest {
|
||||
Metadata metadata = 1;
|
||||
repeated string value = 2;
|
||||
}
|
||||
|
||||
@@ -29,7 +26,6 @@ message String {
|
||||
}
|
||||
|
||||
message Int64Request {
|
||||
Metadata metadata = 1;
|
||||
int64 value = 2;
|
||||
}
|
||||
|
||||
@@ -38,7 +34,6 @@ message Int64 {
|
||||
}
|
||||
|
||||
message BytesRequest {
|
||||
Metadata metadata = 1;
|
||||
bytes value = 2;
|
||||
}
|
||||
|
||||
@@ -47,7 +42,6 @@ message Bytes {
|
||||
}
|
||||
|
||||
message BooleanRequest {
|
||||
Metadata metadata = 1;
|
||||
bool value = 2;
|
||||
}
|
||||
|
||||
|
||||
+32
-21
@@ -108,9 +108,16 @@ message SapAiCoreModelsRequest {
|
||||
string resource_group = 6;
|
||||
}
|
||||
|
||||
// SAP AI Core model with deployment information
|
||||
message SapAiCoreModelDeployment {
|
||||
string model_name = 1;
|
||||
string deployment_id = 2;
|
||||
}
|
||||
|
||||
|
||||
// Response for SAP AI Core models with orchestration availability
|
||||
message SapAiCoreModelsResponse {
|
||||
repeated string model_names = 1;
|
||||
repeated SapAiCoreModelDeployment deployments = 1;
|
||||
bool orchestration_available = 2;
|
||||
}
|
||||
|
||||
@@ -291,16 +298,18 @@ message ModelsApiConfiguration {
|
||||
optional string plan_mode_together_model_id = 117;
|
||||
optional string plan_mode_fireworks_model_id = 118;
|
||||
optional string plan_mode_sap_ai_core_model_id = 119;
|
||||
optional string plan_mode_groq_model_id = 120;
|
||||
optional OpenRouterModelInfo plan_mode_groq_model_info = 121;
|
||||
optional string plan_mode_hugging_face_model_id = 122;
|
||||
optional OpenRouterModelInfo plan_mode_hugging_face_model_info = 123;
|
||||
optional string plan_mode_huawei_cloud_maas_model_id = 124;
|
||||
optional OpenRouterModelInfo plan_mode_huawei_cloud_maas_model_info = 125;
|
||||
optional string plan_mode_baseten_model_id = 126;
|
||||
optional OpenRouterModelInfo plan_mode_baseten_model_info = 127;
|
||||
optional string plan_mode_vercel_ai_gateway_model_id = 128;
|
||||
optional OpenRouterModelInfo plan_mode_vercel_ai_gateway_model_info = 129;
|
||||
optional string plan_mode_sap_ai_core_deployment_id = 120;
|
||||
optional string plan_mode_groq_model_id = 121;
|
||||
optional OpenRouterModelInfo plan_mode_groq_model_info = 122;
|
||||
optional string plan_mode_hugging_face_model_id = 123;
|
||||
optional OpenRouterModelInfo plan_mode_hugging_face_model_info = 124;
|
||||
optional string plan_mode_huawei_cloud_maas_model_id = 125;
|
||||
optional OpenRouterModelInfo plan_mode_huawei_cloud_maas_model_info = 126;
|
||||
optional string plan_mode_baseten_model_id = 127;
|
||||
optional OpenRouterModelInfo plan_mode_baseten_model_info = 128;
|
||||
optional string plan_mode_vercel_ai_gateway_model_id = 129;
|
||||
optional OpenRouterModelInfo plan_mode_vercel_ai_gateway_model_info = 130;
|
||||
|
||||
|
||||
// Act mode configurations
|
||||
optional ApiProvider act_mode_api_provider = 200;
|
||||
@@ -323,16 +332,18 @@ message ModelsApiConfiguration {
|
||||
optional string act_mode_together_model_id = 217;
|
||||
optional string act_mode_fireworks_model_id = 218;
|
||||
optional string act_mode_sap_ai_core_model_id = 219;
|
||||
optional string act_mode_groq_model_id = 220;
|
||||
optional OpenRouterModelInfo act_mode_groq_model_info = 221;
|
||||
optional string act_mode_hugging_face_model_id = 222;
|
||||
optional OpenRouterModelInfo act_mode_hugging_face_model_info = 223;
|
||||
optional string act_mode_huawei_cloud_maas_model_id = 224;
|
||||
optional OpenRouterModelInfo act_mode_huawei_cloud_maas_model_info = 225;
|
||||
optional string act_mode_baseten_model_id = 226;
|
||||
optional OpenRouterModelInfo act_mode_baseten_model_info = 227;
|
||||
optional string act_mode_vercel_ai_gateway_model_id = 228;
|
||||
optional OpenRouterModelInfo act_mode_vercel_ai_gateway_model_info = 229;
|
||||
optional string act_mode_sap_ai_core_deployment_id = 220;
|
||||
optional string act_mode_groq_model_id = 221;
|
||||
optional OpenRouterModelInfo act_mode_groq_model_info = 222;
|
||||
optional string act_mode_hugging_face_model_id = 223;
|
||||
optional OpenRouterModelInfo act_mode_hugging_face_model_info = 224;
|
||||
optional string act_mode_huawei_cloud_maas_model_id = 225;
|
||||
optional OpenRouterModelInfo act_mode_huawei_cloud_maas_model_info = 226;
|
||||
optional string act_mode_baseten_model_id = 227;
|
||||
optional OpenRouterModelInfo act_mode_baseten_model_info = 228;
|
||||
optional string act_mode_vercel_ai_gateway_model_id = 229;
|
||||
optional OpenRouterModelInfo act_mode_vercel_ai_gateway_model_info = 230;
|
||||
|
||||
|
||||
repeated string favorited_model_ids = 300;
|
||||
}
|
||||
|
||||
+18
-1
@@ -8,7 +8,6 @@ service StateService {
|
||||
rpc getLatestState(EmptyRequest) returns (State);
|
||||
rpc updateTerminalConnectionTimeout(UpdateTerminalConnectionTimeoutRequest) returns (UpdateTerminalConnectionTimeoutResponse);
|
||||
rpc updateTerminalReuseEnabled(BooleanRequest) returns (Empty);
|
||||
rpc updateDefaultTerminalProfile(StringRequest) returns (TerminalProfileUpdateResponse);
|
||||
rpc getAvailableTerminalProfiles(EmptyRequest) returns (TerminalProfiles);
|
||||
rpc subscribeToState(EmptyRequest) returns (stream State);
|
||||
rpc toggleFavoriteModel(StringRequest) returns (Empty);
|
||||
@@ -56,6 +55,7 @@ enum OpenaiReasoningEffort {
|
||||
LOW = 0;
|
||||
MEDIUM = 1;
|
||||
HIGH = 2;
|
||||
MINIMAL = 3;
|
||||
}
|
||||
|
||||
enum McpDisplayMode {
|
||||
@@ -106,6 +106,16 @@ message TelemetrySettingRequest {
|
||||
TelemetrySettingEnum setting = 2;
|
||||
}
|
||||
|
||||
// Browser settings for UpdateSettingsRequest
|
||||
message BrowserSettingsUpdate {
|
||||
optional Viewport viewport = 1;
|
||||
optional string remote_browser_host = 2;
|
||||
optional bool remote_browser_enabled = 3;
|
||||
optional string chrome_executable_path = 4;
|
||||
optional bool disable_tool_use = 5;
|
||||
optional string custom_args = 6;
|
||||
}
|
||||
|
||||
// Message for updating settings
|
||||
message UpdateSettingsRequest {
|
||||
Metadata metadata = 1;
|
||||
@@ -126,6 +136,8 @@ message UpdateSettingsRequest {
|
||||
optional FocusChainSettings focus_chain_settings = 17;
|
||||
optional bool use_auto_condense = 18;
|
||||
optional string custom_prompt = 19;
|
||||
optional BrowserSettingsUpdate browser_settings = 20;
|
||||
optional string default_terminal_profile = 21;
|
||||
}
|
||||
|
||||
// Complete API Configuration message
|
||||
@@ -268,6 +280,11 @@ message FocusChainSettings {
|
||||
int32 remind_cline_interval = 2;
|
||||
}
|
||||
|
||||
message Viewport {
|
||||
int32 width = 1;
|
||||
int32 height = 2;
|
||||
}
|
||||
|
||||
message UpdateTerminalConnectionTimeoutResponse {
|
||||
optional int32 timeout_ms = 1;
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ package host;
|
||||
option java_package = "bot.cline.host.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "cline/common.proto";
|
||||
|
||||
// Provides methods for working with IDE windows and editors.
|
||||
service WindowService {
|
||||
// Opens a text document in the IDE editor and returns editor information.
|
||||
@@ -40,7 +38,6 @@ service WindowService {
|
||||
}
|
||||
|
||||
message ShowTextDocumentRequest {
|
||||
cline.Metadata metadata = 1;
|
||||
string path = 2;
|
||||
optional ShowTextDocumentOptions options = 3;
|
||||
}
|
||||
@@ -59,7 +56,6 @@ message TextEditorInfo {
|
||||
}
|
||||
|
||||
message ShowOpenDialogueRequest {
|
||||
cline.Metadata metadata = 1;
|
||||
optional bool can_select_many = 2;
|
||||
optional string open_label = 3;
|
||||
optional ShowOpenDialogueFilterOption filters = 4;
|
||||
|
||||
+1
-48
@@ -10,7 +10,6 @@ import * as path from "path"
|
||||
import { rmrf } from "./file-utils.mjs"
|
||||
import { main as generateHostBridgeClient } from "./generate-host-bridge-client.mjs"
|
||||
import { main as generateProtoBusSetup } from "./generate-protobus-setup.mjs"
|
||||
import { loadProtoDescriptorSet } from "./proto-utils.mjs"
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const PROTOC = path.join(require.resolve("grpc-tools"), "../bin/protoc")
|
||||
@@ -31,14 +30,13 @@ const TS_PROTO_OPTIONS = [
|
||||
"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.
|
||||
"useOptionals=none", // scalar and message fields are required unless they are marked as optional.
|
||||
"useDate=false", // Timestamp fields will not be automatically converted to Date.
|
||||
]
|
||||
|
||||
async function main() {
|
||||
await cleanup()
|
||||
await compileProtos()
|
||||
await checkProtos()
|
||||
await generateProtoBusSetup()
|
||||
await generateHostBridgeClient()
|
||||
}
|
||||
@@ -187,51 +185,6 @@ function checkAppleSiliconCompatibility() {
|
||||
}
|
||||
}
|
||||
|
||||
const int64TypeNames = ["TYPE_INT64", "TYPE_UINT64", "TYPE_SINT64", "TYPE_FIXED64", "TYPE_SFIXED64"]
|
||||
|
||||
async function checkProtos() {
|
||||
const proto = await loadProtoDescriptorSet()
|
||||
const int64Fields = []
|
||||
|
||||
for (const [packageName, packageDef] of Object.entries(proto)) {
|
||||
for (const [messageName, def] of Object.entries(packageDef)) {
|
||||
// Skip service definitions
|
||||
if (def && typeof def === "object" && "service" in def) {
|
||||
continue
|
||||
}
|
||||
// Check message fields
|
||||
if (def && def.type && def.type.field) {
|
||||
for (const field of def.type.field) {
|
||||
if (int64TypeNames.includes(field.type)) {
|
||||
const name = `${packageName}.${messageName}.${field.name}`
|
||||
int64Fields.push({
|
||||
name: name,
|
||||
type: field.type,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (int64Fields.length > 0) {
|
||||
console.log(chalk.yellow(`\nWarning: Found ${int64Fields.length} fields using 64-bit integer types`))
|
||||
for (const field of int64Fields) {
|
||||
const typeNames = {
|
||||
TYPE_INT64: "int64",
|
||||
TYPE_UINT64: "uint64",
|
||||
TYPE_SINT64: "sint64",
|
||||
TYPE_FIXED64: "fixed64",
|
||||
TYPE_SFIXED64: "sfixed64",
|
||||
}
|
||||
log_verbose(chalk.yellow(` - ${field.name} (${typeNames[field.type]})`))
|
||||
}
|
||||
log_verbose(chalk.yellow("\nWARNING: 64-bit integer fields detected in proto definitions"))
|
||||
log_verbose(chalk.yellow("JavaScript cannot safely represent integers larger than 2^53-1 (Number.MAX_SAFE_INTEGER)."))
|
||||
log_verbose(chalk.yellow("Consider using string representation for large numbers or implementing BigInt support.\n"))
|
||||
}
|
||||
}
|
||||
|
||||
function log_verbose(s) {
|
||||
if (process.argv.includes("-v") || process.argv.includes("--verbose")) {
|
||||
console.log(s)
|
||||
|
||||
@@ -6,40 +6,102 @@ import fs from "fs"
|
||||
import { cp } from "fs/promises"
|
||||
import { glob } from "glob"
|
||||
import minimatch from "minimatch"
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import { rmrf } from "./file-utils.mjs"
|
||||
|
||||
const BUILD_DIR = "dist-standalone"
|
||||
const BINARIES_DIR = `${BUILD_DIR}/binaries`
|
||||
const RUNTIME_DEPS_DIR = "standalone/runtime-files"
|
||||
const IS_DEBUG_BUILD = process.env.IS_DEBUG_BUILD === "true"
|
||||
|
||||
// This should match the node version packaged with the JetBrains plugin.
|
||||
const TARGET_NODE_VERSION = "22.15.0"
|
||||
const TARGET_PLATFORMS = [
|
||||
{ platform: "win32", arch: "x64", targetDir: "win-x64" },
|
||||
{ platform: "darwin", arch: "x64", targetDir: "darwin-x64" },
|
||||
{ platform: "darwin", arch: "arm64", targetDir: "darwin-arm64" },
|
||||
{ platform: "linux", arch: "x64", targetDir: "linux-x64" },
|
||||
]
|
||||
const SUPPORTED_BINARY_MODULES = ["better-sqlite3"]
|
||||
|
||||
const UNIVERSAL_BUILD = !process.argv.includes("-s")
|
||||
const IS_VERBOSE = process.argv.includes("-v") || process.argv.includes("--verbose")
|
||||
|
||||
async function main() {
|
||||
await installNodeDependencies()
|
||||
if (UNIVERSAL_BUILD) {
|
||||
console.log("Building universal package for all platforms...")
|
||||
await packageAllBinaryDeps()
|
||||
} else {
|
||||
console.log(`Building package for ${os.platform()}-${os.arch()}...`)
|
||||
}
|
||||
await zipDistribution()
|
||||
}
|
||||
|
||||
async function installNodeDependencies() {
|
||||
// Clean modules from any previous builds
|
||||
await rmrf(path.join(BUILD_DIR, "node_modules"))
|
||||
await rmrf(path.join(BINARIES_DIR))
|
||||
|
||||
await cpr(RUNTIME_DEPS_DIR, BUILD_DIR)
|
||||
|
||||
console.log("Running npm install in distribution directory...")
|
||||
const cwd = process.cwd()
|
||||
process.chdir(BUILD_DIR)
|
||||
execSync("npm install", { stdio: "inherit", cwd: BUILD_DIR })
|
||||
|
||||
try {
|
||||
execSync("npm install", { stdio: "inherit" })
|
||||
// Move the vscode directory into node_modules.
|
||||
// It can't be installed using npm because it will create a symlink which cannot be unzipped correctly on windows.
|
||||
fs.renameSync("vscode", path.join("node_modules", "vscode"))
|
||||
} catch (error) {
|
||||
console.error("Error during setup:", error)
|
||||
// Move the vscode directory into node_modules.
|
||||
// It can't be installed using npm because it will create a symlink which cannot be unzipped correctly on windows.
|
||||
fs.renameSync(`${BUILD_DIR}/vscode`, `${BUILD_DIR}/node_modules/vscode`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads prebuilt binaries for each platform for the modules that include binaries. It uses `npx prebuild-install`
|
||||
* to download the binary.
|
||||
*
|
||||
* The modules are downloaded to dist-standalone/binaries/{os}-{platform}/.
|
||||
* When cline-core is installed, the installer should use the correct module for the current platform.
|
||||
*/
|
||||
async function packageAllBinaryDeps() {
|
||||
// Check for native .node modules.
|
||||
const allNativeModules = await glob("**/*.node", { cwd: path.join(BUILD_DIR, "node_modules"), nodir: true })
|
||||
const isAllowed = (path) => SUPPORTED_BINARY_MODULES.some((allowed) => path.includes(allowed))
|
||||
const blocked = allNativeModules.filter((x) => !isAllowed(x))
|
||||
|
||||
if (blocked.length > 0) {
|
||||
console.error(`Error: Native node modules cannot be included in the standalone distribution:\n\n${blocked.join("\n")}`)
|
||||
console.error(
|
||||
"\nThese modules must support prebuilt-install and be added to the supported list in scripts/package-standalone.mjs",
|
||||
)
|
||||
process.exit(1)
|
||||
} finally {
|
||||
process.chdir(cwd)
|
||||
}
|
||||
|
||||
// Check for native .node modules.
|
||||
const nativeModules = await glob("**/*.node", { cwd: BUILD_DIR, nodir: true })
|
||||
if (nativeModules.length > 0) {
|
||||
console.error("Native node modules cannot be included in the standalone distribution:\n", nativeModules.join("\n"))
|
||||
process.exit(1)
|
||||
for (const module of SUPPORTED_BINARY_MODULES) {
|
||||
console.log(`Installing binaries for ${module}...`)
|
||||
const src = path.join(BUILD_DIR, "node_modules", module)
|
||||
if (!fs.existsSync(src)) {
|
||||
console.warn(`Warning: Trying to install binaries for the module '${module}', but it is not being used by cline.`)
|
||||
continue
|
||||
}
|
||||
|
||||
for (const { platform, arch, targetDir } of TARGET_PLATFORMS) {
|
||||
const binaryDir = `${BINARIES_DIR}/${targetDir}/node_modules`
|
||||
fs.mkdirSync(binaryDir, { recursive: true })
|
||||
|
||||
// Copy the module from the build dir
|
||||
const dest = path.join(binaryDir, module)
|
||||
await cpr(src, dest)
|
||||
|
||||
// Download the binary libs
|
||||
const v = IS_VERBOSE ? "--verbose" : ""
|
||||
const cmd = `npx prebuild-install --platform=${platform} --arch=${arch} --target=${TARGET_NODE_VERSION} ${v}`
|
||||
log_verbose(`${module}: ${cmd}`)
|
||||
execSync(cmd, { cwd: dest, stdio: "inherit" })
|
||||
log_verbose("")
|
||||
}
|
||||
// Remove the original module with the host platform binaries installed directly into node_modules.
|
||||
log_verbose(`Cleaning up host version of ${module}`)
|
||||
await rmrf(src)
|
||||
log_verbose("")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,10 +109,13 @@ async function zipDistribution() {
|
||||
// Zip the build directory (excluding any pre-existing output zip).
|
||||
const zipPath = path.join(BUILD_DIR, "standalone.zip")
|
||||
const output = fs.createWriteStream(zipPath)
|
||||
const archive = archiver("zip", { zlib: { level: 3 } })
|
||||
const startTime = Date.now()
|
||||
const archive = archiver("zip", { zlib: { level: 6 } })
|
||||
|
||||
output.on("close", () => {
|
||||
console.log(`Created ${zipPath} (${(archive.pointer() / 1024 / 1024).toFixed(1)} MB)`)
|
||||
const endTime = Date.now()
|
||||
const duration = (endTime - startTime) / 1000
|
||||
console.log(`Created ${zipPath} (${(archive.pointer() / 1024 / 1024).toFixed(1)} MB) in ${duration.toFixed(2)} seconds`)
|
||||
})
|
||||
archive.on("warning", (err) => {
|
||||
console.warn(`Warning: ${err}`)
|
||||
@@ -73,7 +138,7 @@ async function zipDistribution() {
|
||||
// Add the whole cline directory under "extension", except the for the ignored files.
|
||||
archive.directory(process.cwd(), "extension", (entry) => {
|
||||
if (isIgnored(entry.name)) {
|
||||
log_verbose("Ignoring", entry.name)
|
||||
//log_verbose("Ignoring", entry.name)
|
||||
return false
|
||||
}
|
||||
return entry
|
||||
@@ -145,7 +210,7 @@ function createIsIgnored(standaloneIgnores) {
|
||||
let allIgnore = [...defaultIgnore, ...expandedIgnore, ...standaloneIgnores]
|
||||
|
||||
// Map files need to be included in the debug build. Remove .map ignores when IS_DEBUG_BUILD is set
|
||||
if (process.env.IS_DEBUG_BUILD) {
|
||||
if (IS_DEBUG_BUILD) {
|
||||
allIgnore = allIgnore.filter((pattern) => !pattern.endsWith(".map"))
|
||||
console.log("Debug build: Including .map files in package")
|
||||
}
|
||||
@@ -167,6 +232,7 @@ function createIsIgnored(standaloneIgnores) {
|
||||
|
||||
/* cp -r */
|
||||
async function cpr(source, dest) {
|
||||
log_verbose(`Copying ${source} -> ${dest}`)
|
||||
await cp(source, dest, {
|
||||
recursive: true,
|
||||
preserveTimestamps: true,
|
||||
@@ -175,7 +241,7 @@ async function cpr(source, dest) {
|
||||
}
|
||||
|
||||
function log_verbose(...args) {
|
||||
if (process.argv.includes("-v") || process.argv.includes("--verbose")) {
|
||||
if (IS_VERBOSE) {
|
||||
console.log(...args)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
/**
|
||||
* Simple Cline gRPC Server
|
||||
*
|
||||
* This script provides a minimal way to run the Cline core gRPC service
|
||||
* without requiring the full installation, while automatically mocking all external services. Simply run:
|
||||
*
|
||||
* # One-time setup (generates protobuf files)
|
||||
* npm run compile-standalone
|
||||
* npm run test:sca-server
|
||||
*
|
||||
* The following components are started automatically:
|
||||
* 1. HostBridge test server
|
||||
* 2. ClineApiServerMock (mock implementation of the Cline API)
|
||||
* 3. AuthServiceMock (activated if E2E_TEST="true")
|
||||
*
|
||||
* Environment Variables for Customization:
|
||||
* PROJECT_ROOT - Override project root directory (default: parent of scripts dir)
|
||||
* CLINE_DIST_DIR - Override distribution directory (default: PROJECT_ROOT/dist-standalone)
|
||||
* CLINE_CORE_FILE - Override core file name (default: cline-core.js)
|
||||
* PROTOBUS_PORT - gRPC server port (default: 26040)
|
||||
* HOSTBRIDGE_PORT - HostBridge server port (default: 26041)
|
||||
* WORKSPACE_DIR - Working directory (default: current directory)
|
||||
* E2E_TEST - Enable E2E test mode (default: true)
|
||||
* CLINE_ENVIRONMENT - Environment setting (default: local)
|
||||
*
|
||||
* Ideal for local development, testing, or lightweight E2E scenarios.
|
||||
*/
|
||||
|
||||
import { ChildProcess, spawn } from "child_process"
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
import { ClineApiServerMock } from "../src/test/e2e/fixtures/server/index"
|
||||
|
||||
// Configuration
|
||||
const PROTOBUS_PORT = process.env.PROTOBUS_PORT || "26040"
|
||||
const HOSTBRIDGE_PORT = process.env.HOSTBRIDGE_PORT || "26041"
|
||||
const WORKSPACE_DIR = process.env.WORKSPACE_DIR || process.cwd()
|
||||
const E2E_TEST = process.env.E2E_TEST || "true"
|
||||
const CLINE_ENVIRONMENT = process.env.CLINE_ENVIRONMENT || "local"
|
||||
|
||||
// Locate the standalone build directory and core file with flexible path resolution
|
||||
const projectRoot = process.env.PROJECT_ROOT || path.resolve(__dirname, "..")
|
||||
const distDir = process.env.CLINE_DIST_DIR || path.join(projectRoot, "dist-standalone")
|
||||
const clineCoreFile = process.env.CLINE_CORE_FILE || "cline-core.js"
|
||||
const coreFile = path.join(distDir, clineCoreFile)
|
||||
|
||||
async function main(): Promise<void> {
|
||||
console.log("Starting Simple Cline gRPC Server...")
|
||||
console.log(`Workspace: ${WORKSPACE_DIR}`)
|
||||
console.log(`ProtoBus Port: ${PROTOBUS_PORT}`)
|
||||
console.log(`HostBridge Port: ${HOSTBRIDGE_PORT}`)
|
||||
|
||||
console.log(`Looking for standalone build at: ${coreFile}`)
|
||||
|
||||
if (!fs.existsSync(coreFile)) {
|
||||
console.error(`Standalone build not found at: ${coreFile}`)
|
||||
console.error("Available environment variables for customization:")
|
||||
console.error(" PROJECT_ROOT - Override project root directory")
|
||||
console.error(" CLINE_DIST_DIR - Override distribution directory")
|
||||
console.error(" CLINE_CORE_FILE - Override core file name")
|
||||
console.error("")
|
||||
console.error("To build the standalone version, run: npm run compile-standalone")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
try {
|
||||
const apiServer = await ClineApiServerMock.startGlobalServer()
|
||||
console.log("Cline API Server started in-process")
|
||||
} catch (error) {
|
||||
console.error("Failed to start Cline API Server:", error)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Start hostbridge test server in background.
|
||||
// We run it as a child process to emulate how the extension currently operates
|
||||
console.log("Starting HostBridge test server...")
|
||||
const hostbridge: ChildProcess = spawn("npx", ["tsx", path.join(__dirname, "test-hostbridge-server.ts")], {
|
||||
stdio: "pipe",
|
||||
detached: false,
|
||||
})
|
||||
|
||||
// Start the core service
|
||||
// We run it as a child process to emulate how the extension currently operates
|
||||
console.log("Starting Cline Core Service...")
|
||||
const coreService: ChildProcess = spawn("node", [clineCoreFile], {
|
||||
cwd: distDir,
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_PATH: "./node_modules",
|
||||
DEV_WORKSPACE_FOLDER: WORKSPACE_DIR,
|
||||
PROTOBUS_ADDRESS: `127.0.0.1:${PROTOBUS_PORT}`,
|
||||
HOST_BRIDGE_ADDRESS: `localhost:${HOSTBRIDGE_PORT}`,
|
||||
E2E_TEST: E2E_TEST,
|
||||
CLINE_ENVIRONMENT: CLINE_ENVIRONMENT,
|
||||
},
|
||||
stdio: "inherit",
|
||||
})
|
||||
|
||||
// Handle graceful shutdown
|
||||
const shutdown = async (): Promise<void> => {
|
||||
console.log("\n Shutting down services...")
|
||||
hostbridge.kill()
|
||||
coreService.kill()
|
||||
await ClineApiServerMock.stopGlobalServer()
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
process.on("SIGINT", shutdown)
|
||||
process.on("SIGTERM", shutdown)
|
||||
|
||||
coreService.on("exit", (code) => {
|
||||
console.log(`Core service exited with code ${code}`)
|
||||
hostbridge.kill()
|
||||
process.exit(code || 0)
|
||||
})
|
||||
|
||||
hostbridge.on("exit", (code) => {
|
||||
console.log(`HostBridge exited with code ${code}`)
|
||||
coreService.kill()
|
||||
process.exit(code || 0)
|
||||
})
|
||||
|
||||
console.log("Cline gRPC Server is running!")
|
||||
console.log(`Connect to: 127.0.0.1:${PROTOBUS_PORT}`)
|
||||
console.log("Press Ctrl+C to stop")
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main().catch((error) => {
|
||||
console.error("Failed to start simple Cline server:", error)
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
@@ -331,6 +331,7 @@ function createHandlerForProvider(
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
|
||||
deploymentId: mode === "plan" ? options.planModeSapAiCoreDeploymentId : options.actModeSapAiCoreDeploymentId,
|
||||
sapAiCoreUseOrchestrationMode: options.sapAiCoreUseOrchestrationMode,
|
||||
})
|
||||
case "claude-code":
|
||||
|
||||
@@ -32,6 +32,7 @@ export class ClineHandler implements ApiHandler {
|
||||
private client: OpenAI | undefined
|
||||
private readonly _baseUrl = clineEnvConfig.apiBaseUrl
|
||||
lastGenerationId?: string
|
||||
private lastRequestId?: string
|
||||
|
||||
constructor(options: ClineHandlerOptions) {
|
||||
this.options = options
|
||||
@@ -54,6 +55,31 @@ export class ClineHandler implements ApiHandler {
|
||||
"X-Task-ID": this.options.ulid || "",
|
||||
"X-Cline-Version": extensionVersion,
|
||||
},
|
||||
// Capture real HTTP request ID from initial streaming response headers
|
||||
fetch: async (...args: Parameters<typeof fetch>): Promise<Awaited<ReturnType<typeof fetch>>> => {
|
||||
const [input, init] = args
|
||||
const resp = await fetch(input, init)
|
||||
try {
|
||||
let urlStr = ""
|
||||
if (typeof input === "string") {
|
||||
urlStr = input
|
||||
} else if (input instanceof URL) {
|
||||
urlStr = input.toString()
|
||||
} else if (typeof (input as { url?: unknown }).url === "string") {
|
||||
urlStr = (input as { url: string }).url
|
||||
}
|
||||
// Only record for chat completions (the primary streaming request)
|
||||
if (urlStr.includes("/chat/completions")) {
|
||||
const rid = resp.headers.get("x-request-id") || resp.headers.get("request-id")
|
||||
if (rid) {
|
||||
this.lastRequestId = rid
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore header capture errors
|
||||
}
|
||||
return resp
|
||||
},
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw new Error(`Error creating Cline client: ${error.message}`)
|
||||
@@ -70,6 +96,7 @@ export class ClineHandler implements ApiHandler {
|
||||
const client = await this.ensureClient()
|
||||
|
||||
this.lastGenerationId = undefined
|
||||
this.lastRequestId = undefined
|
||||
|
||||
let didOutputUsage: boolean = false
|
||||
|
||||
@@ -92,6 +119,7 @@ export class ClineHandler implements ApiHandler {
|
||||
const metadataStr = error.metadata ? `\nMetadata: ${JSON.stringify(error.metadata, null, 2)}` : ""
|
||||
throw new Error(`Cline API Error ${error.code}: ${error.message}${metadataStr}`)
|
||||
}
|
||||
|
||||
if (!this.lastGenerationId && chunk.id) {
|
||||
this.lastGenerationId = chunk.id
|
||||
}
|
||||
@@ -203,6 +231,11 @@ export class ClineHandler implements ApiHandler {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Expose the last HTTP request ID captured from response headers (X-Request-ID)
|
||||
getLastRequestId(): string | undefined {
|
||||
return this.lastRequestId
|
||||
}
|
||||
|
||||
getModel(): { id: string; info: ModelInfo } {
|
||||
const modelId = this.options.openRouterModelId
|
||||
const modelInfo = this.options.openRouterModelInfo
|
||||
|
||||
@@ -21,6 +21,7 @@ interface SapAiCoreHandlerOptions extends CommonApiHandlerOptions {
|
||||
apiModelId?: string
|
||||
sapAiCoreUseOrchestrationMode?: boolean
|
||||
thinkingBudgetTokens?: number
|
||||
deploymentId?: string
|
||||
reasoningEffort?: string
|
||||
}
|
||||
|
||||
@@ -28,6 +29,7 @@ interface Deployment {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
interface Token {
|
||||
access_token: string
|
||||
expires_in: number
|
||||
@@ -394,11 +396,8 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
return this.token.access_token
|
||||
}
|
||||
|
||||
// TODO: these fallback fetching deployment id methods can be removed in future version if decided that users migration to fetching deployment id in design-time (open SAP AI Core provider UI) considered as completed.
|
||||
private async getAiCoreDeployments(): Promise<Deployment[]> {
|
||||
if (this.options.sapAiCoreClientSecret === "") {
|
||||
return [{ id: "notconfigured", name: "ai-core-not-configured" }]
|
||||
}
|
||||
|
||||
const token = await this.getToken()
|
||||
const headers = {
|
||||
Authorization: `Bearer ${token}`,
|
||||
@@ -546,7 +545,13 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
const model = this.getModel()
|
||||
const deploymentId = await this.getDeploymentForModel(model.id)
|
||||
let deploymentId = this.options.deploymentId
|
||||
|
||||
if (!deploymentId) {
|
||||
// Fallback to runtime deployment id fetching for users who haven't opened the SAP provider UI
|
||||
console.log(`No pre-configured deployment ID found for model ${model.id}, falling back to runtime fetching`)
|
||||
deploymentId = await this.getDeploymentForModel(model.id)
|
||||
}
|
||||
|
||||
const anthropicModels = [
|
||||
"anthropic--claude-4-sonnet",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ClineDefaultTool } from "@shared/tools"
|
||||
export type AssistantMessageContent = TextContent | ToolUse
|
||||
|
||||
export { parseAssistantMessageV2 } from "./parse-assistant-message"
|
||||
@@ -8,32 +9,6 @@ export interface TextContent {
|
||||
partial: boolean
|
||||
}
|
||||
|
||||
export const toolUseNames = [
|
||||
"execute_command",
|
||||
"read_file",
|
||||
"write_to_file",
|
||||
"replace_in_file",
|
||||
"search_files",
|
||||
"list_files",
|
||||
"list_code_definition_names",
|
||||
"browser_action",
|
||||
"use_mcp_tool",
|
||||
"access_mcp_resource",
|
||||
"ask_followup_question",
|
||||
"plan_mode_respond",
|
||||
"load_mcp_documentation",
|
||||
"attempt_completion",
|
||||
"new_task",
|
||||
"condense",
|
||||
"summarize_task",
|
||||
"report_bug",
|
||||
"new_rule",
|
||||
"web_fetch",
|
||||
] as const
|
||||
|
||||
// Converts array of tool call names into a union type ("execute_command" | "read_file" | ...)
|
||||
export type ToolUseName = (typeof toolUseNames)[number]
|
||||
|
||||
export const toolParamNames = [
|
||||
"command",
|
||||
"requires_approval",
|
||||
@@ -69,7 +44,7 @@ export type ToolParamName = (typeof toolParamNames)[number]
|
||||
|
||||
export interface ToolUse {
|
||||
type: "tool_use"
|
||||
name: ToolUseName
|
||||
name: ClineDefaultTool // id of the tool being used
|
||||
// params is a partial record, allowing only some or none of the possible parameters to be used
|
||||
params: Partial<Record<ToolParamName, string>>
|
||||
partial: boolean
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AssistantMessageContent, TextContent, ToolParamName, ToolUse, ToolUseName, toolParamNames, toolUseNames } from "." // Assuming types are defined in index.ts or a similar file
|
||||
import { ClineDefaultTool, toolUseNames } from "@shared/tools"
|
||||
import { AssistantMessageContent, TextContent, ToolParamName, ToolUse, toolParamNames } from "." // Assuming types are defined in index.ts or a similar file
|
||||
|
||||
// parseAssistantmessageV1 removed in https://github.com/cline/cline/pull/5425
|
||||
|
||||
@@ -33,7 +34,7 @@ export function parseAssistantMessageV2(assistantMessage: string): AssistantMess
|
||||
let currentParamName: ToolParamName | undefined
|
||||
|
||||
// Precompute tags for faster lookups
|
||||
const toolUseOpenTags = new Map<string, ToolUseName>()
|
||||
const toolUseOpenTags = new Map<string, ClineDefaultTool>()
|
||||
const toolParamOpenTags = new Map<string, ToolParamName>()
|
||||
for (const name of toolUseNames) {
|
||||
toolUseOpenTags.set(`<${name}>`, name)
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import { UpdateBrowserSettingsRequest } from "@shared/proto/cline/browser"
|
||||
import { Boolean } from "@shared/proto/cline/common"
|
||||
import { DEFAULT_BROWSER_SETTINGS, BrowserSettings as SharedBrowserSettings } from "../../../shared/BrowserSettings"
|
||||
import { Controller } from "../index"
|
||||
|
||||
/**
|
||||
* Update browser settings
|
||||
* @param controller The controller instance
|
||||
* @param request The browser settings request message
|
||||
* @returns Success response
|
||||
*/
|
||||
export async function updateBrowserSettings(controller: Controller, request: UpdateBrowserSettingsRequest): Promise<Boolean> {
|
||||
try {
|
||||
// Get current browser settings to preserve fields not in the request
|
||||
const currentSettings = controller.stateManager.getGlobalStateKey("browserSettings")
|
||||
const mergedWithDefaults = { ...DEFAULT_BROWSER_SETTINGS, ...currentSettings }
|
||||
|
||||
// Convert from protobuf format to shared format, merging with existing settings
|
||||
const newBrowserSettings: SharedBrowserSettings = {
|
||||
...mergedWithDefaults, // Start with existing settings (and defaults)
|
||||
viewport: {
|
||||
// Apply updates from request
|
||||
width: request.viewport?.width || mergedWithDefaults.viewport.width,
|
||||
height: request.viewport?.height || mergedWithDefaults.viewport.height,
|
||||
},
|
||||
// Explicitly handle optional boolean and string fields from the request
|
||||
remoteBrowserEnabled:
|
||||
request.remoteBrowserEnabled === undefined
|
||||
? mergedWithDefaults.remoteBrowserEnabled
|
||||
: request.remoteBrowserEnabled,
|
||||
remoteBrowserHost:
|
||||
request.remoteBrowserHost === undefined ? mergedWithDefaults.remoteBrowserHost : request.remoteBrowserHost,
|
||||
chromeExecutablePath:
|
||||
// If chromeExecutablePath is explicitly in the request (even as ""), use it.
|
||||
// Otherwise, fall back to mergedWithDefaults.
|
||||
"chromeExecutablePath" in request ? request.chromeExecutablePath : mergedWithDefaults.chromeExecutablePath,
|
||||
disableToolUse: request.disableToolUse === undefined ? mergedWithDefaults.disableToolUse : request.disableToolUse,
|
||||
customArgs: "customArgs" in request ? request.customArgs : mergedWithDefaults.customArgs,
|
||||
}
|
||||
|
||||
// Update global state with new settings
|
||||
controller.stateManager.setGlobalState("browserSettings", newBrowserSettings)
|
||||
|
||||
// Update task browser settings if task exists
|
||||
if (controller.task) {
|
||||
controller.task.browserSettings = newBrowserSettings
|
||||
controller.task.browserSession.browserSettings = newBrowserSettings
|
||||
}
|
||||
|
||||
// Post updated state to webview
|
||||
await controller.postStateToWebview()
|
||||
|
||||
return Boolean.create({
|
||||
value: true,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Error updating browser settings:", error)
|
||||
return Boolean.create({
|
||||
value: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { Controller } from ".."
|
||||
|
||||
export async function checkpointDiff(controller: Controller, request: Int64Request): Promise<Empty> {
|
||||
if (request.value) {
|
||||
await controller.task?.presentMultifileDiff(request.value, false)
|
||||
await controller.task?.checkpointManager?.presentMultifileDiff(request.value, false)
|
||||
}
|
||||
return Empty
|
||||
return Empty.create()
|
||||
}
|
||||
|
||||
@@ -23,7 +23,11 @@ export async function checkpointRestore(controller: Controller, request: Checkpo
|
||||
})
|
||||
|
||||
// NOTE: cancelTask awaits abortTask, which awaits diffViewProvider.revertChanges, which reverts any edited files, allowing us to reset to a checkpoint rather than running into a state where the revertChanges function is called alongside or after the checkpoint reset
|
||||
await controller.task?.restoreCheckpoint(request.number, request.restoreType as ClineCheckpointRestore, request.offset)
|
||||
await controller.task?.checkpointManager?.restoreCheckpoint(
|
||||
request.number,
|
||||
request.restoreType as ClineCheckpointRestore,
|
||||
request.offset,
|
||||
)
|
||||
}
|
||||
return Empty.create({})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { refreshClineRulesToggles } from "@core/context/instructions/user-instructions/cline-rules"
|
||||
import { createRuleFile as createRuleFileImpl } from "@core/context/instructions/user-instructions/rule-helpers"
|
||||
import { getWorkspaceBasename } from "@core/workspace"
|
||||
import { RuleFile, RuleFileRequest } from "@shared/proto/cline/file"
|
||||
import * as path from "path"
|
||||
import { refreshWorkflowToggles } from "@/core/context/instructions/user-instructions/workflows"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { ShowMessageType } from "@/shared/proto/host/window"
|
||||
@@ -68,7 +68,7 @@ export async function createRuleFile(controller: Controller, request: RuleFileRe
|
||||
|
||||
return RuleFile.create({
|
||||
filePath: filePath,
|
||||
displayName: path.basename(filePath),
|
||||
displayName: getWorkspaceBasename(filePath, "Controller.createRuleFile"),
|
||||
alreadyExists: fileExists,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { deleteRuleFile as deleteRuleFileImpl } from "@core/context/instructions/user-instructions/rule-helpers"
|
||||
import { getWorkspaceBasename } from "@core/workspace"
|
||||
import { RuleFile, RuleFileRequest } from "@shared/proto/cline/file"
|
||||
import * as path from "path"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { Controller } from ".."
|
||||
@@ -40,7 +40,7 @@ export async function deleteRuleFile(controller: Controller, request: RuleFileRe
|
||||
//await refreshWorkflowToggles(controller.context, cwd)
|
||||
await controller.postStateToWebview()
|
||||
|
||||
const fileName = path.basename(request.rulePath)
|
||||
const fileName = getWorkspaceBasename(request.rulePath, "Controller.deleteRuleFile")
|
||||
|
||||
const fileTypeName = request.type === "workflow" ? "workflow" : "rule"
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { workspaceResolver } from "@core/workspace"
|
||||
import { BooleanResponse, StringRequest } from "@shared/proto/cline/common"
|
||||
import { getWorkspacePath } from "@utils/path"
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
@@ -25,7 +25,12 @@ export async function ifFileExistsRelativePath(_controller: Controller, request:
|
||||
}
|
||||
|
||||
// Resolve the relative path to absolute path
|
||||
const absolutePath = path.resolve(workspacePath, request.value)
|
||||
const resolvedPath = workspaceResolver.resolveWorkspacePath(
|
||||
workspacePath,
|
||||
request.value,
|
||||
"Controller.ifFileExistsRelativePath",
|
||||
)
|
||||
const absolutePath = typeof resolvedPath === "string" ? resolvedPath : resolvedPath.absolutePath
|
||||
// Check if the file exists
|
||||
try {
|
||||
return BooleanResponse.create({ value: fs.statSync(absolutePath).isFile() })
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { workspaceResolver } from "@core/workspace"
|
||||
import { openFile as openFileIntegration } from "@integrations/misc/open-file"
|
||||
import { Empty, StringRequest } from "@shared/proto/cline/common"
|
||||
import { getWorkspacePath } from "@utils/path"
|
||||
import * as path from "path"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
@@ -20,7 +20,12 @@ export async function openFileRelativePath(_controller: Controller, request: Str
|
||||
|
||||
if (request.value) {
|
||||
// Resolve the relative path to absolute path
|
||||
const absolutePath = path.resolve(workspacePath, request.value)
|
||||
const resolvedPath = workspaceResolver.resolveWorkspacePath(
|
||||
workspacePath,
|
||||
request.value,
|
||||
"Controller.openFileRelativePath",
|
||||
)
|
||||
const absolutePath = typeof resolvedPath === "string" ? resolvedPath : resolvedPath.absolutePath
|
||||
|
||||
// Open the file using the existing integration
|
||||
openFileIntegration(absolutePath)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import path from "node:path"
|
||||
import { getWorkspaceBasename } from "@core/workspace"
|
||||
import type { ToggleClineRuleRequest } from "@shared/proto/cline/file"
|
||||
import { ToggleClineRules } from "@shared/proto/cline/file"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
@@ -36,14 +36,7 @@ export async function toggleClineRule(controller: Controller, request: ToggleCli
|
||||
// Track rule toggle telemetry with current task context
|
||||
if (controller.task?.ulid) {
|
||||
// Extract just the filename for privacy (no full paths)
|
||||
const ruleFileName = path.basename(rulePath)
|
||||
telemetryService.captureClineRuleToggled(controller.task.ulid, ruleFileName, enabled, isGlobal)
|
||||
}
|
||||
|
||||
// Track rule toggle telemetry with current task context
|
||||
if (controller.task?.ulid) {
|
||||
// Extract just the filename for privacy (no full paths)
|
||||
const ruleFileName = path.basename(rulePath)
|
||||
const ruleFileName = getWorkspaceBasename(rulePath, "Controller.toggleClineRule")
|
||||
telemetryService.captureClineRuleToggled(controller.task.ulid, ruleFileName, enabled, isGlobal)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { GrpcRecorder, GrpcRecorderNoops, IRecorder } from "@/core/controller/grpc-recorder/grpc-recorder"
|
||||
import { LogFileHandler, LogFileHandlerNoops } from "@/core/controller/grpc-recorder/log-file-handler"
|
||||
|
||||
/**
|
||||
* A builder class for constructing a gRPC recorder instance.
|
||||
*
|
||||
* This class follows the Builder pattern, allowing consumers
|
||||
* to configure logging behavior and control whether recording
|
||||
* is enabled or disabled before creating a final `IRecorder`.
|
||||
*/
|
||||
export class GrpcRecorderBuilder {
|
||||
private fileHandler: LogFileHandler | null = null
|
||||
private enabled: boolean = true
|
||||
|
||||
public withLogFileHandler(handler: LogFileHandler): this {
|
||||
this.fileHandler = handler
|
||||
return this
|
||||
}
|
||||
|
||||
public enableIf(condition: boolean): this {
|
||||
this.enabled = condition
|
||||
return this
|
||||
}
|
||||
|
||||
public build(): IRecorder {
|
||||
if (!this.enabled) {
|
||||
return new GrpcRecorderNoops()
|
||||
}
|
||||
|
||||
const handler = this.fileHandler ?? new LogFileHandlerNoops()
|
||||
return new GrpcRecorder(handler)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { GrpcRecorder, IRecorder } from "@core/controller/grpc-recorder/grpc-recorder"
|
||||
import { expect } from "chai"
|
||||
import { ExtensionMessage } from "@/shared/ExtensionMessage"
|
||||
import { GrpcRequest } from "@/shared/WebviewMessage"
|
||||
|
||||
describe("grpc-recorder", () => {
|
||||
let recorder: IRecorder
|
||||
|
||||
before(async () => {
|
||||
recorder = GrpcRecorder.builder().enableIf(true).build()
|
||||
})
|
||||
|
||||
describe("GrpcRecorder", () => {
|
||||
it("matches multiple request, response and stats", async () => {
|
||||
interface UseCase {
|
||||
request: GrpcRequest
|
||||
response: ExtensionMessage["grpc_response"]
|
||||
expectedStatus: string
|
||||
}
|
||||
const requestResponseUseCases: UseCase[] = [
|
||||
{
|
||||
request: {
|
||||
service: "the-service",
|
||||
method: "the-method",
|
||||
message: "the-message",
|
||||
request_id: "request-id-1",
|
||||
is_streaming: false,
|
||||
},
|
||||
response: {
|
||||
request_id: "request-id-1",
|
||||
message: "the-message-response",
|
||||
error: "",
|
||||
},
|
||||
expectedStatus: "completed",
|
||||
},
|
||||
{
|
||||
request: {
|
||||
service: "streaming-service",
|
||||
method: "stream-method",
|
||||
message: { data: "streaming-data", count: 42 },
|
||||
request_id: "request-id-2",
|
||||
is_streaming: true,
|
||||
},
|
||||
response: {
|
||||
request_id: "request-id-2",
|
||||
message: { streamData: "chunk-1" },
|
||||
error: "",
|
||||
is_streaming: true,
|
||||
sequence_number: 1,
|
||||
},
|
||||
expectedStatus: "completed",
|
||||
},
|
||||
{
|
||||
request: {
|
||||
service: "another-service",
|
||||
method: "another-method",
|
||||
message: { complex: { nested: "object", array: [1, 2, 3] } },
|
||||
request_id: "request-id-3",
|
||||
is_streaming: false,
|
||||
},
|
||||
response: {
|
||||
request_id: "request-id-3",
|
||||
message: "",
|
||||
error: "Something went wrong",
|
||||
},
|
||||
expectedStatus: "error",
|
||||
},
|
||||
]
|
||||
|
||||
const initialExpectedStatus = "pending"
|
||||
|
||||
requestResponseUseCases.forEach((us: UseCase, index: number) => {
|
||||
recorder.recordRequest(us.request)
|
||||
|
||||
let sessionLog = recorder.getSessionLog()
|
||||
expect(sessionLog.entries).length(index + 1)
|
||||
|
||||
expect(sessionLog.entries[index]).to.include({
|
||||
service: us.request.service,
|
||||
method: us.request.method,
|
||||
isStreaming: us.request.is_streaming,
|
||||
requestId: us.request.request_id,
|
||||
status: initialExpectedStatus,
|
||||
})
|
||||
|
||||
if (us.response) {
|
||||
recorder.recordResponse(us.request.request_id, us.response)
|
||||
}
|
||||
sessionLog = recorder.getSessionLog()
|
||||
|
||||
expect(sessionLog.entries[index].status).equal(us.expectedStatus)
|
||||
expect(sessionLog.entries[index].response).to.deep.include({
|
||||
error: us.response?.error,
|
||||
})
|
||||
})
|
||||
|
||||
const sessionLog = recorder.getSessionLog()
|
||||
expect(sessionLog.stats).to.include({
|
||||
totalRequests: 3,
|
||||
pendingRequests: 0,
|
||||
completedRequests: 2,
|
||||
errorRequests: 1,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,179 @@
|
||||
import { GrpcResponse } from "@shared/ExtensionMessage"
|
||||
import { GrpcRequest } from "@shared/WebviewMessage"
|
||||
import { GrpcRecorderBuilder } from "@/core/controller/grpc-recorder/grpc-recorder.builder"
|
||||
import { ILogFileHandler } from "@/core/controller/grpc-recorder/log-file-handler"
|
||||
import { GrpcLogEntry, GrpcSessionLog, SessionStats } from "@/core/controller/grpc-recorder/types"
|
||||
|
||||
export class GrpcRecorderNoops implements IRecorder {
|
||||
recordRequest(_request: GrpcRequest): void {}
|
||||
recordResponse(_requestId: string, _response: GrpcResponse): void {}
|
||||
recordError(_requestId: string, _error: string): void {}
|
||||
getSessionLog(): GrpcSessionLog {
|
||||
return {
|
||||
startTime: "",
|
||||
entries: [],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface IRecorder {
|
||||
recordRequest(request: GrpcRequest): void
|
||||
recordResponse(requestId: string, response: GrpcResponse): void
|
||||
recordError(requestId: string, error: string): void
|
||||
getSessionLog(): GrpcSessionLog
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of a gRPC recorder.
|
||||
*
|
||||
* Responsibilities:
|
||||
* - Records requests, responses, and errors.
|
||||
* - Tracks request/response lifecycle, including duration and status.
|
||||
* - Maintains a session log of all recorded entries.
|
||||
* - Persists logs asynchronously through a file handler.
|
||||
*/
|
||||
export class GrpcRecorder implements IRecorder {
|
||||
private sessionLog: GrpcSessionLog
|
||||
private pendingRequests: Map<string, { entry: GrpcLogEntry; startTime: number }> = new Map()
|
||||
|
||||
constructor(private fileHandler: ILogFileHandler) {
|
||||
this.sessionLog = {
|
||||
startTime: new Date().toISOString(),
|
||||
entries: [],
|
||||
}
|
||||
|
||||
this.fileHandler.initialize(this.sessionLog).catch((error) => {
|
||||
console.error("Failed to initialize gRPC log file:", error)
|
||||
})
|
||||
}
|
||||
|
||||
public static builder(): GrpcRecorderBuilder {
|
||||
return new GrpcRecorderBuilder()
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a gRPC request.
|
||||
*
|
||||
* - Stores the request as a "pending" log entry.
|
||||
* - Tracks the request start time for later duration calculation.
|
||||
* - Persists the log asynchronously.
|
||||
*
|
||||
* @param request - The incoming gRPC request.
|
||||
*/
|
||||
public recordRequest(request: GrpcRequest): void {
|
||||
const entry: GrpcLogEntry = {
|
||||
requestId: request.request_id,
|
||||
service: request.service,
|
||||
method: request.method,
|
||||
isStreaming: request.is_streaming || false,
|
||||
request: {
|
||||
message: request.message,
|
||||
},
|
||||
status: "pending",
|
||||
}
|
||||
|
||||
this.pendingRequests.set(request.request_id, {
|
||||
entry,
|
||||
startTime: Date.now(),
|
||||
})
|
||||
|
||||
this.sessionLog.entries.push(entry)
|
||||
this.flushLogAsync()
|
||||
}
|
||||
|
||||
public getSessionLog(): GrpcSessionLog {
|
||||
return this.sessionLog
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a gRPC response for a given request.
|
||||
*
|
||||
* - Looks up the pending request entry.
|
||||
* - Updates the entry with response data, status, and duration.
|
||||
* - Removes the request from pending if it's not streaming.
|
||||
* - Recomputes session stats.
|
||||
* - Persists the log asynchronously.
|
||||
*
|
||||
* @param requestId - The ID of the request being responded to.
|
||||
* @param response - The corresponding gRPC response.
|
||||
*/
|
||||
public recordResponse(requestId: string, response: GrpcResponse): void {
|
||||
const pendingRequest = this.pendingRequests.get(requestId)
|
||||
if (!pendingRequest) {
|
||||
console.warn(`No pending request found for response with ID: ${requestId}`)
|
||||
return
|
||||
}
|
||||
|
||||
const { entry, startTime } = pendingRequest
|
||||
|
||||
entry.response = {
|
||||
message: response?.message ? response.message : undefined,
|
||||
error: response?.error,
|
||||
isStreaming: response?.is_streaming,
|
||||
sequenceNumber: response?.sequence_number,
|
||||
}
|
||||
|
||||
entry.duration = Date.now() - startTime
|
||||
entry.status = response?.error ? "error" : "completed"
|
||||
|
||||
if (!response?.is_streaming) {
|
||||
this.pendingRequests.delete(requestId)
|
||||
}
|
||||
|
||||
this.sessionLog.stats = this.getStats()
|
||||
|
||||
this.flushLogAsync()
|
||||
}
|
||||
|
||||
/**
|
||||
* Records an error for a given request.
|
||||
*
|
||||
* - Marks the request as failed.
|
||||
* - Records the error message and request duration.
|
||||
* - Removes it from the pending requests.
|
||||
* - Persists the log asynchronously.
|
||||
*
|
||||
* @param requestId - The ID of the request that errored.
|
||||
* @param error - Error message.
|
||||
*/
|
||||
public recordError(requestId: string, error: string): void {
|
||||
const pendingRequest = this.pendingRequests.get(requestId)
|
||||
if (!pendingRequest) {
|
||||
console.warn(`No pending request found for error with ID: ${requestId}`)
|
||||
return
|
||||
}
|
||||
|
||||
const { entry, startTime } = pendingRequest
|
||||
|
||||
entry.response = {
|
||||
error: error,
|
||||
}
|
||||
entry.duration = Date.now() - startTime
|
||||
entry.status = "error"
|
||||
|
||||
this.pendingRequests.delete(requestId)
|
||||
this.flushLogAsync()
|
||||
}
|
||||
|
||||
private flushLogAsync(): void {
|
||||
setImmediate(() => {
|
||||
this.fileHandler.write(this.sessionLog).catch((error) => {
|
||||
console.error("Failed to flush gRPC log:", error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
public getStats(): SessionStats {
|
||||
const totalRequests = this.sessionLog.entries.length
|
||||
const pendingRequests = this.sessionLog.entries.filter((e) => e.status === "pending").length
|
||||
const completedRequests = this.sessionLog.entries.filter((e) => e.status === "completed").length
|
||||
const errorRequests = this.sessionLog.entries.filter((e) => e.status === "error").length
|
||||
|
||||
return {
|
||||
totalRequests,
|
||||
pendingRequests,
|
||||
completedRequests,
|
||||
errorRequests,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { expect } from "chai"
|
||||
import { before, describe, it } from "mocha"
|
||||
import { LogFileHandler } from "@/core/controller/grpc-recorder/log-file-handler"
|
||||
|
||||
describe("log-file-handler", () => {
|
||||
let logHandler: LogFileHandler
|
||||
|
||||
before(async () => {
|
||||
logHandler = new LogFileHandler()
|
||||
expect(logHandler.getFilePath()).not.empty
|
||||
})
|
||||
|
||||
describe("LogFileHandler", () => {
|
||||
it("returns file name with timestamp when env var not set", () => {
|
||||
const result = logHandler.getFileName()
|
||||
expect(result).to.contains("grpc_recorded_session")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,57 @@
|
||||
import { writeFile } from "@utils/fs"
|
||||
import fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import { GrpcSessionLog } from "@/core/controller/grpc-recorder/types"
|
||||
|
||||
const LOG_FILE_PREFIX = "grpc_recorded_session"
|
||||
|
||||
export class LogFileHandlerNoops implements ILogFileHandler {
|
||||
async initialize(_initialData: GrpcSessionLog): Promise<void> {}
|
||||
async write(_sessionLog: GrpcSessionLog): Promise<void> {}
|
||||
}
|
||||
|
||||
export interface ILogFileHandler {
|
||||
initialize(initialData: GrpcSessionLog): Promise<void>
|
||||
write(sessionLog: GrpcSessionLog): Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of `ILogFileHandler` that persists logs to disk.
|
||||
*
|
||||
* - Creates a log file inside the workspace `tests/specs` folder.
|
||||
* - Uses a timestamped filename by default, unless overridden by an env var.
|
||||
* - Saves logs in JSON format.
|
||||
*/
|
||||
export class LogFileHandler implements ILogFileHandler {
|
||||
private logFilePath: string
|
||||
|
||||
constructor() {
|
||||
const fileName = this.getFileName()
|
||||
const workspaceFolder = process.env.DEV_WORKSPACE_FOLDER ?? process.cwd()
|
||||
const folderPath = path.join(workspaceFolder, "tests", "specs")
|
||||
this.logFilePath = path.join(folderPath, fileName)
|
||||
}
|
||||
|
||||
public getFilePath(): string {
|
||||
return this.logFilePath
|
||||
}
|
||||
|
||||
public getFileName(): string {
|
||||
const envFileName = path.basename(process.env.GRPC_RECORDER_FILE_NAME || "").replace(/[^a-zA-Z0-9-_]/g, "_")
|
||||
if (envFileName && envFileName.trim().length > 0) {
|
||||
return `${LOG_FILE_PREFIX}_${envFileName}.json`
|
||||
}
|
||||
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
|
||||
return `${LOG_FILE_PREFIX}_${timestamp}.json`
|
||||
}
|
||||
|
||||
public async initialize(initialData: GrpcSessionLog): Promise<void> {
|
||||
await fs.mkdir(path.dirname(this.logFilePath), { recursive: true })
|
||||
await writeFile(this.logFilePath, JSON.stringify(initialData, null, 2), "utf8")
|
||||
}
|
||||
|
||||
public async write(sessionLog: GrpcSessionLog): Promise<void> {
|
||||
await writeFile(this.logFilePath, JSON.stringify(sessionLog, null, 2), "utf8")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
export interface GrpcLogEntry {
|
||||
requestId: string
|
||||
service: string
|
||||
method: string
|
||||
isStreaming: boolean
|
||||
request: {
|
||||
message: any
|
||||
}
|
||||
response?: {
|
||||
message?: any
|
||||
error?: string
|
||||
isStreaming?: boolean
|
||||
sequenceNumber?: number
|
||||
}
|
||||
duration?: number
|
||||
status: "pending" | "completed" | "error"
|
||||
}
|
||||
|
||||
export interface SessionStats {
|
||||
totalRequests: number
|
||||
pendingRequests: number
|
||||
completedRequests: number
|
||||
errorRequests: number
|
||||
}
|
||||
|
||||
export interface GrpcSessionLog {
|
||||
startTime: string
|
||||
stats?: SessionStats
|
||||
entries: GrpcLogEntry[]
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import { telemetryService } from "@/services/telemetry"
|
||||
import { ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { getLatestAnnouncementId } from "@/utils/announcements"
|
||||
import { getCwd, getDesktopDir } from "@/utils/path"
|
||||
import { PromptRegistry } from "../prompts/system-prompt"
|
||||
import { ensureMcpServersDirectoryExists, ensureSettingsDirectoryExists, GlobalFileNames } from "../storage/disk"
|
||||
import { PersistenceErrorEvent, StateManager } from "../storage/StateManager"
|
||||
import { Task } from "../task"
|
||||
@@ -40,7 +41,6 @@ https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/c
|
||||
|
||||
export class Controller {
|
||||
readonly id: string
|
||||
private disposables: vscode.Disposable[] = []
|
||||
task?: Task
|
||||
|
||||
mcpHub: McpHub
|
||||
@@ -53,7 +53,7 @@ export class Controller {
|
||||
id: string,
|
||||
) {
|
||||
this.id = id
|
||||
|
||||
PromptRegistry.getInstance() // Ensure prompts and tools are registered
|
||||
HostProvider.get().logToChannel("ClineProvider instantiated")
|
||||
this.accountService = ClineAccountService.getInstance()
|
||||
this.stateManager = new StateManager(context)
|
||||
@@ -91,6 +91,10 @@ export class Controller {
|
||||
}
|
||||
}
|
||||
|
||||
this.stateManager.onSyncExternalChange = async () => {
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
|
||||
this.mcpHub = new McpHub(
|
||||
() => ensureMcpServersDirectoryExists(),
|
||||
() => ensureSettingsDirectoryExists(this.context),
|
||||
@@ -115,12 +119,6 @@ export class Controller {
|
||||
*/
|
||||
async dispose() {
|
||||
await this.clearTask()
|
||||
while (this.disposables.length) {
|
||||
const x = this.disposables.pop()
|
||||
if (x) {
|
||||
x.dispose()
|
||||
}
|
||||
}
|
||||
this.mcpHub.dispose()
|
||||
|
||||
console.error("Controller disposed")
|
||||
@@ -195,10 +193,19 @@ export class Controller {
|
||||
}
|
||||
this.stateManager.setGlobalState("autoApprovalSettings", updatedAutoApprovalSettings)
|
||||
}
|
||||
// Apply remote feature flag gate to focus chain settings
|
||||
// Apply remote feature flag gate to focus chain settings. Respect if user has disabled it.
|
||||
let focusChainEnabled: boolean
|
||||
if (focusChainSettings?.enabled === false) {
|
||||
focusChainEnabled = false
|
||||
} else if (focusChainFeatureFlagEnabled === false) {
|
||||
focusChainEnabled = false
|
||||
} else {
|
||||
focusChainEnabled = Boolean(focusChainSettings?.enabled)
|
||||
}
|
||||
|
||||
const effectiveFocusChainSettings = {
|
||||
...(focusChainSettings || { enabled: true, remindClineInterval: 6 }),
|
||||
enabled: Boolean(focusChainSettings?.enabled) && Boolean(focusChainFeatureFlagEnabled),
|
||||
enabled: focusChainEnabled,
|
||||
}
|
||||
|
||||
this.task = new Task(
|
||||
@@ -629,8 +636,8 @@ export class Controller {
|
||||
const workflowToggles = this.stateManager.getWorkspaceStateKey("workflowToggles")
|
||||
|
||||
const currentTaskItem = this.task?.taskId ? (taskHistory || []).find((item) => item.id === this.task?.taskId) : undefined
|
||||
const checkpointTrackerErrorMessage = this.task?.taskState.checkpointTrackerErrorMessage
|
||||
const clineMessages = this.task?.messageStateHandler.getClineMessages() || []
|
||||
const checkpointManagerErrorMessage = this.task?.taskState.checkpointManagerErrorMessage
|
||||
|
||||
const processedTaskHistory = (taskHistory || [])
|
||||
.filter((item) => item.ts && item.task)
|
||||
@@ -649,12 +656,9 @@ export class Controller {
|
||||
apiConfiguration,
|
||||
uriScheme,
|
||||
currentTaskItem,
|
||||
checkpointTrackerErrorMessage,
|
||||
clineMessages,
|
||||
currentFocusChainChecklist: this.task?.taskState.currentFocusChainChecklist || null,
|
||||
taskHistory: processedTaskHistory,
|
||||
shouldShowAnnouncement,
|
||||
platform,
|
||||
checkpointManagerErrorMessage,
|
||||
autoApprovalSettings,
|
||||
browserSettings,
|
||||
focusChainSettings,
|
||||
@@ -685,6 +689,9 @@ export class Controller {
|
||||
mcpResponsesCollapsed,
|
||||
terminalOutputLineLimit,
|
||||
customPrompt,
|
||||
taskHistory: processedTaskHistory,
|
||||
platform,
|
||||
shouldShowAnnouncement,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import axios from "axios"
|
||||
import { SapAiCoreModelsRequest, SapAiCoreModelsResponse } from "@/shared/proto/cline/models"
|
||||
import { SapAiCoreModelDeployment, SapAiCoreModelsRequest, SapAiCoreModelsResponse } from "@/shared/proto/cline/models"
|
||||
import { Controller } from ".."
|
||||
|
||||
interface Token {
|
||||
@@ -11,6 +11,11 @@ interface Token {
|
||||
expires_at: number
|
||||
}
|
||||
|
||||
interface Deployment {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates with SAP AI Core and returns an access token
|
||||
* @param clientId SAP AI Core client ID
|
||||
@@ -35,19 +40,19 @@ async function getToken(clientId: string, clientSecret: string, tokenUrl: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches model names and orchestration availability from SAP AI Core deployments
|
||||
* Fetches deployments and orchestration availability from SAP AI Core deployments
|
||||
* @param accessToken Access token for authentication
|
||||
* @param baseUrl SAP AI Core base URL
|
||||
* @param resourceGroup SAP AI Core resource group
|
||||
* @returns Promise<{modelNames: string[], orchestrationAvailable: boolean}> Model names and orchestration availability
|
||||
* @returns Promise<{deployments: Deployment[], orchestrationAvailable: boolean}> Deployments and orchestration availability
|
||||
*/
|
||||
async function fetchAiCoreModelsAndOrchestration(
|
||||
async function fetchAiCoreDeploymentsAndOrchestration(
|
||||
accessToken: string,
|
||||
baseUrl: string,
|
||||
resourceGroup: string,
|
||||
): Promise<{ modelNames: string[]; orchestrationAvailable: boolean }> {
|
||||
): Promise<{ deployments: Deployment[]; orchestrationAvailable: boolean }> {
|
||||
if (!accessToken) {
|
||||
return { modelNames: ["ai-core-not-configured"], orchestrationAvailable: false }
|
||||
return { deployments: [], orchestrationAvailable: false }
|
||||
}
|
||||
|
||||
const headers = {
|
||||
@@ -61,26 +66,29 @@ async function fetchAiCoreModelsAndOrchestration(
|
||||
|
||||
try {
|
||||
const response = await axios.get(url, { headers })
|
||||
const deployments = response.data.resources
|
||||
const allDeployments = response.data.resources
|
||||
|
||||
// Filter running deployments
|
||||
const runningDeployments = deployments.filter((deployment: any) => deployment.targetStatus === "RUNNING")
|
||||
const runningDeployments = allDeployments.filter((deployment: any) => deployment.targetStatus === "RUNNING")
|
||||
|
||||
// Check for orchestration deployment
|
||||
const orchestrationAvailable = runningDeployments.some((deployment: any) => deployment.scenarioId === "orchestration")
|
||||
|
||||
// Extract model names from deployments
|
||||
const modelNames = runningDeployments
|
||||
// Extract deployments with model names and IDs
|
||||
const deployments = runningDeployments
|
||||
.map((deployment: any) => {
|
||||
const model = deployment.details?.resources?.backend_details?.model
|
||||
if (!model?.name || !model?.version) {
|
||||
return null // Skip this row
|
||||
}
|
||||
return `${model.name}:${model.version}`
|
||||
return {
|
||||
id: deployment.id,
|
||||
name: `${model.name}:${model.version}`,
|
||||
}
|
||||
})
|
||||
.filter((modelName: string | null) => modelName !== null)
|
||||
.filter((deployment: any) => deployment !== null)
|
||||
|
||||
return { modelNames, orchestrationAvailable }
|
||||
return { deployments, orchestrationAvailable }
|
||||
} catch (error) {
|
||||
console.error("Error fetching deployments:", error)
|
||||
throw new Error("Failed to fetch deployments")
|
||||
@@ -91,7 +99,7 @@ async function fetchAiCoreModelsAndOrchestration(
|
||||
* Fetches available models from SAP AI Core deployments and orchestration availability
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing SAP AI Core configuration
|
||||
* @returns SapAiCoreModelsResponse with model names and orchestration availability
|
||||
* @returns SapAiCoreModelsResponse with deployments and orchestration availability
|
||||
*/
|
||||
export async function getSapAiCoreModels(
|
||||
controller: Controller,
|
||||
@@ -102,30 +110,38 @@ export async function getSapAiCoreModels(
|
||||
if (!request.clientId || !request.clientSecret || !request.baseUrl) {
|
||||
// Return empty response if configuration is incomplete
|
||||
return SapAiCoreModelsResponse.create({
|
||||
modelNames: [],
|
||||
deployments: [],
|
||||
orchestrationAvailable: false,
|
||||
})
|
||||
}
|
||||
|
||||
// Direct authentication and model/orchestration fetching
|
||||
// Direct authentication and deployment/orchestration fetching
|
||||
const token = await getToken(request.clientId, request.clientSecret, request.tokenUrl)
|
||||
const { modelNames, orchestrationAvailable } = await fetchAiCoreModelsAndOrchestration(
|
||||
const { deployments, orchestrationAvailable } = await fetchAiCoreDeploymentsAndOrchestration(
|
||||
token.access_token,
|
||||
request.baseUrl,
|
||||
request.resourceGroup,
|
||||
)
|
||||
|
||||
// Extract base model names (without version) and sort
|
||||
const baseModelNames = modelNames.map((modelName: string) => modelName.split(":")[0].toLowerCase()).sort()
|
||||
// Create model-deployment pairs
|
||||
const modelDeployments = deployments
|
||||
.map((deployment) => {
|
||||
const modelName = deployment.name.split(":")[0].toLowerCase()
|
||||
return SapAiCoreModelDeployment.create({
|
||||
modelName: modelName,
|
||||
deploymentId: deployment.id,
|
||||
})
|
||||
})
|
||||
.sort((a, b) => a.modelName.localeCompare(b.modelName))
|
||||
|
||||
return SapAiCoreModelsResponse.create({
|
||||
modelNames: baseModelNames,
|
||||
deployments: modelDeployments,
|
||||
orchestrationAvailable,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Error fetching SAP AI Core models:", error)
|
||||
return SapAiCoreModelsResponse.create({
|
||||
modelNames: [],
|
||||
deployments: [],
|
||||
orchestrationAvailable: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { TerminalInfo } from "@/integrations/terminal/TerminalRegistry"
|
||||
import * as proto from "@/shared/proto"
|
||||
import { ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { Controller } from "../index"
|
||||
|
||||
export async function updateDefaultTerminalProfile(
|
||||
controller: Controller,
|
||||
request: proto.cline.StringRequest,
|
||||
): Promise<proto.cline.TerminalProfileUpdateResponse> {
|
||||
const profileId = request.value
|
||||
|
||||
// Update the terminal profile in the state
|
||||
controller.stateManager.setGlobalState("defaultTerminalProfile", profileId)
|
||||
|
||||
let closedCount = 0
|
||||
let busyTerminals: TerminalInfo[] = []
|
||||
|
||||
// Update the terminal manager of the current task if it exists
|
||||
if (controller.task) {
|
||||
// Call the updated setDefaultTerminalProfile method that returns closed terminal info
|
||||
const result = controller.task.terminalManager.setDefaultTerminalProfile(profileId)
|
||||
closedCount = result.closedCount
|
||||
busyTerminals = result.busyTerminals
|
||||
|
||||
// Show information message if terminals were closed
|
||||
if (closedCount > 0) {
|
||||
const message = `Closed ${closedCount} ${closedCount === 1 ? "terminal" : "terminals"} with different profile.`
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message,
|
||||
})
|
||||
}
|
||||
|
||||
// Show warning if there are busy terminals that couldn't be closed
|
||||
if (busyTerminals.length > 0) {
|
||||
const message =
|
||||
`${busyTerminals.length} busy ${busyTerminals.length === 1 ? "terminal has" : "terminals have"} a different profile. ` +
|
||||
`Close ${busyTerminals.length === 1 ? "it" : "them"} to use the new profile for all commands.`
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.WARNING,
|
||||
message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast state update to all webviews
|
||||
await controller.postStateToWebview()
|
||||
|
||||
return proto.cline.TerminalProfileUpdateResponse.create({
|
||||
closedCount,
|
||||
busyTerminalsCount: busyTerminals.length,
|
||||
hasBusyTerminals: busyTerminals.length > 0,
|
||||
})
|
||||
}
|
||||
@@ -9,8 +9,12 @@ import {
|
||||
import { convertProtoApiConfigurationToApiConfiguration } from "@shared/proto-conversions/state/settings-conversion"
|
||||
import { OpenaiReasoningEffort } from "@shared/storage/types"
|
||||
import { TelemetrySetting } from "@shared/TelemetrySetting"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { TerminalInfo } from "@/integrations/terminal/TerminalRegistry"
|
||||
import { McpDisplayMode } from "@/shared/McpDisplayMode"
|
||||
import { ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { telemetryService } from "../../../services/telemetry"
|
||||
import { BrowserSettings as SharedBrowserSettings } from "../../../shared/BrowserSettings"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
@@ -98,6 +102,9 @@ export async function updateSettings(controller: Controller, request: UpdateSett
|
||||
case ProtoOpenaiReasoningEffort.HIGH:
|
||||
reasoningEffort = "high"
|
||||
break
|
||||
case ProtoOpenaiReasoningEffort.MINIMAL:
|
||||
reasoningEffort = "minimal"
|
||||
break
|
||||
default:
|
||||
throw new Error(`Invalid OpenAI reasoning effort value: ${request.openaiReasoningEffort}`)
|
||||
}
|
||||
@@ -176,6 +183,91 @@ export async function updateSettings(controller: Controller, request: UpdateSett
|
||||
controller.stateManager.setGlobalState("customPrompt", value)
|
||||
}
|
||||
|
||||
// Update browser settings
|
||||
if (request.browserSettings !== undefined) {
|
||||
// Get current browser settings to preserve fields not in the request
|
||||
const currentSettings = controller.stateManager.getGlobalStateKey("browserSettings")
|
||||
|
||||
// Convert from protobuf format to shared format, merging with existing settings
|
||||
const newBrowserSettings: SharedBrowserSettings = {
|
||||
...currentSettings, // Start with existing settings (and defaults)
|
||||
viewport: {
|
||||
// Apply updates from request
|
||||
width: request.browserSettings.viewport?.width || currentSettings.viewport.width,
|
||||
height: request.browserSettings.viewport?.height || currentSettings.viewport.height,
|
||||
},
|
||||
// Explicitly handle optional boolean and string fields from the request
|
||||
remoteBrowserEnabled:
|
||||
request.browserSettings.remoteBrowserEnabled === undefined
|
||||
? currentSettings.remoteBrowserEnabled
|
||||
: request.browserSettings.remoteBrowserEnabled,
|
||||
remoteBrowserHost:
|
||||
request.browserSettings.remoteBrowserHost === undefined
|
||||
? currentSettings.remoteBrowserHost
|
||||
: request.browserSettings.remoteBrowserHost,
|
||||
chromeExecutablePath:
|
||||
// If chromeExecutablePath is explicitly in the request (even as ""), use it.
|
||||
// Otherwise, fall back to mergedWithDefaults.
|
||||
"chromeExecutablePath" in request.browserSettings
|
||||
? request.browserSettings.chromeExecutablePath
|
||||
: currentSettings.chromeExecutablePath,
|
||||
disableToolUse:
|
||||
request.browserSettings.disableToolUse === undefined
|
||||
? currentSettings.disableToolUse
|
||||
: request.browserSettings.disableToolUse,
|
||||
customArgs:
|
||||
"customArgs" in request.browserSettings ? request.browserSettings.customArgs : currentSettings.customArgs,
|
||||
}
|
||||
|
||||
// Update global state with new settings
|
||||
controller.stateManager.setGlobalState("browserSettings", newBrowserSettings)
|
||||
|
||||
// Update task browser settings if task exists
|
||||
if (controller.task) {
|
||||
controller.task.browserSettings = newBrowserSettings
|
||||
controller.task.browserSession.browserSettings = newBrowserSettings
|
||||
}
|
||||
}
|
||||
|
||||
// Update default terminal profile
|
||||
if (request.defaultTerminalProfile !== undefined) {
|
||||
const profileId = request.defaultTerminalProfile
|
||||
|
||||
// Update the terminal profile in the state
|
||||
controller.stateManager.setGlobalState("defaultTerminalProfile", profileId)
|
||||
|
||||
let closedCount = 0
|
||||
let busyTerminals: TerminalInfo[] = []
|
||||
|
||||
// Update the terminal manager of the current task if it exists
|
||||
if (controller.task) {
|
||||
// Call the updated setDefaultTerminalProfile method that returns closed terminal info
|
||||
const result = controller.task.terminalManager.setDefaultTerminalProfile(profileId)
|
||||
closedCount = result.closedCount
|
||||
busyTerminals = result.busyTerminals
|
||||
|
||||
// Show information message if terminals were closed
|
||||
if (closedCount > 0) {
|
||||
const message = `Closed ${closedCount} ${closedCount === 1 ? "terminal" : "terminals"} with different profile.`
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message,
|
||||
})
|
||||
}
|
||||
|
||||
// Show warning if there are busy terminals that couldn't be closed
|
||||
if (busyTerminals.length > 0) {
|
||||
const message =
|
||||
`${busyTerminals.length} busy ${busyTerminals.length === 1 ? "terminal has" : "terminals have"} a different profile. ` +
|
||||
`Close ${busyTerminals.length === 1 ? "it" : "them"} to use the new profile for all commands.`
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.WARNING,
|
||||
message,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Post updated state to webview
|
||||
await controller.postStateToWebview()
|
||||
|
||||
|
||||
@@ -138,7 +138,11 @@ async function cleanupTaskFiles(controller: Controller, preserveTaskIds: string[
|
||||
// Delete only non-preserved task directories
|
||||
for (const dir of taskDirs) {
|
||||
if (!preserveTaskIds.includes(dir)) {
|
||||
await fs.rm(path.join(taskDirPath, dir), { recursive: true, force: true })
|
||||
// Task dir path is not workspace specific
|
||||
await fs.rm(path.join(taskDirPath, dir), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Controller } from ".."
|
||||
export async function taskCompletionViewChanges(controller: Controller, request: Int64Request): Promise<Empty> {
|
||||
try {
|
||||
if (request.value && controller.task) {
|
||||
await controller.task.presentMultifileDiff(request.value, true)
|
||||
await controller.task.checkpointManager?.presentMultifileDiff(request.value, true)
|
||||
}
|
||||
return Empty.create()
|
||||
} catch (error) {
|
||||
|
||||
@@ -218,7 +218,7 @@ Perform your research without commentary or narration. Execute commands and read
|
||||
You must use the read_file tool to examine relevant source files, configuration files, and documentation. You must use terminal commands to gather information about the codebase structure and patterns. All terminal output must be piped to cat for visibility.
|
||||
|
||||
### Essential Terminal Commands
|
||||
Execute these commands to build your understanding. You must tailor them to the codebase and ensure the output is not overly verbose. These are only examples, the exact commands will differ depending on the codebase.
|
||||
First, determine the language(s) used in the codebase, then execute these commands to build your understanding. You must tailor them to the codebase and ensure the output is not overly verbose. For example, you should exclude dependency folders such as node_modules, venv or php vendor, etc. These are only examples, the exact commands will differ depending on the codebase.
|
||||
|
||||
${
|
||||
isPowerShell
|
||||
|
||||
@@ -59,6 +59,7 @@ describe("PromptRegistry", () => {
|
||||
{ id: "google/gemini", expected: ModelFamily.GENERIC },
|
||||
{ id: "claude-sonnet-4", expected: ModelFamily.NEXT_GEN },
|
||||
{ id: "gpt-5", expected: ModelFamily.GPT_5 },
|
||||
{ id: "openai/gpt-5", expected: ModelFamily.GPT_5 },
|
||||
{ id: "unknown-model", expected: ModelFamily.GENERIC },
|
||||
]
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
* model families and context configurations using snapshot testing.
|
||||
*
|
||||
* Usage:
|
||||
* - Run tests normally: `npm test` or `yarn test`
|
||||
* - Run tests normally: `npm run test:unit -- --update-snapshots`
|
||||
* Tests will fail if generated prompts don't match existing snapshots
|
||||
*
|
||||
* - Update snapshots: `npm test -- --update-snapshots` or `yarn test --update-snapshots`
|
||||
* - Update snapshots: `npm run test:unit -- --update-snapshots`
|
||||
* This will regenerate all snapshot files with current prompt output
|
||||
*
|
||||
* When tests fail:
|
||||
@@ -48,9 +48,7 @@ ${differences}
|
||||
- ❌ Unexpected changes (bugs, regressions)
|
||||
|
||||
3. 🔄 If changes are correct, update snapshots:
|
||||
npm test -- --update-snapshots
|
||||
# or
|
||||
yarn test --update-snapshots
|
||||
npm run test:unit -- --update-snapshots
|
||||
|
||||
4. 🐛 If changes are unintentional, investigate:
|
||||
- Check recent changes to prompt generation logic
|
||||
@@ -80,15 +78,19 @@ const compareStrings = (expected: string, actual: string): string | null => {
|
||||
if (differences.length < 10) {
|
||||
// Limit to first 10 differences for readability
|
||||
differences.push(`Line ${i + 1}:`)
|
||||
if (expectedLine)
|
||||
if (expectedLine) {
|
||||
differences.push(` - Expected: ${expectedLine.substring(0, 100)}${expectedLine.length > 100 ? "..." : ""}`)
|
||||
if (actualLine)
|
||||
}
|
||||
if (actualLine) {
|
||||
differences.push(` + Actual: ${actualLine.substring(0, 100)}${actualLine.length > 100 ? "..." : ""}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (differences.length === 0) return null
|
||||
if (differences.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const summary = [
|
||||
`Expected length: ${expected.length} characters`,
|
||||
|
||||
@@ -164,7 +164,7 @@ export class PromptBuilder {
|
||||
const description = [`Description: ${config.description}`]
|
||||
|
||||
if (!config.parameters?.length) {
|
||||
return [title, description.join("\n")].join("\n")
|
||||
config.parameters = []
|
||||
}
|
||||
|
||||
// Clone parameters to avoid mutating original
|
||||
@@ -197,7 +197,7 @@ export class PromptBuilder {
|
||||
|
||||
private static buildParametersSection(params: any[]): string {
|
||||
if (!params.length) {
|
||||
return ""
|
||||
return "Parameters: None"
|
||||
}
|
||||
|
||||
const paramList = params.map((p) => {
|
||||
|
||||
@@ -17,11 +17,7 @@ const generic: ClineToolSpec = {
|
||||
id,
|
||||
variant: ModelFamily.GENERIC,
|
||||
name: "load_mcp_documentation",
|
||||
description: `Load documentation about creating MCP servers. This tool should be used when the user requests to create or install an MCP server (the user may ask you something along the lines of "add a tool" that does some function, in other words to create an MCP server that provides tools and resources that may connect to external APIs for example. You have the ability to create an MCP server and add it to a configuration file that will then expose the tools and resources for you to use with \`use_mcp_tool\` and \`access_mcp_resource\`). The documentation provides detailed information about the MCP server creation process, including setup instructions, best practices, and examples.
|
||||
Parameters: None
|
||||
Usage:
|
||||
<load_mcp_documentation>
|
||||
</load_mcp_documentation>`,
|
||||
description: `Load documentation about creating MCP servers. This tool should be used when the user requests to create or install an MCP server (the user may ask you something along the lines of "add a tool" that does some function, in other words to create an MCP server that provides tools and resources that may connect to external APIs for example. You have the ability to create an MCP server and add it to a configuration file that will then expose the tools and resources for you to use with \`use_mcp_tool\` and \`access_mcp_resource\`). The documentation provides detailed information about the MCP server creation process, including setup instructions, best practices, and examples.`,
|
||||
contextRequirements: (context) => context.mcpHub !== undefined && context.mcpHub !== null,
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ export { config as gpt5Config, type GPT5VariantConfig } from "./gpt-5/config"
|
||||
export { config as nextGenConfig, type NextGenVariantConfig } from "./next-gen/config"
|
||||
export { config as xsConfig, type XsVariantConfig } from "./xs/config"
|
||||
|
||||
import { ModelFamily } from "@/shared/prompts"
|
||||
import { config as genericConfig } from "./generic/config"
|
||||
import { config as gpt5Config } from "./gpt-5/config"
|
||||
import { config as nextGenConfig } from "./next-gen/config"
|
||||
@@ -26,20 +27,21 @@ export const VARIANT_CONFIGS = {
|
||||
* Generic variant - Fallback for all model types
|
||||
* Optimized for broad compatibility and stable performance
|
||||
*/
|
||||
generic: genericConfig,
|
||||
|
||||
[ModelFamily.GENERIC]: genericConfig,
|
||||
/**
|
||||
* Next-gen variant - Advanced models with enhanced capabilities
|
||||
* Includes additional features like feedback loops and web fetching
|
||||
*/
|
||||
"next-gen": nextGenConfig,
|
||||
gpt5: gpt5Config,
|
||||
|
||||
[ModelFamily.NEXT_GEN]: nextGenConfig,
|
||||
/**
|
||||
* GPT-5 variant
|
||||
*/
|
||||
[ModelFamily.GPT_5]: gpt5Config,
|
||||
/**
|
||||
* XS variant - Compact models with limited context windows
|
||||
* Streamlined for efficiency with essential tools only
|
||||
*/
|
||||
xs: xsConfig,
|
||||
[ModelFamily.XS]: xsConfig,
|
||||
} as const
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ApiConfiguration, fireworksDefaultModelId } from "@shared/api"
|
||||
import chokidar, { FSWatcher } from "chokidar"
|
||||
import type { ExtensionContext } from "vscode"
|
||||
import { writeTaskHistoryToState } from "./disk"
|
||||
import { getTaskHistoryStateFilePath, readTaskHistoryFromState, writeTaskHistoryToState } from "./disk"
|
||||
import { STATE_MANAGER_NOT_INITIALIZED } from "./error-messages"
|
||||
import { GlobalState, GlobalStateKey, LocalState, LocalStateKey, SecretKey, Secrets } from "./state-keys"
|
||||
import { readGlobalStateFromDisk, readSecretsFromDisk, readWorkspaceStateFromDisk } from "./utils/state-helpers"
|
||||
@@ -29,10 +30,14 @@ export class StateManager {
|
||||
private pendingWorkspaceState = new Set<LocalStateKey>()
|
||||
private persistenceTimeout: NodeJS.Timeout | null = null
|
||||
private readonly PERSISTENCE_DELAY_MS = 500
|
||||
private taskHistoryWatcher: FSWatcher | null = null
|
||||
|
||||
// Callback for persistence errors
|
||||
onPersistenceError?: (event: PersistenceErrorEvent) => void
|
||||
|
||||
// Callback to sync external state changes with the UI client
|
||||
onSyncExternalChange?: () => void | Promise<void>
|
||||
|
||||
constructor(context: ExtensionContext) {
|
||||
this.context = context
|
||||
}
|
||||
@@ -52,6 +57,9 @@ export class StateManager {
|
||||
this.populateCache(globalState, secrets, workspaceState)
|
||||
|
||||
this.isInitialized = true
|
||||
|
||||
// Start watcher for taskHistory.json so external edits update cache (no persist loop)
|
||||
await this.setupTaskHistoryWatcher()
|
||||
} catch (error) {
|
||||
console.error("[StateManager] Failed to initialize:", error)
|
||||
throw error
|
||||
@@ -163,6 +171,56 @@ export class StateManager {
|
||||
this.scheduleDebouncedPersistence()
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize chokidar watcher for the taskHistory.json file
|
||||
* Updates in-memory cache on external changes without writing back to disk.
|
||||
*/
|
||||
private async setupTaskHistoryWatcher(): Promise<void> {
|
||||
try {
|
||||
const historyFile = await getTaskHistoryStateFilePath(this.context)
|
||||
|
||||
// Close any existing watcher before creating a new one
|
||||
if (this.taskHistoryWatcher) {
|
||||
await this.taskHistoryWatcher.close()
|
||||
this.taskHistoryWatcher = null
|
||||
}
|
||||
|
||||
this.taskHistoryWatcher = chokidar.watch(historyFile, {
|
||||
persistent: true,
|
||||
ignoreInitial: true,
|
||||
atomic: true,
|
||||
awaitWriteFinish: { stabilityThreshold: 300, pollInterval: 100 },
|
||||
})
|
||||
|
||||
const syncTaskHistoryFromDisk = async () => {
|
||||
try {
|
||||
if (!this.isInitialized) {
|
||||
return
|
||||
}
|
||||
const onDisk = await readTaskHistoryFromState(this.context)
|
||||
const cached = this.globalStateCache["taskHistory"]
|
||||
if (JSON.stringify(onDisk) !== JSON.stringify(cached)) {
|
||||
this.globalStateCache["taskHistory"] = onDisk
|
||||
await this.onSyncExternalChange?.()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[StateManager] Failed to reload task history on change:", err)
|
||||
}
|
||||
}
|
||||
|
||||
this.taskHistoryWatcher
|
||||
.on("add", () => syncTaskHistoryFromDisk())
|
||||
.on("change", () => syncTaskHistoryFromDisk())
|
||||
.on("unlink", async () => {
|
||||
this.globalStateCache["taskHistory"] = []
|
||||
await this.onSyncExternalChange?.()
|
||||
})
|
||||
.on("error", (error) => console.error("[StateManager] TaskHistory watcher error:", error))
|
||||
} catch (err) {
|
||||
console.error("[StateManager] Failed to set up taskHistory watcher:", err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for getting API configuration
|
||||
* Ensures cache is initialized if not already done
|
||||
@@ -277,6 +335,7 @@ export class StateManager {
|
||||
planModeTogetherModelId,
|
||||
planModeFireworksModelId,
|
||||
planModeSapAiCoreModelId,
|
||||
planModeSapAiCoreDeploymentId,
|
||||
planModeGroqModelId,
|
||||
planModeGroqModelInfo,
|
||||
planModeBasetenModelId,
|
||||
@@ -308,6 +367,7 @@ export class StateManager {
|
||||
actModeTogetherModelId,
|
||||
actModeFireworksModelId,
|
||||
actModeSapAiCoreModelId,
|
||||
actModeSapAiCoreDeploymentId,
|
||||
actModeGroqModelId,
|
||||
actModeGroqModelInfo,
|
||||
actModeBasetenModelId,
|
||||
@@ -343,6 +403,7 @@ export class StateManager {
|
||||
planModeTogetherModelId,
|
||||
planModeFireworksModelId,
|
||||
planModeSapAiCoreModelId,
|
||||
planModeSapAiCoreDeploymentId,
|
||||
planModeGroqModelId,
|
||||
planModeGroqModelInfo,
|
||||
planModeBasetenModelId,
|
||||
@@ -375,6 +436,7 @@ export class StateManager {
|
||||
actModeTogetherModelId,
|
||||
actModeFireworksModelId,
|
||||
actModeSapAiCoreModelId,
|
||||
actModeSapAiCoreDeploymentId,
|
||||
actModeGroqModelId,
|
||||
actModeGroqModelInfo,
|
||||
actModeBasetenModelId,
|
||||
@@ -515,6 +577,11 @@ export class StateManager {
|
||||
clearTimeout(this.persistenceTimeout)
|
||||
this.persistenceTimeout = null
|
||||
}
|
||||
// Close file watcher if active
|
||||
if (this.taskHistoryWatcher) {
|
||||
this.taskHistoryWatcher.close()
|
||||
this.taskHistoryWatcher = null
|
||||
}
|
||||
|
||||
this.pendingGlobalState.clear()
|
||||
this.pendingSecrets.clear()
|
||||
@@ -729,6 +796,7 @@ export class StateManager {
|
||||
planModeTogetherModelId: this.globalStateCache["planModeTogetherModelId"],
|
||||
planModeFireworksModelId: this.globalStateCache["planModeFireworksModelId"] || fireworksDefaultModelId,
|
||||
planModeSapAiCoreModelId: this.globalStateCache["planModeSapAiCoreModelId"],
|
||||
planModeSapAiCoreDeploymentId: this.globalStateCache["planModeSapAiCoreDeploymentId"],
|
||||
planModeGroqModelId: this.globalStateCache["planModeGroqModelId"],
|
||||
planModeGroqModelInfo: this.globalStateCache["planModeGroqModelInfo"],
|
||||
planModeBasetenModelId: this.globalStateCache["planModeBasetenModelId"],
|
||||
@@ -761,6 +829,7 @@ export class StateManager {
|
||||
actModeTogetherModelId: this.globalStateCache["actModeTogetherModelId"],
|
||||
actModeFireworksModelId: this.globalStateCache["actModeFireworksModelId"] || fireworksDefaultModelId,
|
||||
actModeSapAiCoreModelId: this.globalStateCache["actModeSapAiCoreModelId"],
|
||||
actModeSapAiCoreDeploymentId: this.globalStateCache["actModeSapAiCoreDeploymentId"],
|
||||
actModeGroqModelId: this.globalStateCache["actModeGroqModelId"],
|
||||
actModeGroqModelInfo: this.globalStateCache["actModeGroqModelInfo"],
|
||||
actModeBasetenModelId: this.globalStateCache["actModeBasetenModelId"],
|
||||
|
||||
@@ -201,7 +201,11 @@ export async function readTaskHistoryFromState(context: vscode.ExtensionContext)
|
||||
try {
|
||||
const filePath = await getTaskHistoryStateFilePath(context)
|
||||
if (await fileExistsAtPath(filePath)) {
|
||||
return JSON.parse(await fs.readFile(filePath, "utf8"))
|
||||
const contents = await fs.readFile(filePath, "utf8")
|
||||
if (contents.trim() === "") {
|
||||
return []
|
||||
}
|
||||
return JSON.parse(contents)
|
||||
}
|
||||
return []
|
||||
} catch (error) {
|
||||
|
||||
@@ -105,6 +105,7 @@ export interface GlobalState {
|
||||
planModeTogetherModelId: string | undefined
|
||||
planModeFireworksModelId: string | undefined
|
||||
planModeSapAiCoreModelId: string | undefined
|
||||
planModeSapAiCoreDeploymentId: string | undefined
|
||||
planModeGroqModelId: string | undefined
|
||||
planModeGroqModelInfo: ModelInfo | undefined
|
||||
planModeBasetenModelId: string | undefined
|
||||
@@ -134,6 +135,7 @@ export interface GlobalState {
|
||||
actModeTogetherModelId: string | undefined
|
||||
actModeFireworksModelId: string | undefined
|
||||
actModeSapAiCoreModelId: string | undefined
|
||||
actModeSapAiCoreDeploymentId: string | undefined
|
||||
actModeGroqModelId: string | undefined
|
||||
actModeGroqModelInfo: ModelInfo | undefined
|
||||
actModeBasetenModelId: string | undefined
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { HistoryItem } from "@/shared/HistoryItem"
|
||||
import { ensureRulesDirectoryExists, readTaskHistoryFromState, writeTaskHistoryToState } from "./disk"
|
||||
import { StateManager } from "./StateManager"
|
||||
|
||||
@@ -68,20 +69,37 @@ export async function migrateWorkspaceToGlobalStorage(context: vscode.ExtensionC
|
||||
|
||||
export async function migrateTaskHistoryToFile(context: vscode.ExtensionContext) {
|
||||
try {
|
||||
// If the old taskHistory vs code global state is undefined, do nothing
|
||||
const vscodeGlobalStateTaskHistory = await context.globalState.get("taskHistory")
|
||||
if (vscodeGlobalStateTaskHistory === undefined) {
|
||||
// Get data from old location
|
||||
const vscodeGlobalStateTaskHistory = context.globalState.get<HistoryItem[] | undefined>("taskHistory")
|
||||
|
||||
// Normalize old location data to array (empty array if undefined/null/not-array)
|
||||
const oldLocationData = Array.isArray(vscodeGlobalStateTaskHistory) ? vscodeGlobalStateTaskHistory : []
|
||||
|
||||
// Early return if no migration needed
|
||||
if (oldLocationData.length === 0) {
|
||||
console.log("[Storage Migration] No task history to migrate")
|
||||
return
|
||||
}
|
||||
// Read legacy from VS Code globalState, default to []
|
||||
console.log("[Storage Migration] taskHistory from vscode global state: ", vscodeGlobalStateTaskHistory)
|
||||
// Always create the file, even when empty
|
||||
await writeTaskHistoryToState(context, Array.isArray(vscodeGlobalStateTaskHistory) ? vscodeGlobalStateTaskHistory : [])
|
||||
// Don't remove the old taskHistory yet, while this version is not in production, for better dev experience.
|
||||
// This is because the old version of the code (still in prod) is still reading taskHistory from the vs code global state, so it will appear as if all the user's tasks have been deleted.
|
||||
// await context.globalState.update("taskHistory", undefined)
|
||||
console.log("[Storage Migration] taskHistory file in new location: ", await readTaskHistoryFromState(context))
|
||||
console.log("[Storage Migration] old vscode global state: ", await context.globalState.get("taskHistory"))
|
||||
|
||||
let finalData: HistoryItem[]
|
||||
let migrationAction: string
|
||||
|
||||
const newLocationData = await readTaskHistoryFromState(context)
|
||||
if (newLocationData.length === 0) {
|
||||
// Move old data to new location
|
||||
finalData = oldLocationData
|
||||
migrationAction = "Migrated task history from old location to new location"
|
||||
} else {
|
||||
// Merge old data (more recent) with new data
|
||||
finalData = [...newLocationData, ...oldLocationData]
|
||||
migrationAction = "Merged task history from old and new locations"
|
||||
}
|
||||
|
||||
// Perform migration operations sequentially - only clear old data if write succeeds
|
||||
await writeTaskHistoryToState(context, finalData)
|
||||
void context.globalState.update("taskHistory", undefined)
|
||||
|
||||
console.log(`[Storage Migration] ${migrationAction}`)
|
||||
} catch (error) {
|
||||
console.error("[Storage Migration] Failed to migrate task history to file:", error)
|
||||
}
|
||||
|
||||
@@ -239,6 +239,7 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
|
||||
const planModeTogetherModelId = context.globalState.get("planModeTogetherModelId") as string | undefined
|
||||
const planModeFireworksModelId = context.globalState.get("planModeFireworksModelId") as string | undefined
|
||||
const planModeSapAiCoreModelId = context.globalState.get("planModeSapAiCoreModelId") as string | undefined
|
||||
const planModeSapAiCoreDeploymentId = context.globalState.get("planModeSapAiCoreDeploymentId") as string | undefined
|
||||
const planModeGroqModelId = context.globalState.get("planModeGroqModelId") as string | undefined
|
||||
const planModeGroqModelInfo = context.globalState.get("planModeGroqModelInfo") as ModelInfo | undefined
|
||||
const planModeHuggingFaceModelId = context.globalState.get("planModeHuggingFaceModelId") as string | undefined
|
||||
@@ -278,6 +279,7 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
|
||||
const actModeTogetherModelId = context.globalState.get("actModeTogetherModelId") as string | undefined
|
||||
const actModeFireworksModelId = context.globalState.get("actModeFireworksModelId") as string | undefined
|
||||
const actModeSapAiCoreModelId = context.globalState.get("actModeSapAiCoreModelId") as string | undefined
|
||||
const actModeSapAiCoreDeploymentId = context.globalState.get("actModeSapAiCoreDeploymentId") as string | undefined
|
||||
const actModeGroqModelId = context.globalState.get("actModeGroqModelId") as string | undefined
|
||||
const actModeGroqModelInfo = context.globalState.get("actModeGroqModelInfo") as ModelInfo | undefined
|
||||
const actModeHuggingFaceModelId = context.globalState.get("actModeHuggingFaceModelId") as string | undefined
|
||||
@@ -380,6 +382,7 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
|
||||
planModeTogetherModelId,
|
||||
planModeFireworksModelId,
|
||||
planModeSapAiCoreModelId,
|
||||
planModeSapAiCoreDeploymentId,
|
||||
planModeGroqModelId,
|
||||
planModeGroqModelInfo,
|
||||
planModeHuggingFaceModelId,
|
||||
@@ -411,6 +414,7 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
|
||||
actModeTogetherModelId,
|
||||
actModeFireworksModelId,
|
||||
actModeSapAiCoreModelId,
|
||||
actModeSapAiCoreDeploymentId,
|
||||
actModeGroqModelId,
|
||||
actModeGroqModelInfo,
|
||||
actModeHuggingFaceModelId,
|
||||
|
||||
@@ -43,7 +43,7 @@ export class TaskState {
|
||||
// Error tracking
|
||||
consecutiveMistakeCount: number = 0
|
||||
didAutomaticallyRetryFailedApiRequest = false
|
||||
checkpointTrackerErrorMessage?: string
|
||||
checkpointManagerErrorMessage?: string
|
||||
|
||||
// Task Initialization
|
||||
isInitialized = false
|
||||
|
||||
@@ -10,10 +10,11 @@ import { BrowserSettings } from "@shared/BrowserSettings"
|
||||
import { ClineAsk, ClineSay } from "@shared/ExtensionMessage"
|
||||
import { FocusChainSettings } from "@shared/FocusChainSettings"
|
||||
import { Mode } from "@shared/storage/types"
|
||||
import { ClineDefaultTool } from "@shared/tools"
|
||||
import { ClineAskResponse } from "@shared/WebviewMessage"
|
||||
import * as vscode from "vscode"
|
||||
import { modelDoesntSupportWebp } from "@/utils/model-utils"
|
||||
import { ToolUse, ToolUseName } from "../assistant-message"
|
||||
import { ToolUse } from "../assistant-message"
|
||||
import { ContextManager } from "../context/context-management/ContextManager"
|
||||
import { formatResponse } from "../prompts/responses"
|
||||
import { StateManager } from "../storage/StateManager"
|
||||
@@ -51,12 +52,12 @@ export class ToolExecutor {
|
||||
private coordinator: ToolExecutorCoordinator
|
||||
|
||||
// Auto-approval methods using the AutoApprove class
|
||||
private shouldAutoApproveTool(toolName: ToolUseName): boolean | [boolean, boolean] {
|
||||
private shouldAutoApproveTool(toolName: ClineDefaultTool): boolean | [boolean, boolean] {
|
||||
return this.autoApprover.shouldAutoApproveTool(toolName)
|
||||
}
|
||||
|
||||
private async shouldAutoApproveToolWithPath(
|
||||
blockname: ToolUseName,
|
||||
blockname: ClineDefaultTool,
|
||||
autoApproveActionpath: string | undefined,
|
||||
): Promise<boolean> {
|
||||
return this.autoApprover.shouldAutoApproveToolWithPath(blockname, autoApproveActionpath)
|
||||
@@ -106,7 +107,7 @@ export class ToolExecutor {
|
||||
files?: string[]
|
||||
}>,
|
||||
private saveCheckpoint: (isAttemptCompletionMessage?: boolean, completionMessageTs?: number) => Promise<void>,
|
||||
private sayAndCreateMissingParamError: (toolName: ToolUseName, paramName: string, relPath?: string) => Promise<any>,
|
||||
private sayAndCreateMissingParamError: (toolName: ClineDefaultTool, paramName: string, relPath?: string) => Promise<any>,
|
||||
private removeLastPartialMessageIfExistsWithType: (type: "ask" | "say", askOrSay: ClineAsk | ClineSay) => Promise<void>,
|
||||
private executeCommandTool: (command: string) => Promise<[boolean, any]>,
|
||||
private doesLatestTaskCompletionHaveNewChanges: () => Promise<boolean>,
|
||||
@@ -186,9 +187,9 @@ export class ToolExecutor {
|
||||
|
||||
// Register WriteToFileToolHandler for all three file tools with proper typing
|
||||
const writeHandler = new WriteToFileToolHandler(validator)
|
||||
this.coordinator.register(writeHandler) // registers as "write_to_file"
|
||||
this.coordinator.register(new SharedToolHandler("replace_in_file", writeHandler))
|
||||
this.coordinator.register(new SharedToolHandler("new_rule", writeHandler))
|
||||
this.coordinator.register(writeHandler) // registers as "write_to_file" (ClineDefaultTool.FILE_NEW)
|
||||
this.coordinator.register(new SharedToolHandler(ClineDefaultTool.FILE_EDIT, writeHandler))
|
||||
this.coordinator.register(new SharedToolHandler(ClineDefaultTool.NEW_RULE, writeHandler))
|
||||
|
||||
this.coordinator.register(new ListCodeDefinitionNamesToolHandler(validator))
|
||||
this.coordinator.register(new SearchFilesToolHandler(validator))
|
||||
@@ -246,6 +247,7 @@ export class ToolExecutor {
|
||||
* Handles errors during tool execution
|
||||
*/
|
||||
private async handleError(action: string, error: Error, block: ToolUse): Promise<void> {
|
||||
console.log(error)
|
||||
const errorString = `Error ${action}: ${error.message}`
|
||||
await this.say("error", errorString)
|
||||
|
||||
@@ -272,7 +274,11 @@ export class ToolExecutor {
|
||||
/**
|
||||
* Tools that are restricted in plan mode and can only be used in act mode
|
||||
*/
|
||||
private static readonly PLAN_MODE_RESTRICTED_TOOLS: ToolUseName[] = ["write_to_file", "replace_in_file", "new_rule"]
|
||||
private static readonly PLAN_MODE_RESTRICTED_TOOLS: ClineDefaultTool[] = [
|
||||
ClineDefaultTool.FILE_NEW,
|
||||
ClineDefaultTool.FILE_EDIT,
|
||||
ClineDefaultTool.NEW_RULE,
|
||||
]
|
||||
|
||||
/**
|
||||
* Execute a tool through the coordinator if it's registered
|
||||
@@ -337,7 +343,7 @@ export class ToolExecutor {
|
||||
/**
|
||||
* Check if a tool is restricted in plan mode
|
||||
*/
|
||||
private isPlanModeToolRestricted(toolName: ToolUseName): boolean {
|
||||
private isPlanModeToolRestricted(toolName: ClineDefaultTool): boolean {
|
||||
return ToolExecutor.PLAN_MODE_RESTRICTED_TOOLS.includes(toolName)
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ export interface FocusChainDependencies {
|
||||
context: vscode.ExtensionContext
|
||||
stateManager: StateManager
|
||||
postStateToWebview: () => Promise<void>
|
||||
say: (type: ClineSay, text?: string, images?: string[], files?: string[], partial?: boolean) => Promise<undefined>
|
||||
say: (type: ClineSay, text?: string, images?: string[], files?: string[], partial?: boolean) => Promise<number | undefined>
|
||||
focusChainSettings: FocusChainSettings
|
||||
}
|
||||
|
||||
@@ -37,7 +37,13 @@ export class FocusChainManager {
|
||||
private context: vscode.ExtensionContext
|
||||
private stateManager: StateManager
|
||||
private postStateToWebview: () => Promise<void>
|
||||
private say: (type: ClineSay, text?: string, images?: string[], files?: string[], partial?: boolean) => Promise<undefined>
|
||||
private say: (
|
||||
type: ClineSay,
|
||||
text?: string,
|
||||
images?: string[],
|
||||
files?: string[],
|
||||
partial?: boolean,
|
||||
) => Promise<number | undefined>
|
||||
private focusChainFileWatcherCancel?: () => void
|
||||
private hasTrackedFirstProgress = false
|
||||
private focusChainSettings: FocusChainSettings
|
||||
|
||||
+161
-521
@@ -2,7 +2,7 @@ import { setTimeout as setTimeoutPromise } from "node:timers/promises"
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { ApiHandler, ApiProviderInfo, buildApiHandler } from "@core/api"
|
||||
import { ApiStream } from "@core/api/transform/stream"
|
||||
import { parseAssistantMessageV2, ToolUseName } from "@core/assistant-message"
|
||||
import { parseAssistantMessageV2 } from "@core/assistant-message"
|
||||
import { ContextManager } from "@core/context/context-management/ContextManager"
|
||||
import { checkContextWindowExceededError } from "@core/context/context-management/context-error-handling"
|
||||
import { getContextWindowInfo } from "@core/context/context-management/context-window-utils"
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
refreshExternalRulesToggles,
|
||||
} from "@core/context/instructions/user-instructions/external-rules"
|
||||
import { sendPartialMessageEvent } from "@core/controller/ui/subscribeToPartialMessage"
|
||||
import { sendRelinquishControlEvent } from "@core/controller/ui/subscribeToRelinquishControl"
|
||||
import { ClineIgnoreController } from "@core/ignore/ClineIgnoreController"
|
||||
import { parseMentions } from "@core/mentions"
|
||||
import { summarizeTask } from "@core/prompts/contextManagement"
|
||||
@@ -32,7 +31,7 @@ import {
|
||||
getSavedApiConversationHistory,
|
||||
getSavedClineMessages,
|
||||
} from "@core/storage/disk"
|
||||
import CheckpointTracker from "@integrations/checkpoints/CheckpointTracker"
|
||||
import { createTaskCheckpointManager, TaskCheckpointManager } from "@integrations/checkpoints"
|
||||
import { DiffViewProvider } from "@integrations/editor/DiffViewProvider"
|
||||
import { formatContentBlockToMarkdown } from "@integrations/misc/export-markdown"
|
||||
import { processFilesIntoText } from "@integrations/misc/extract-text"
|
||||
@@ -51,12 +50,12 @@ import { combineApiRequests } from "@shared/combineApiRequests"
|
||||
import { combineCommandSequences } from "@shared/combineCommandSequences"
|
||||
import { ClineApiReqCancelReason, ClineApiReqInfo, ClineAsk, ClineMessage, ClineSay } from "@shared/ExtensionMessage"
|
||||
import { FocusChainSettings } from "@shared/FocusChainSettings"
|
||||
import { getApiMetrics } from "@shared/getApiMetrics"
|
||||
import { HistoryItem } from "@shared/HistoryItem"
|
||||
import { DEFAULT_LANGUAGE_SETTINGS, getLanguageKey, LanguageDisplay } from "@shared/Languages"
|
||||
import { convertClineMessageToProto } from "@shared/proto-conversions/cline-message"
|
||||
import { Mode, OpenaiReasoningEffort } from "@shared/storage/types"
|
||||
import { ClineAskResponse, ClineCheckpointRestore } from "@shared/WebviewMessage"
|
||||
import { ClineDefaultTool } from "@shared/tools"
|
||||
import { ClineAskResponse } from "@shared/WebviewMessage"
|
||||
import { getGitRemoteUrls, getLatestGitCommitHash } from "@utils/git"
|
||||
import { arePathsEqual, getDesktopDir } from "@utils/path"
|
||||
import cloneDeep from "clone-deep"
|
||||
@@ -70,7 +69,7 @@ import type { SystemPromptContext } from "@/core/prompts/system-prompt"
|
||||
import { getSystemPrompt } from "@/core/prompts/system-prompt"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { errorService } from "@/services/error"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { TerminalHangStage, TerminalUserInterventionAction, telemetryService } from "@/services/telemetry"
|
||||
import { ShowMessageType } from "@/shared/proto/index.host"
|
||||
import { isInTestMode } from "../../services/test/TestMode"
|
||||
import { ensureLocalClineDirExists } from "../context/instructions/user-instructions/rule-helpers"
|
||||
@@ -80,7 +79,6 @@ import { isNextGenModelFamily } from "../prompts/system-prompt/utils"
|
||||
import { StateManager } from "../storage/StateManager"
|
||||
import { FocusChainManager } from "./focus-chain"
|
||||
import { MessageStateHandler } from "./message-state"
|
||||
import { showChangedFilesDiff } from "./multifile-diff"
|
||||
import { TaskState } from "./TaskState"
|
||||
import { ToolExecutor } from "./ToolExecutor"
|
||||
import { updateApiReqMsg } from "./utils"
|
||||
@@ -112,7 +110,7 @@ export class Task {
|
||||
browserSession: BrowserSession
|
||||
contextManager: ContextManager
|
||||
private diffViewProvider: DiffViewProvider
|
||||
private checkpointTracker?: CheckpointTracker
|
||||
public checkpointManager?: TaskCheckpointManager
|
||||
private clineIgnoreController: ClineIgnoreController
|
||||
private toolExecutor: ToolExecutor
|
||||
|
||||
@@ -227,8 +225,8 @@ export class Task {
|
||||
this.ulid = historyItem.ulid ?? ulid()
|
||||
this.taskIsFavorited = historyItem.isFavorited
|
||||
this.taskState.conversationHistoryDeletedRange = historyItem.conversationHistoryDeletedRange
|
||||
if (historyItem.checkpointTrackerErrorMessage) {
|
||||
this.taskState.checkpointTrackerErrorMessage = historyItem.checkpointTrackerErrorMessage
|
||||
if (historyItem.checkpointManagerErrorMessage) {
|
||||
this.taskState.checkpointManagerErrorMessage = historyItem.checkpointManagerErrorMessage
|
||||
}
|
||||
} else if (task || images || files) {
|
||||
this.taskId = Date.now().toString()
|
||||
@@ -264,6 +262,44 @@ export class Task {
|
||||
})
|
||||
}
|
||||
|
||||
// Initialize checkpoint manager
|
||||
try {
|
||||
this.checkpointManager = createTaskCheckpointManager(
|
||||
{
|
||||
taskId: this.taskId,
|
||||
},
|
||||
{
|
||||
enableCheckpoints: enableCheckpointsSetting,
|
||||
},
|
||||
{
|
||||
context: controller.context,
|
||||
diffViewProvider: this.diffViewProvider,
|
||||
messageStateHandler: this.messageStateHandler,
|
||||
fileContextTracker: this.fileContextTracker,
|
||||
taskState: this.taskState,
|
||||
},
|
||||
{
|
||||
updateTaskHistory: this.updateTaskHistory,
|
||||
say: this.say.bind(this),
|
||||
cancelTask: this.cancelTask,
|
||||
postStateToWebview: this.postStateToWebview,
|
||||
},
|
||||
{
|
||||
conversationHistoryDeletedRange: this.taskState.conversationHistoryDeletedRange,
|
||||
checkpointManagerErrorMessage: this.taskState.checkpointManagerErrorMessage,
|
||||
},
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize checkpoint manager:", error)
|
||||
if (enableCheckpointsSetting) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error"
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Failed to initialize checkpoint manager: ${errorMessage}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare effective API configuration
|
||||
const effectiveApiConfiguration: ApiConfiguration = {
|
||||
...apiConfiguration,
|
||||
@@ -364,11 +400,11 @@ export class Task {
|
||||
strictPlanModeEnabled,
|
||||
this.say.bind(this),
|
||||
this.ask.bind(this),
|
||||
this.saveCheckpoint.bind(this),
|
||||
this.saveCheckpointCallback.bind(this),
|
||||
this.sayAndCreateMissingParamError.bind(this),
|
||||
this.removeLastPartialMessageIfExistsWithType.bind(this),
|
||||
this.executeCommandTool.bind(this),
|
||||
this.doesLatestTaskCompletionHaveNewChanges.bind(this),
|
||||
() => this.checkpointManager?.doesLatestTaskCompletionHaveNewChanges() ?? Promise.resolve(false),
|
||||
this.FocusChainManager?.updateFCListFromToolResponse.bind(this.FocusChainManager) || (async () => {}),
|
||||
)
|
||||
}
|
||||
@@ -419,310 +455,6 @@ export class Task {
|
||||
}
|
||||
}
|
||||
|
||||
async restoreCheckpoint(messageTs: number, restoreType: ClineCheckpointRestore, offset?: number) {
|
||||
const clineMessages = this.messageStateHandler.getClineMessages()
|
||||
const messageIndex = clineMessages.findIndex((m) => m.ts === messageTs) - (offset || 0)
|
||||
// Find the last message before messageIndex that has a lastCheckpointHash
|
||||
const lastHashIndex = findLastIndex(clineMessages.slice(0, messageIndex), (m) => m.lastCheckpointHash !== undefined)
|
||||
const message = clineMessages[messageIndex]
|
||||
const lastMessageWithHash = clineMessages[lastHashIndex]
|
||||
|
||||
if (!message) {
|
||||
console.error("Message not found", clineMessages)
|
||||
return
|
||||
}
|
||||
|
||||
let didWorkspaceRestoreFail = false
|
||||
|
||||
switch (restoreType) {
|
||||
case "task":
|
||||
break
|
||||
case "taskAndWorkspace":
|
||||
case "workspace":
|
||||
if (!this.enableCheckpoints) {
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Checkpoints are disabled in settings.",
|
||||
})
|
||||
didWorkspaceRestoreFail = true
|
||||
break
|
||||
}
|
||||
|
||||
if (!this.checkpointTracker && !this.taskState.checkpointTrackerErrorMessage) {
|
||||
try {
|
||||
this.checkpointTracker = await CheckpointTracker.create(
|
||||
this.taskId,
|
||||
this.controller.context.globalStorageUri.fsPath,
|
||||
this.enableCheckpoints,
|
||||
)
|
||||
this.messageStateHandler.setCheckpointTracker(this.checkpointTracker)
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error"
|
||||
console.error("Failed to initialize checkpoint tracker:", errorMessage)
|
||||
this.taskState.checkpointTrackerErrorMessage = errorMessage
|
||||
await this.postStateToWebview()
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: errorMessage,
|
||||
})
|
||||
didWorkspaceRestoreFail = true
|
||||
}
|
||||
}
|
||||
if (message.lastCheckpointHash && this.checkpointTracker) {
|
||||
try {
|
||||
await this.checkpointTracker.resetHead(message.lastCheckpointHash)
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error"
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Failed to restore checkpoint: " + errorMessage,
|
||||
})
|
||||
didWorkspaceRestoreFail = true
|
||||
}
|
||||
} else if (offset && lastMessageWithHash.lastCheckpointHash && this.checkpointTracker) {
|
||||
try {
|
||||
await this.checkpointTracker.resetHead(lastMessageWithHash.lastCheckpointHash)
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error"
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "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"
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Failed to restore checkpoint: " + errorMessage,
|
||||
})
|
||||
didWorkspaceRestoreFail = true
|
||||
}
|
||||
} else {
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Failed to restore checkpoint",
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if (!didWorkspaceRestoreFail) {
|
||||
switch (restoreType) {
|
||||
case "task":
|
||||
case "taskAndWorkspace": {
|
||||
this.taskState.conversationHistoryDeletedRange = message.conversationHistoryDeletedRange
|
||||
const apiConversationHistory = this.messageStateHandler.getApiConversationHistory()
|
||||
const newConversationHistory = apiConversationHistory.slice(0, (message.conversationHistoryIndex || 0) + 2) // +1 since this index corresponds to the last user message, and another +1 since slice end index is exclusive
|
||||
await this.messageStateHandler.overwriteApiConversationHistory(newConversationHistory)
|
||||
|
||||
// update the context history state
|
||||
await this.contextManager.truncateContextHistory(
|
||||
message.ts,
|
||||
await ensureTaskDirectoryExists(this.getContext(), this.taskId),
|
||||
)
|
||||
|
||||
// aggregate deleted api reqs info so we don't lose costs/tokens
|
||||
const clineMessages = this.messageStateHandler.getClineMessages()
|
||||
const deletedMessages = 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 = clineMessages.slice(0, messageIndex + 1)
|
||||
await this.messageStateHandler.overwriteClineMessages(newClineMessages) // calls saveClineMessages which saves historyItem
|
||||
|
||||
await this.say(
|
||||
"deleted_api_reqs",
|
||||
JSON.stringify({
|
||||
tokensIn: deletedApiReqsMetrics.totalTokensIn,
|
||||
tokensOut: deletedApiReqsMetrics.totalTokensOut,
|
||||
cacheWrites: deletedApiReqsMetrics.totalCacheWrites,
|
||||
cacheReads: deletedApiReqsMetrics.totalCacheReads,
|
||||
cost: deletedApiReqsMetrics.totalCost,
|
||||
} satisfies ClineApiReqInfo),
|
||||
)
|
||||
break
|
||||
}
|
||||
case "workspace":
|
||||
break
|
||||
}
|
||||
|
||||
switch (restoreType) {
|
||||
case "task":
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Task messages have been restored to the checkpoint",
|
||||
})
|
||||
break
|
||||
case "workspace":
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Workspace files have been restored to the checkpoint",
|
||||
})
|
||||
break
|
||||
case "taskAndWorkspace":
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Task and workspace have been restored to the checkpoint",
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
if (restoreType !== "task") {
|
||||
// Set isCheckpointCheckedOut flag on the message
|
||||
// Find all checkpoint messages before this one
|
||||
const checkpointMessages = this.messageStateHandler
|
||||
.getClineMessages()
|
||||
.filter((m) => m.say === "checkpoint_created")
|
||||
const currentMessageIndex = checkpointMessages.findIndex((m) => m.ts === messageTs)
|
||||
|
||||
// Set isCheckpointCheckedOut to false for all checkpoint messages
|
||||
checkpointMessages.forEach((m, i) => {
|
||||
m.isCheckpointCheckedOut = i === currentMessageIndex
|
||||
})
|
||||
}
|
||||
|
||||
await this.messageStateHandler.saveClineMessagesAndUpdateHistory()
|
||||
|
||||
this.cancelTask() // the task is already cancelled by the provider beforehand, but we need to re-init to get the updated messages
|
||||
} else {
|
||||
sendRelinquishControlEvent()
|
||||
}
|
||||
}
|
||||
|
||||
async presentMultifileDiff(messageTs: number, seeNewChangesSinceLastTaskCompletion: boolean) {
|
||||
try {
|
||||
if (!this.enableCheckpoints) {
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Checkpoints are disabled in settings. Cannot show diff.",
|
||||
})
|
||||
return
|
||||
}
|
||||
// TODO: handle if this is called from outside original workspace, in which case we need to
|
||||
// show user error message we can't show diff outside of workspace?
|
||||
if (!this.checkpointTracker && !this.taskState.checkpointTrackerErrorMessage) {
|
||||
try {
|
||||
this.checkpointTracker = await CheckpointTracker.create(
|
||||
this.taskId,
|
||||
this.controller.context.globalStorageUri.fsPath,
|
||||
this.enableCheckpoints,
|
||||
)
|
||||
this.messageStateHandler.setCheckpointTracker(this.checkpointTracker)
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize checkpoint tracker:", error)
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error"
|
||||
this.taskState.checkpointTrackerErrorMessage = errorMessage
|
||||
await this.postStateToWebview()
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: errorMessage,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
if (!this.checkpointTracker) {
|
||||
return
|
||||
}
|
||||
|
||||
showChangedFilesDiff(
|
||||
this.messageStateHandler,
|
||||
this.checkpointTracker,
|
||||
messageTs,
|
||||
seeNewChangesSinceLastTaskCompletion,
|
||||
)
|
||||
} finally {
|
||||
sendRelinquishControlEvent()
|
||||
}
|
||||
}
|
||||
|
||||
async doesLatestTaskCompletionHaveNewChanges() {
|
||||
if (!this.enableCheckpoints) {
|
||||
return false
|
||||
}
|
||||
|
||||
const clineMessages = this.messageStateHandler.getClineMessages()
|
||||
const messageIndex = findLastIndex(clineMessages, (m) => m.say === "completion_result")
|
||||
const message = clineMessages[messageIndex]
|
||||
if (!message) {
|
||||
console.error("Completion message not found")
|
||||
return false
|
||||
}
|
||||
const hash = message.lastCheckpointHash
|
||||
if (!hash) {
|
||||
console.error("No checkpoint hash found")
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.enableCheckpoints && !this.checkpointTracker && !this.taskState.checkpointTrackerErrorMessage) {
|
||||
try {
|
||||
this.checkpointTracker = await CheckpointTracker.create(
|
||||
this.taskId,
|
||||
this.controller.context.globalStorageUri.fsPath,
|
||||
this.enableCheckpoints,
|
||||
)
|
||||
this.messageStateHandler.setCheckpointTracker(this.checkpointTracker)
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error"
|
||||
console.error("Failed to initialize checkpoint tracker:", errorMessage)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Get last task completed
|
||||
const lastTaskCompletedMessage = findLast(
|
||||
this.messageStateHandler.getClineMessages().slice(0, messageIndex),
|
||||
(m) => m.say === "completion_result",
|
||||
)
|
||||
|
||||
try {
|
||||
// Get last task completed
|
||||
const lastTaskCompletedMessageCheckpointHash = lastTaskCompletedMessage?.lastCheckpointHash // ask is only used to relinquish control, its the last say we care about
|
||||
// if undefined, then we get diff from beginning of git
|
||||
// if (!lastTaskCompletedMessage) {
|
||||
// console.error("No previous task completion message found")
|
||||
// return
|
||||
// }
|
||||
// This value *should* always exist
|
||||
const firstCheckpointMessageCheckpointHash = this.messageStateHandler
|
||||
.getClineMessages()
|
||||
.find((m) => m.say === "checkpoint_created")?.lastCheckpointHash
|
||||
|
||||
const previousCheckpointHash = lastTaskCompletedMessageCheckpointHash || firstCheckpointMessageCheckpointHash // either use the diff between the first checkpoint and the task completion, or the diff between the latest two task completions
|
||||
|
||||
if (!previousCheckpointHash) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Get count of changed files between current state and commit
|
||||
const changedFilesCount = (await this.checkpointTracker?.getDiffCount(previousCheckpointHash, hash)) || 0
|
||||
if (changedFilesCount > 0) {
|
||||
return true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to get diff set:", error)
|
||||
return false
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Communicate with webview
|
||||
|
||||
// partial has three valid states true (partial message), false (completion of partial message), undefined (individual complete message)
|
||||
@@ -865,7 +597,13 @@ export class Task {
|
||||
this.taskState.askResponseFiles = files
|
||||
}
|
||||
|
||||
async say(type: ClineSay, text?: string, images?: string[], files?: string[], partial?: boolean): Promise<undefined> {
|
||||
async say(
|
||||
type: ClineSay,
|
||||
text?: string,
|
||||
images?: string[],
|
||||
files?: string[],
|
||||
partial?: boolean,
|
||||
): Promise<number | undefined> {
|
||||
if (this.taskState.abort) {
|
||||
throw new Error("Cline instance aborted")
|
||||
}
|
||||
@@ -883,6 +621,7 @@ export class Task {
|
||||
lastMessage.partial = partial
|
||||
const protoMessage = convertClineMessageToProto(lastMessage)
|
||||
await sendPartialMessageEvent(protoMessage)
|
||||
return undefined
|
||||
} else {
|
||||
// this is a new partial message, so add it with partial state
|
||||
const sayTs = Date.now()
|
||||
@@ -897,6 +636,7 @@ export class Task {
|
||||
partial,
|
||||
})
|
||||
await this.postStateToWebview()
|
||||
return sayTs
|
||||
}
|
||||
} else {
|
||||
// partial=false means its a complete version of a previously partial message
|
||||
@@ -914,6 +654,7 @@ export class Task {
|
||||
// await this.postStateToWebview()
|
||||
const protoMessage = convertClineMessageToProto(lastMessage)
|
||||
await sendPartialMessageEvent(protoMessage) // more performant than an entire postStateToWebview
|
||||
return undefined
|
||||
} else {
|
||||
// this is a new partial=false message, so add it like normal
|
||||
const sayTs = Date.now()
|
||||
@@ -927,6 +668,7 @@ export class Task {
|
||||
files,
|
||||
})
|
||||
await this.postStateToWebview()
|
||||
return sayTs
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -942,10 +684,11 @@ export class Task {
|
||||
files,
|
||||
})
|
||||
await this.postStateToWebview()
|
||||
return sayTs
|
||||
}
|
||||
}
|
||||
|
||||
async sayAndCreateMissingParamError(toolName: ToolUseName, paramName: string, relPath?: string) {
|
||||
async sayAndCreateMissingParamError(toolName: ClineDefaultTool, paramName: string, relPath?: string) {
|
||||
await this.say(
|
||||
"error",
|
||||
`Cline tried to use ${toolName}${
|
||||
@@ -964,6 +707,10 @@ export class Task {
|
||||
}
|
||||
}
|
||||
|
||||
private async saveCheckpointCallback(isAttemptCompletionMessage?: boolean, completionMessageTs?: number): Promise<void> {
|
||||
return this.checkpointManager?.saveCheckpoint(isAttemptCompletionMessage, completionMessageTs) ?? Promise.resolve()
|
||||
}
|
||||
|
||||
// Task lifecycle
|
||||
|
||||
private async startTask(task?: string, images?: string[], files?: string[]): Promise<void> {
|
||||
@@ -1014,12 +761,6 @@ export class Task {
|
||||
console.error("Failed to initialize ClineIgnoreController:", error)
|
||||
// Optionally, inform the user or handle the error appropriately
|
||||
}
|
||||
// UPDATE: we don't need this anymore since most tasks are now created with checkpoints enabled
|
||||
// right now we let users init checkpoints for old tasks, assuming they're continuing them from the same workspace (which we never tied to tasks, so no way for us to know if it's opened in the right workspace)
|
||||
// const doesShadowGitExist = await CheckpointTracker.doesShadowGitExist(this.taskId, this.controllerRef.deref())
|
||||
// if (!doesShadowGitExist) {
|
||||
// this.checkpointTrackerErrorMessage = "Checkpoints are only available for new tasks"
|
||||
// }
|
||||
|
||||
const savedClineMessages = await getSavedClineMessages(this.getContext(), this.taskId)
|
||||
|
||||
@@ -1077,9 +818,7 @@ export class Task {
|
||||
let responseFiles: string[] | undefined
|
||||
if (response === "messageResponse") {
|
||||
await this.say("user_feedback", text, images, files)
|
||||
if (!this.taskState.checkpointTrackerErrorMessage?.includes("Checkpoints initialization timed out.")) {
|
||||
await this.saveCheckpoint()
|
||||
}
|
||||
await this.checkpointManager?.saveCheckpoint()
|
||||
responseText = text
|
||||
responseImages = images
|
||||
responseFiles = files
|
||||
@@ -1244,147 +983,6 @@ export class Task {
|
||||
}
|
||||
}
|
||||
|
||||
// Checkpoints
|
||||
|
||||
async saveCheckpoint(isAttemptCompletionMessage: boolean = false) {
|
||||
if (
|
||||
!this.enableCheckpoints ||
|
||||
this.taskState.checkpointTrackerErrorMessage?.includes("Checkpoints initialization timed out.")
|
||||
) {
|
||||
// If checkpoints are disabled or previously encountered a timeout error, do nothing.
|
||||
return
|
||||
}
|
||||
// Set isCheckpointCheckedOut to false for all checkpoint_created messages
|
||||
this.messageStateHandler.getClineMessages().forEach((message) => {
|
||||
if (message.say === "checkpoint_created") {
|
||||
message.isCheckpointCheckedOut = false
|
||||
}
|
||||
})
|
||||
|
||||
if (!isAttemptCompletionMessage) {
|
||||
// ensure we aren't creating a duplicate checkpoint
|
||||
const lastMessage = this.messageStateHandler.getClineMessages().at(-1)
|
||||
if (lastMessage?.say === "checkpoint_created") {
|
||||
return
|
||||
}
|
||||
|
||||
// Initialize checkpoint tracker if it doesn't exist
|
||||
if (!this.checkpointTracker && !this.taskState.checkpointTrackerErrorMessage) {
|
||||
try {
|
||||
this.checkpointTracker = await CheckpointTracker.create(
|
||||
this.taskId,
|
||||
this.controller.context.globalStorageUri.fsPath,
|
||||
this.enableCheckpoints,
|
||||
)
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error"
|
||||
console.error("Failed to initialize checkpoint tracker:", errorMessage)
|
||||
this.taskState.checkpointTrackerErrorMessage = errorMessage
|
||||
await this.postStateToWebview()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Create a checkpoint commit and update clineMessages with a commitHash
|
||||
if (this.checkpointTracker) {
|
||||
// We are letting this run in a non-blocking way so that the UI doesn't freeze when creating checkpoints.
|
||||
// We show that a checkpoint is created in the chatview, then in the background run the git operation (which can take multiple seconds for large shadow git repos), and once that's been completed update the previous checkpoint message with the newly created hash to be associated with.
|
||||
// NOTE: the attempt completion flow is different in that it requires the latest checkpoint hash to be present before determining if it can present the 'see new changes' button. In ToolExecutor, when we call saveCheckpoint(true), we must make sure that the checkpoint hash is present in the last completion_result message before returning, since it is always followed by a addNewChangesFlagToLastCompletionResultMessage(), which calls doesLatestTaskCompletionHaveNewChanges() that uses the latest message hash to determine if there any changes since the last attempt_completion checkpoint.
|
||||
await this.say("checkpoint_created")
|
||||
this.checkpointTracker.commit().then(async (commitHash) => {
|
||||
if (commitHash) {
|
||||
const lastCheckpointMessageIndex = findLastIndex(
|
||||
this.messageStateHandler.getClineMessages(),
|
||||
(m) => m.say === "checkpoint_created",
|
||||
)
|
||||
if (lastCheckpointMessageIndex !== -1) {
|
||||
await this.messageStateHandler.updateClineMessage(lastCheckpointMessageIndex, {
|
||||
lastCheckpointHash: commitHash,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
} // silently fails for now
|
||||
|
||||
//
|
||||
} else {
|
||||
// attempt completion requires checkpoint to be sync so that we can present button after attempt_completion
|
||||
// Check if checkpoint tracker exists, if not, create it. Skip if there was a previous checkpoints initialization timeout error.
|
||||
if (
|
||||
!this.checkpointTracker &&
|
||||
!this.taskState.checkpointTrackerErrorMessage?.includes("Checkpoints initialization timed out.")
|
||||
) {
|
||||
try {
|
||||
this.checkpointTracker = await CheckpointTracker.create(
|
||||
this.taskId,
|
||||
this.controller.context.globalStorageUri.fsPath,
|
||||
this.enableCheckpoints,
|
||||
)
|
||||
this.messageStateHandler.setCheckpointTracker(this.checkpointTracker)
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error"
|
||||
console.error("Failed to initialize checkpoint tracker for attempt completion:", errorMessage)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
this.checkpointTracker &&
|
||||
!this.taskState.checkpointTrackerErrorMessage?.includes("Checkpoints initialization timed out.")
|
||||
) {
|
||||
const commitHash = await this.checkpointTracker.commit()
|
||||
|
||||
// For attempt_completion, find the last completion_result message and set its checkpoint hash. This will be used to present the 'see new changes' button
|
||||
const lastCompletionResultMessage = findLast(
|
||||
this.messageStateHandler.getClineMessages(),
|
||||
(m) => m.say === "completion_result" || m.ask === "completion_result",
|
||||
)
|
||||
if (lastCompletionResultMessage) {
|
||||
lastCompletionResultMessage.lastCheckpointHash = commitHash
|
||||
await this.messageStateHandler.saveClineMessagesAndUpdateHistory()
|
||||
}
|
||||
} else {
|
||||
console.error("Checkpoint tracker does not exist and could not be initialized for attempt completion")
|
||||
}
|
||||
}
|
||||
|
||||
// if (commitHash) {
|
||||
|
||||
// Previously we checkpointed every message, but this is excessive and unnecessary.
|
||||
// // Start from the end and work backwards until we find a tool use or another message with a hash
|
||||
// for (let i = this.clineMessages.length - 1; i >= 0; i--) {
|
||||
// const message = this.clineMessages[i]
|
||||
// if (message.lastCheckpointHash) {
|
||||
// // Found a message with a hash, so we can stop
|
||||
// break
|
||||
// }
|
||||
// // Update this message with a hash
|
||||
// message.lastCheckpointHash = commitHash
|
||||
|
||||
// // We only care about adding the hash to the last tool use (we don't want to add this hash to every prior message ie for tasks pre-checkpoint)
|
||||
// const isToolUse =
|
||||
// message.say === "tool" ||
|
||||
// message.ask === "tool" ||
|
||||
// message.say === "command" ||
|
||||
// message.ask === "command" ||
|
||||
// message.say === "completion_result" ||
|
||||
// message.ask === "completion_result" ||
|
||||
// message.ask === "followup" ||
|
||||
// message.say === "use_mcp_server" ||
|
||||
// message.ask === "use_mcp_server" ||
|
||||
// message.say === "browser_action" ||
|
||||
// message.say === "browser_action_launch" ||
|
||||
// message.ask === "browser_action_launch"
|
||||
|
||||
// if (isToolUse) {
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// // Save the updated messages
|
||||
// await this.saveClineMessagesAndUpdateHistory()
|
||||
// }
|
||||
}
|
||||
|
||||
// Tools
|
||||
|
||||
/**
|
||||
@@ -1491,6 +1089,10 @@ export class Task {
|
||||
let chunkTimer: NodeJS.Timeout | null = null
|
||||
let chunkEnroute = false
|
||||
|
||||
// Track if buffer gets stuck
|
||||
let bufferStuckTimer: NodeJS.Timeout | null = null
|
||||
const BUFFER_STUCK_TIMEOUT_MS = 6000 // 6 seconds
|
||||
|
||||
const flushBuffer = async (force = false) => {
|
||||
if (chunkEnroute || outputBuffer.length === 0) {
|
||||
if (force && !chunkEnroute && outputBuffer.length > 0) {
|
||||
@@ -1503,9 +1105,18 @@ export class Task {
|
||||
outputBuffer = []
|
||||
outputBufferSize = 0
|
||||
chunkEnroute = true
|
||||
|
||||
// Start timer to detect if buffer gets stuck
|
||||
bufferStuckTimer = setTimeout(() => {
|
||||
telemetryService.captureTerminalHang(TerminalHangStage.BUFFER_STUCK)
|
||||
bufferStuckTimer = null
|
||||
}, BUFFER_STUCK_TIMEOUT_MS)
|
||||
|
||||
try {
|
||||
const { response, text, images, files } = await this.ask("command_output", chunk)
|
||||
if (response === "yesButtonClicked") {
|
||||
// Track when user clicks "Process while Running"
|
||||
telemetryService.captureTerminalUserIntervention(TerminalUserInterventionAction.PROCESS_WHILE_RUNNING)
|
||||
// proceed while running - but still capture user feedback if provided
|
||||
if (text || (images && images.length > 0) || (files && files.length > 0)) {
|
||||
userFeedback = { text, images, files }
|
||||
@@ -1517,7 +1128,13 @@ export class Task {
|
||||
process.continue()
|
||||
} catch {
|
||||
Logger.error("Error while asking for command output")
|
||||
telemetryService.captureTerminalHang(TerminalHangStage.STREAM_TIMEOUT)
|
||||
} finally {
|
||||
// Clear the stuck timer
|
||||
if (bufferStuckTimer) {
|
||||
clearTimeout(bufferStuckTimer)
|
||||
bufferStuckTimer = null
|
||||
}
|
||||
chunkEnroute = false
|
||||
// If more output accumulated while chunkEnroute, flush again
|
||||
if (outputBuffer.length > 0) {
|
||||
@@ -1552,8 +1169,24 @@ export class Task {
|
||||
})
|
||||
|
||||
let completed = false
|
||||
let completionTimer: NodeJS.Timeout | null = null
|
||||
const COMPLETION_TIMEOUT_MS = 6000 // 6 seconds
|
||||
|
||||
// Start timer to detect if waiting for completion takes too long
|
||||
completionTimer = setTimeout(() => {
|
||||
if (!completed) {
|
||||
telemetryService.captureTerminalHang(TerminalHangStage.WAITING_FOR_COMPLETION)
|
||||
completionTimer = null
|
||||
}
|
||||
}, COMPLETION_TIMEOUT_MS)
|
||||
|
||||
process.once("completed", async () => {
|
||||
completed = true
|
||||
// Clear the completion timer
|
||||
if (completionTimer) {
|
||||
clearTimeout(completionTimer)
|
||||
completionTimer = null
|
||||
}
|
||||
// Flush any remaining buffered output
|
||||
if (!didContinue && outputBuffer.length > 0) {
|
||||
if (chunkTimer) {
|
||||
@@ -1570,6 +1203,12 @@ export class Task {
|
||||
|
||||
await process
|
||||
|
||||
// Clear timer if process completes normally
|
||||
if (completionTimer) {
|
||||
clearTimeout(completionTimer)
|
||||
completionTimer = null
|
||||
}
|
||||
|
||||
// Wait for a short delay to ensure all messages are sent to the webview
|
||||
// This delay allows time for non-awaited promises to be created and
|
||||
// for their associated messages to be sent to the webview, maintaining
|
||||
@@ -1581,7 +1220,7 @@ export class Task {
|
||||
|
||||
if (userFeedback) {
|
||||
await this.say("user_feedback", userFeedback.text, userFeedback.images, userFeedback.files)
|
||||
await this.saveCheckpoint()
|
||||
await this.checkpointManager?.saveCheckpoint()
|
||||
|
||||
let fileContentString = ""
|
||||
if (userFeedback.files && userFeedback.files.length > 0) {
|
||||
@@ -1634,6 +1273,14 @@ export class Task {
|
||||
return { model, providerId, customPrompt }
|
||||
}
|
||||
|
||||
private getApiRequestIdSafe(): string | undefined {
|
||||
const apiLike = this.api as Partial<{
|
||||
getLastRequestId: () => string | undefined
|
||||
lastGenerationId?: string
|
||||
}>
|
||||
return apiLike.getLastRequestId?.() ?? apiLike.lastGenerationId
|
||||
}
|
||||
|
||||
private async handleContextWindowExceededError(): Promise<void> {
|
||||
const apiConversationHistory = this.messageStateHandler.getApiConversationHistory()
|
||||
|
||||
@@ -1973,6 +1620,9 @@ export class Task {
|
||||
: "Cline uses complex prompts and iterative task execution that may be challenging for less capable models. For best results, it's recommended to use Claude 4 Sonnet for its advanced agentic coding capabilities.",
|
||||
)
|
||||
if (response === "messageResponse") {
|
||||
// Display the user's message in the chat UI
|
||||
await this.say("user_feedback", text, images, files)
|
||||
|
||||
// This userContent is for the *next* API call.
|
||||
const feedbackUserContent: UserContent = []
|
||||
feedbackUserContent.push({
|
||||
@@ -2063,63 +1713,34 @@ export class Task {
|
||||
}),
|
||||
)
|
||||
|
||||
// Initialize checkpoint tracker first if enabled and it's the first request
|
||||
// Initialize checkpointManager first if enabled and it's the first request
|
||||
if (
|
||||
isFirstRequest &&
|
||||
this.enableCheckpoints &&
|
||||
!this.checkpointTracker &&
|
||||
!this.taskState.checkpointTrackerErrorMessage
|
||||
this.checkpointManager && // TODO REVIEW: may be able to implement a replacement for the 15s timer
|
||||
!this.taskState.checkpointManagerErrorMessage
|
||||
) {
|
||||
try {
|
||||
// Warning Timer - If checkpoints take a while to to initialize, show a warning message
|
||||
let checkpointsWarningTimer: NodeJS.Timeout | null = null
|
||||
let checkpointsWarningShown = false
|
||||
|
||||
checkpointsWarningTimer = setTimeout(async () => {
|
||||
if (!checkpointsWarningShown) {
|
||||
checkpointsWarningShown = true
|
||||
this.taskState.checkpointTrackerErrorMessage =
|
||||
"Checkpoints are taking longer than expected to initialize. Working in a large repository? Consider re-opening Cline in a project that uses git, or disabling checkpoints."
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
}, 7_000)
|
||||
|
||||
// Timeout - If checkpoints take too long to initialize, warn user and disable checkpoints for the task
|
||||
this.checkpointTracker = await pTimeout(
|
||||
CheckpointTracker.create(
|
||||
this.taskId,
|
||||
this.controller.context.globalStorageUri.fsPath,
|
||||
this.enableCheckpoints,
|
||||
),
|
||||
{
|
||||
milliseconds: 15_000,
|
||||
message:
|
||||
"Checkpoints taking too long to initialize. Consider re-opening Cline in a project that uses git, or disabling checkpoints.",
|
||||
},
|
||||
)
|
||||
if (checkpointsWarningTimer) {
|
||||
clearTimeout(checkpointsWarningTimer)
|
||||
checkpointsWarningTimer = null
|
||||
}
|
||||
await pTimeout(this.checkpointManager.checkpointTrackerCheckAndInit(), {
|
||||
milliseconds: 15_000,
|
||||
message:
|
||||
"Checkpoints taking too long to initialize. Consider re-opening Cline in a project that uses git, or disabling checkpoints.",
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error"
|
||||
console.error("Failed to initialize checkpoint tracker:", errorMessage)
|
||||
|
||||
// If the error was a timeout, we disabled all checkpoint operations for the rest of the task
|
||||
if (errorMessage.includes("Checkpoints taking too long to initialize")) {
|
||||
this.taskState.checkpointTrackerErrorMessage =
|
||||
"Checkpoints initialization timed out. Consider re-opening Cline in a project that uses git, or disabling checkpoints."
|
||||
await this.postStateToWebview()
|
||||
} else {
|
||||
this.taskState.checkpointTrackerErrorMessage = errorMessage // will be displayed right away since we saveClineMessages next which posts state to webview
|
||||
}
|
||||
console.error("Failed to initialize checkpoint manager:", errorMessage)
|
||||
this.taskState.checkpointManagerErrorMessage = errorMessage // will be displayed right away since we saveClineMessages next which posts state to webview
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Checkpoint initialization timed out: ${errorMessage}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
if (isFirstRequest && this.enableCheckpoints && this.checkpointManager) {
|
||||
const commitHash = await this.checkpointManager.commit() // Actual commit
|
||||
await this.say("checkpoint_created") // Now this is conditional
|
||||
const lastCheckpointMessageIndex = findLastIndex(
|
||||
this.messageStateHandler.getClineMessages(),
|
||||
@@ -2136,11 +1757,11 @@ export class Task {
|
||||
} else if (
|
||||
isFirstRequest &&
|
||||
this.enableCheckpoints &&
|
||||
!this.checkpointTracker &&
|
||||
this.taskState.checkpointTrackerErrorMessage
|
||||
!this.checkpointManager &&
|
||||
this.taskState.checkpointManagerErrorMessage
|
||||
) {
|
||||
// Checkpoints are enabled, but tracker failed to initialize.
|
||||
// checkpointTrackerErrorMessage is already set and will be part of the state.
|
||||
// checkpointManagerErrorMessage is already set and will be part of the state.
|
||||
// No explicit UI message here, error message will be in ExtensionState.
|
||||
}
|
||||
|
||||
@@ -2534,10 +2155,29 @@ export class Task {
|
||||
didEndLoop = recDidEndLoop
|
||||
} else {
|
||||
// if there's no assistant_responses, that means we got no text or tool_use content blocks from API which we should assume is an error
|
||||
await this.say(
|
||||
"error",
|
||||
"Unexpected API Response: The language model did not provide any assistant messages. This may indicate an issue with the API or the model's output.",
|
||||
)
|
||||
const { model, providerId } = this.getCurrentProviderInfo()
|
||||
const reqId = this.getApiRequestIdSafe()
|
||||
|
||||
// Minimal diagnostics: structured log and telemetry
|
||||
console.error("[EmptyAssistantMessage]", {
|
||||
ulid: this.ulid,
|
||||
providerId,
|
||||
modelId: model.id,
|
||||
requestId: reqId,
|
||||
})
|
||||
telemetryService.captureProviderApiError({
|
||||
ulid: this.ulid,
|
||||
model: model.id,
|
||||
provider: providerId,
|
||||
errorMessage: "empty_assistant_message",
|
||||
requestId: reqId,
|
||||
})
|
||||
|
||||
const baseErrorMessage =
|
||||
"Unexpected API Response: The language model did not provide any assistant messages. This may indicate an issue with the API or the model's output."
|
||||
const errorText = reqId ? `${baseErrorMessage} (reqId: ${reqId})` : baseErrorMessage
|
||||
|
||||
await this.say("error", errorText)
|
||||
await this.messageStateHandler.addToApiConversationHistory({
|
||||
role: "assistant",
|
||||
content: [
|
||||
|
||||
@@ -19,7 +19,7 @@ interface MessageStateHandlerParams {
|
||||
taskIsFavorited?: boolean
|
||||
updateTaskHistory: (historyItem: HistoryItem) => Promise<HistoryItem[]>
|
||||
taskState: TaskState
|
||||
checkpointTrackerErrorMessage?: string
|
||||
checkpointManagerErrorMessage?: string
|
||||
}
|
||||
|
||||
export class MessageStateHandler {
|
||||
@@ -27,7 +27,7 @@ export class MessageStateHandler {
|
||||
private clineMessages: ClineMessage[] = []
|
||||
private taskIsFavorited: boolean
|
||||
private checkpointTracker: CheckpointTracker | undefined
|
||||
private checkpointTrackerErrorMessage: string | undefined
|
||||
private checkpointManagerErrorMessage: string | undefined
|
||||
private updateTaskHistory: (historyItem: HistoryItem) => Promise<HistoryItem[]>
|
||||
private context: vscode.ExtensionContext
|
||||
private taskId: string
|
||||
@@ -41,7 +41,7 @@ export class MessageStateHandler {
|
||||
this.taskState = params.taskState
|
||||
this.taskIsFavorited = params.taskIsFavorited ?? false
|
||||
this.updateTaskHistory = params.updateTaskHistory
|
||||
this.checkpointTrackerErrorMessage = this.taskState.checkpointTrackerErrorMessage
|
||||
this.checkpointManagerErrorMessage = this.taskState.checkpointManagerErrorMessage
|
||||
}
|
||||
|
||||
setCheckpointTracker(tracker: CheckpointTracker | undefined) {
|
||||
@@ -103,7 +103,7 @@ export class MessageStateHandler {
|
||||
cwdOnTaskInitialization: cwd,
|
||||
conversationHistoryDeletedRange: this.taskState.conversationHistoryDeletedRange,
|
||||
isFavorited: this.taskIsFavorited,
|
||||
checkpointTrackerErrorMessage: this.taskState.checkpointTrackerErrorMessage,
|
||||
checkpointManagerErrorMessage: this.taskState.checkpointManagerErrorMessage,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to save cline messages:", error)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { ToolUse } from "@core/assistant-message"
|
||||
import { ClineDefaultTool } from "@/shared/tools"
|
||||
import type { ToolResponse } from "../index"
|
||||
import type { TaskConfig } from "./types/TaskConfig"
|
||||
import type { StronglyTypedUIHelpers } from "./types/UIHelpers"
|
||||
|
||||
export interface IToolHandler {
|
||||
readonly name: string
|
||||
readonly name: ClineDefaultTool
|
||||
execute(config: TaskConfig, block: ToolUse): Promise<ToolResponse>
|
||||
getDescription(block: ToolUse): string
|
||||
}
|
||||
@@ -23,7 +24,7 @@ export interface IFullyManagedTool extends IToolHandler, IPartialBlockHandler {
|
||||
*/
|
||||
export class SharedToolHandler implements IFullyManagedTool {
|
||||
constructor(
|
||||
public readonly name: string,
|
||||
public readonly name: ClineDefaultTool,
|
||||
private baseHandler: IFullyManagedTool,
|
||||
) {}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ToolUseName } from "@core/assistant-message"
|
||||
import { resolveWorkspacePath } from "@core/workspace"
|
||||
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
|
||||
import * as path from "path"
|
||||
import { getCwd, getDesktopDir } from "@/utils/path"
|
||||
import { ClineDefaultTool } from "@shared/tools"
|
||||
import { getCwd, getDesktopDir, isLocatedInPath } from "@/utils/path"
|
||||
|
||||
export class AutoApprove {
|
||||
autoApprovalSettings: AutoApprovalSettings
|
||||
@@ -12,35 +12,35 @@ export class AutoApprove {
|
||||
|
||||
// Check if the tool should be auto-approved based on the settings
|
||||
// Returns bool for most tools, and tuple for tools with nested settings
|
||||
shouldAutoApproveTool(toolName: ToolUseName): boolean | [boolean, boolean] {
|
||||
shouldAutoApproveTool(toolName: ClineDefaultTool): boolean | [boolean, boolean] {
|
||||
if (this.autoApprovalSettings.enabled) {
|
||||
switch (toolName) {
|
||||
case "read_file":
|
||||
case "list_files":
|
||||
case "list_code_definition_names":
|
||||
case "search_files":
|
||||
case ClineDefaultTool.FILE_READ:
|
||||
case ClineDefaultTool.LIST_FILES:
|
||||
case ClineDefaultTool.LIST_CODE_DEF:
|
||||
case ClineDefaultTool.SEARCH:
|
||||
return [
|
||||
this.autoApprovalSettings.actions.readFiles,
|
||||
this.autoApprovalSettings.actions.readFilesExternally ?? false,
|
||||
]
|
||||
case "new_rule":
|
||||
case "write_to_file":
|
||||
case "replace_in_file":
|
||||
case ClineDefaultTool.NEW_RULE:
|
||||
case ClineDefaultTool.FILE_NEW:
|
||||
case ClineDefaultTool.FILE_EDIT:
|
||||
return [
|
||||
this.autoApprovalSettings.actions.editFiles,
|
||||
this.autoApprovalSettings.actions.editFilesExternally ?? false,
|
||||
]
|
||||
case "execute_command":
|
||||
case ClineDefaultTool.BASH:
|
||||
return [
|
||||
this.autoApprovalSettings.actions.executeSafeCommands ?? false,
|
||||
this.autoApprovalSettings.actions.executeAllCommands ?? false,
|
||||
]
|
||||
case "browser_action":
|
||||
case ClineDefaultTool.BROWSER:
|
||||
return this.autoApprovalSettings.actions.useBrowser
|
||||
case "web_fetch":
|
||||
case ClineDefaultTool.WEB_FETCH:
|
||||
return this.autoApprovalSettings.actions.useBrowser
|
||||
case "access_mcp_resource":
|
||||
case "use_mcp_tool":
|
||||
case ClineDefaultTool.MCP_ACCESS:
|
||||
case ClineDefaultTool.MCP_USE:
|
||||
return this.autoApprovalSettings.actions.useMcp
|
||||
}
|
||||
}
|
||||
@@ -50,12 +50,15 @@ export class AutoApprove {
|
||||
// Check if the tool should be auto-approved based on the settings
|
||||
// and the path of the action. Returns true if the tool should be auto-approved
|
||||
// based on the user's settings and the path of the action.
|
||||
async shouldAutoApproveToolWithPath(blockname: ToolUseName, autoApproveActionpath: string | undefined): Promise<boolean> {
|
||||
async shouldAutoApproveToolWithPath(
|
||||
blockname: ClineDefaultTool,
|
||||
autoApproveActionpath: string | undefined,
|
||||
): Promise<boolean> {
|
||||
let isLocalRead: boolean = false
|
||||
if (autoApproveActionpath) {
|
||||
const cwd = await getCwd(getDesktopDir())
|
||||
const absolutePath = path.resolve(cwd, autoApproveActionpath)
|
||||
isLocalRead = absolutePath.startsWith(cwd)
|
||||
const absolutePath = resolveWorkspacePath(cwd, autoApproveActionpath, "AutoApprove.shouldAutoApproveToolWithPath")
|
||||
isLocalRead = isLocatedInPath(cwd, absolutePath)
|
||||
} else {
|
||||
// If we do not get a path for some reason, default to a (safer) false return
|
||||
isLocalRead = false
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { ToolUse } from "@core/assistant-message"
|
||||
import { formatResponse } from "@core/prompts/responses"
|
||||
import { ClineAsk, ClineAskUseMcpServer } from "@shared/ExtensionMessage"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { ClineDefaultTool } from "@/shared/tools"
|
||||
import type { ToolResponse } from "../../index"
|
||||
import { showNotificationForApprovalIfAutoApprovalEnabled } from "../../utils"
|
||||
import type { IFullyManagedTool } from "../ToolExecutorCoordinator"
|
||||
@@ -10,9 +11,7 @@ import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
|
||||
import { ToolResultUtils } from "../utils/ToolResultUtils"
|
||||
|
||||
export class AccessMcpResourceHandler implements IFullyManagedTool {
|
||||
readonly name = "access_mcp_resource"
|
||||
|
||||
constructor() {}
|
||||
readonly name = ClineDefaultTool.MCP_ACCESS
|
||||
|
||||
getDescription(block: ToolUse): string {
|
||||
return `[${block.name} for '${block.params.server_name}']`
|
||||
@@ -23,7 +22,7 @@ export class AccessMcpResourceHandler implements IFullyManagedTool {
|
||||
const uri = block.params.uri
|
||||
|
||||
const partialMessage = JSON.stringify({
|
||||
type: "access_mcp_resource",
|
||||
type: this.name,
|
||||
serverName: uiHelpers.removeClosingTag(block, "server_name", server_name),
|
||||
toolName: undefined,
|
||||
uri: uiHelpers.removeClosingTag(block, "uri", uri),
|
||||
@@ -49,12 +48,12 @@ export class AccessMcpResourceHandler implements IFullyManagedTool {
|
||||
// Validate required parameters
|
||||
if (!server_name) {
|
||||
config.taskState.consecutiveMistakeCount++
|
||||
return await config.callbacks.sayAndCreateMissingParamError("access_mcp_resource", "server_name")
|
||||
return await config.callbacks.sayAndCreateMissingParamError(ClineDefaultTool.MCP_ACCESS, "server_name")
|
||||
}
|
||||
|
||||
if (!uri) {
|
||||
config.taskState.consecutiveMistakeCount++
|
||||
return await config.callbacks.sayAndCreateMissingParamError("access_mcp_resource", "uri")
|
||||
return await config.callbacks.sayAndCreateMissingParamError(ClineDefaultTool.MCP_ACCESS, "uri")
|
||||
}
|
||||
|
||||
config.taskState.consecutiveMistakeCount = 0
|
||||
|
||||
@@ -2,8 +2,9 @@ import { processFilesIntoText } from "@integrations/misc/extract-text"
|
||||
import { showSystemNotification } from "@integrations/notifications"
|
||||
import { findLast, parsePartialArrayString } from "@shared/array"
|
||||
import { ClineAsk, ClineAskQuestion } from "@shared/ExtensionMessage"
|
||||
import { ClineDefaultTool } from "@shared/tools"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { ToolUse, ToolUseName } from "../../../assistant-message"
|
||||
import { ToolUse } from "../../../assistant-message"
|
||||
import { formatResponse } from "../../../prompts/responses"
|
||||
import { ToolResponse } from "../.."
|
||||
import type { IPartialBlockHandler, IToolHandler } from "../ToolExecutorCoordinator"
|
||||
@@ -11,8 +12,7 @@ import type { TaskConfig } from "../types/TaskConfig"
|
||||
import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
|
||||
|
||||
export class AskFollowupQuestionToolHandler implements IToolHandler, IPartialBlockHandler {
|
||||
name = "ask_followup_question"
|
||||
supportedTools: ToolUseName[] = ["ask_followup_question"]
|
||||
readonly name = ClineDefaultTool.ASK
|
||||
|
||||
getDescription(block: ToolUse): string {
|
||||
return `[${block.name} for '${block.params.question}']`
|
||||
@@ -36,7 +36,7 @@ export class AskFollowupQuestionToolHandler implements IToolHandler, IPartialBlo
|
||||
// Validate required parameter
|
||||
if (!question) {
|
||||
config.taskState.consecutiveMistakeCount++
|
||||
return await config.callbacks.sayAndCreateMissingParamError("ask_followup_question", "question")
|
||||
return await config.callbacks.sayAndCreateMissingParamError(this.name, "question")
|
||||
}
|
||||
config.taskState.consecutiveMistakeCount = 0
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { showSystemNotification } from "@integrations/notifications"
|
||||
import { findLastIndex } from "@shared/array"
|
||||
import { COMPLETION_RESULT_CHANGES_FLAG } from "@shared/ExtensionMessage"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { ClineDefaultTool } from "@/shared/tools"
|
||||
import type { ToolResponse } from "../../index"
|
||||
import type { IPartialBlockHandler, IToolHandler } from "../ToolExecutorCoordinator"
|
||||
import type { TaskConfig } from "../types/TaskConfig"
|
||||
@@ -13,9 +14,7 @@ import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
|
||||
import { ToolResultUtils } from "../utils/ToolResultUtils"
|
||||
|
||||
export class AttemptCompletionHandler implements IToolHandler, IPartialBlockHandler {
|
||||
readonly name = "attempt_completion"
|
||||
|
||||
constructor() {}
|
||||
readonly name = ClineDefaultTool.ATTEMPT
|
||||
|
||||
getDescription(block: ToolUse): string {
|
||||
return `[${block.name}]`
|
||||
@@ -48,7 +47,7 @@ export class AttemptCompletionHandler implements IToolHandler, IPartialBlockHand
|
||||
// Validate required parameters
|
||||
if (!result) {
|
||||
config.taskState.consecutiveMistakeCount++
|
||||
return await config.callbacks.sayAndCreateMissingParamError("attempt_completion", "result")
|
||||
return await config.callbacks.sayAndCreateMissingParamError(this.name, "result")
|
||||
}
|
||||
|
||||
config.taskState.consecutiveMistakeCount = 0
|
||||
@@ -96,6 +95,11 @@ export class AttemptCompletionHandler implements IToolHandler, IPartialBlockHand
|
||||
await config.callbacks.saveCheckpoint(true)
|
||||
}
|
||||
|
||||
// Attempt completion is a special tool where we want to update the focus chain list before the user provides response
|
||||
if (!block.partial && config.focusChainSettings.enabled) {
|
||||
await config.callbacks.updateFCListFromToolResponse(block.params.task_progress)
|
||||
}
|
||||
|
||||
// complete command message - need to ask for approval
|
||||
const didApprove = await ToolResultUtils.askApprovalAndPushFeedback("command", command, config)
|
||||
if (!didApprove) {
|
||||
@@ -122,6 +126,11 @@ export class AttemptCompletionHandler implements IToolHandler, IPartialBlockHand
|
||||
if (config.messageState.getClineMessages().at(-1)?.ask === "command_output") {
|
||||
await config.callbacks.say("command_output", "")
|
||||
}
|
||||
|
||||
if (!block.partial && config.focusChainSettings.enabled) {
|
||||
await config.callbacks.updateFCListFromToolResponse(block.params.task_progress)
|
||||
}
|
||||
|
||||
const { response, text, images, files: completionFiles } = await config.callbacks.ask("completion_result", "", false)
|
||||
if (response === "yesButtonClicked") {
|
||||
return "" // signals to recursive loop to stop (for now this never happens since yesButtonClicked will trigger a new task)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { BrowserAction, BrowserActionResult, browserActions, ClineSayBrowserAction } from "@shared/ExtensionMessage"
|
||||
import { ClineDefaultTool } from "@/shared/tools"
|
||||
import { ToolUse } from "../../../assistant-message"
|
||||
import { formatResponse } from "../../../prompts/responses"
|
||||
import { ToolResponse } from "../.."
|
||||
@@ -9,7 +10,7 @@ import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
|
||||
import { ToolResultUtils } from "../utils/ToolResultUtils"
|
||||
|
||||
export class BrowserToolHandler implements IFullyManagedTool {
|
||||
readonly name = "browser_action"
|
||||
readonly name = ClineDefaultTool.BROWSER
|
||||
|
||||
getDescription(block: ToolUse): string {
|
||||
return `[${block.name} for '${block.params.action}']`
|
||||
@@ -45,7 +46,7 @@ export class BrowserToolHandler implements IFullyManagedTool {
|
||||
}
|
||||
} else {
|
||||
await uiHelpers.say(
|
||||
"browser_action",
|
||||
this.name,
|
||||
JSON.stringify({
|
||||
action: action as BrowserAction,
|
||||
coordinate: uiHelpers.removeClosingTag(block, "coordinate", coordinate),
|
||||
@@ -68,7 +69,7 @@ export class BrowserToolHandler implements IFullyManagedTool {
|
||||
if (!action || !browserActions.includes(action)) {
|
||||
// if the block is complete and we don't have a valid action this is a mistake
|
||||
config.taskState.consecutiveMistakeCount++
|
||||
const errorResult = await config.callbacks.sayAndCreateMissingParamError("browser_action", "action")
|
||||
const errorResult = await config.callbacks.sayAndCreateMissingParamError(this.name, "action")
|
||||
await config.services.browserSession.closeBrowser()
|
||||
return errorResult
|
||||
}
|
||||
@@ -80,7 +81,7 @@ export class BrowserToolHandler implements IFullyManagedTool {
|
||||
if (action === "launch") {
|
||||
if (!url) {
|
||||
config.taskState.consecutiveMistakeCount++
|
||||
const errorResult = await config.callbacks.sayAndCreateMissingParamError("browser_action", "url")
|
||||
const errorResult = await config.callbacks.sayAndCreateMissingParamError(this.name, "url")
|
||||
await config.services.browserSession.closeBrowser()
|
||||
return errorResult
|
||||
}
|
||||
@@ -119,7 +120,7 @@ export class BrowserToolHandler implements IFullyManagedTool {
|
||||
if (action === "click") {
|
||||
if (!coordinate) {
|
||||
config.taskState.consecutiveMistakeCount++
|
||||
const errorResult = await config.callbacks.sayAndCreateMissingParamError("browser_action", "coordinate")
|
||||
const errorResult = await config.callbacks.sayAndCreateMissingParamError(this.name, "coordinate")
|
||||
await config.services.browserSession.closeBrowser()
|
||||
return errorResult
|
||||
}
|
||||
@@ -127,7 +128,7 @@ export class BrowserToolHandler implements IFullyManagedTool {
|
||||
if (action === "type") {
|
||||
if (!text) {
|
||||
config.taskState.consecutiveMistakeCount++
|
||||
const errorResult = await config.callbacks.sayAndCreateMissingParamError("browser_action", "text")
|
||||
const errorResult = await config.callbacks.sayAndCreateMissingParamError(this.name, "text")
|
||||
await config.services.browserSession.closeBrowser()
|
||||
return errorResult
|
||||
}
|
||||
@@ -136,7 +137,7 @@ export class BrowserToolHandler implements IFullyManagedTool {
|
||||
|
||||
// Send browser action message
|
||||
await config.callbacks.say(
|
||||
"browser_action",
|
||||
this.name,
|
||||
JSON.stringify({
|
||||
action: action as BrowserAction,
|
||||
coordinate,
|
||||
|
||||
@@ -4,13 +4,14 @@ import { ensureTaskDirectoryExists } from "@core/storage/disk"
|
||||
import { processFilesIntoText } from "@integrations/misc/extract-text"
|
||||
import { showSystemNotification } from "@integrations/notifications"
|
||||
import { ClineAsk } from "@shared/ExtensionMessage"
|
||||
import { ClineDefaultTool } from "@/shared/tools"
|
||||
import type { ToolResponse } from "../../index"
|
||||
import type { IPartialBlockHandler, IToolHandler } from "../ToolExecutorCoordinator"
|
||||
import type { TaskConfig } from "../types/TaskConfig"
|
||||
import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
|
||||
|
||||
export class CondenseHandler implements IToolHandler, IPartialBlockHandler {
|
||||
readonly name = "condense"
|
||||
readonly name = ClineDefaultTool.CONDENSE
|
||||
|
||||
constructor() {}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { COMMAND_REQ_APP_STRING } from "@shared/combineCommandSequences"
|
||||
import { ClineAsk } from "@shared/ExtensionMessage"
|
||||
import { fixModelHtmlEscaping } from "@utils/string"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { ClineDefaultTool } from "@/shared/tools"
|
||||
import type { ToolResponse } from "../../index"
|
||||
import { showNotificationForApprovalIfAutoApprovalEnabled } from "../../utils"
|
||||
import type { IFullyManagedTool } from "../ToolExecutorCoordinator"
|
||||
@@ -14,7 +15,7 @@ import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
|
||||
import { ToolResultUtils } from "../utils/ToolResultUtils"
|
||||
|
||||
export class ExecuteCommandToolHandler implements IFullyManagedTool {
|
||||
readonly name = "execute_command"
|
||||
readonly name = ClineDefaultTool.BASH
|
||||
|
||||
constructor(_validator: ToolValidator) {}
|
||||
|
||||
@@ -26,7 +27,7 @@ export class ExecuteCommandToolHandler implements IFullyManagedTool {
|
||||
const command = block.params.command
|
||||
|
||||
// Check if this should be auto-approved to determine UI flow
|
||||
const shouldAutoApprove = uiHelpers.shouldAutoApproveTool("execute_command")
|
||||
const shouldAutoApprove = uiHelpers.shouldAutoApproveTool(this.name)
|
||||
|
||||
if (shouldAutoApprove) {
|
||||
// For auto-approved commands, we can't partially stream a say prematurely
|
||||
@@ -48,12 +49,12 @@ export class ExecuteCommandToolHandler implements IFullyManagedTool {
|
||||
// Validate required parameters
|
||||
if (!command) {
|
||||
config.taskState.consecutiveMistakeCount++
|
||||
return await config.callbacks.sayAndCreateMissingParamError("execute_command", "command")
|
||||
return await config.callbacks.sayAndCreateMissingParamError(this.name, "command")
|
||||
}
|
||||
|
||||
if (!requiresApprovalRaw) {
|
||||
config.taskState.consecutiveMistakeCount++
|
||||
return await config.callbacks.sayAndCreateMissingParamError("execute_command", "requires_approval")
|
||||
return await config.callbacks.sayAndCreateMissingParamError(this.name, "requires_approval")
|
||||
}
|
||||
|
||||
config.taskState.consecutiveMistakeCount = 0
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { ToolUse } from "@core/assistant-message"
|
||||
import { getWorkspaceBasename, resolveWorkspacePath } from "@core/workspace"
|
||||
import { parseSourceCodeForDefinitionsTopLevel } from "@services/tree-sitter"
|
||||
import { getReadablePath, isLocatedInWorkspace } from "@utils/path"
|
||||
import * as path from "path"
|
||||
import { formatResponse } from "@/core/prompts/responses"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { ClineDefaultTool } from "@/shared/tools"
|
||||
import type { ToolResponse } from "../../index"
|
||||
import { showNotificationForApprovalIfAutoApprovalEnabled } from "../../utils"
|
||||
import type { IFullyManagedTool } from "../ToolExecutorCoordinator"
|
||||
@@ -13,7 +14,7 @@ import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
|
||||
import { ToolResultUtils } from "../utils/ToolResultUtils"
|
||||
|
||||
export class ListCodeDefinitionNamesToolHandler implements IFullyManagedTool {
|
||||
readonly name = "list_code_definition_names"
|
||||
readonly name = ClineDefaultTool.LIST_CODE_DEF
|
||||
|
||||
constructor(private validator: ToolValidator) {}
|
||||
|
||||
@@ -53,11 +54,11 @@ export class ListCodeDefinitionNamesToolHandler implements IFullyManagedTool {
|
||||
const pathValidation = this.validator.assertRequiredParams(block, "path")
|
||||
if (!pathValidation.ok) {
|
||||
config.taskState.consecutiveMistakeCount++
|
||||
return await config.callbacks.sayAndCreateMissingParamError("list_code_definition_names", "path")
|
||||
return await config.callbacks.sayAndCreateMissingParamError(this.name, "path")
|
||||
}
|
||||
|
||||
config.taskState.consecutiveMistakeCount = 0
|
||||
const absolutePath = path.resolve(config.cwd, relDirPath!)
|
||||
const absolutePath = resolveWorkspacePath(config.cwd, relDirPath!, "ListCodeDefinitionNamesToolHandler.execute")
|
||||
// Execute the actual parse source code operation
|
||||
const result = await parseSourceCodeForDefinitionsTopLevel(absolutePath, config.services.clineIgnoreController)
|
||||
|
||||
@@ -81,7 +82,7 @@ export class ListCodeDefinitionNamesToolHandler implements IFullyManagedTool {
|
||||
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, true, true)
|
||||
} else {
|
||||
// Manual approval flow
|
||||
const notificationMessage = `Cline wants to analyze code definitions in ${path.basename(absolutePath)}`
|
||||
const notificationMessage = `Cline wants to analyze code definitions in ${getWorkspaceBasename(absolutePath, "ListCodeDefinitionNamesToolHandler.notification")}`
|
||||
|
||||
// Show notification
|
||||
showNotificationForApprovalIfAutoApprovalEnabled(
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { ToolUse } from "@core/assistant-message"
|
||||
import { formatResponse } from "@core/prompts/responses"
|
||||
import { getWorkspaceBasename, resolveWorkspacePath } from "@core/workspace"
|
||||
import { listFiles } from "@services/glob/list-files"
|
||||
import { getReadablePath, isLocatedInWorkspace } from "@utils/path"
|
||||
import * as path from "path"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { ClineDefaultTool } from "@/shared/tools"
|
||||
import type { ToolResponse } from "../../index"
|
||||
import { showNotificationForApprovalIfAutoApprovalEnabled } from "../../utils"
|
||||
import type { IFullyManagedTool } from "../ToolExecutorCoordinator"
|
||||
@@ -13,7 +14,7 @@ import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
|
||||
import { ToolResultUtils } from "../utils/ToolResultUtils"
|
||||
|
||||
export class ListFilesToolHandler implements IFullyManagedTool {
|
||||
readonly name = "list_files"
|
||||
readonly name = ClineDefaultTool.LIST_FILES
|
||||
|
||||
constructor(private validator: ToolValidator) {}
|
||||
|
||||
@@ -58,11 +59,11 @@ export class ListFilesToolHandler implements IFullyManagedTool {
|
||||
const pathValidation = this.validator.assertRequiredParams(block, "path")
|
||||
if (!pathValidation.ok) {
|
||||
config.taskState.consecutiveMistakeCount++
|
||||
return await config.callbacks.sayAndCreateMissingParamError("list_files", "path")
|
||||
return await config.callbacks.sayAndCreateMissingParamError(this.name, "path")
|
||||
}
|
||||
|
||||
config.taskState.consecutiveMistakeCount = 0
|
||||
const absolutePath = path.resolve(config.cwd, relDirPath!)
|
||||
const absolutePath = resolveWorkspacePath(config.cwd, relDirPath!, "ListFilesToolHandler.execute")
|
||||
|
||||
// Execute the actual list files operation
|
||||
const [files, didHitLimit] = await listFiles(absolutePath, recursive, 200)
|
||||
@@ -89,7 +90,7 @@ export class ListFilesToolHandler implements IFullyManagedTool {
|
||||
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, true, true)
|
||||
} else {
|
||||
// Manual approval flow
|
||||
const notificationMessage = `Cline wants to view directory ${path.basename(absolutePath)}/`
|
||||
const notificationMessage = `Cline wants to view directory ${getWorkspaceBasename(absolutePath, "ListFilesToolHandler.notification")}/`
|
||||
|
||||
// Show notification
|
||||
showNotificationForApprovalIfAutoApprovalEnabled(
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { ToolUse } from "@core/assistant-message"
|
||||
import { loadMcpDocumentation } from "@core/prompts/loadMcpDocumentation"
|
||||
import { ClineDefaultTool } from "@/shared/tools"
|
||||
import type { ToolResponse } from "../../index"
|
||||
import type { IPartialBlockHandler, IToolHandler } from "../ToolExecutorCoordinator"
|
||||
import type { TaskConfig } from "../types/TaskConfig"
|
||||
import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
|
||||
|
||||
export class LoadMcpDocumentationHandler implements IToolHandler, IPartialBlockHandler {
|
||||
readonly name = "load_mcp_documentation"
|
||||
readonly name = ClineDefaultTool.MCP_DOCS
|
||||
|
||||
constructor() {}
|
||||
|
||||
@@ -16,12 +17,12 @@ export class LoadMcpDocumentationHandler implements IToolHandler, IPartialBlockH
|
||||
|
||||
async handlePartialBlock(_block: ToolUse, uiHelpers: StronglyTypedUIHelpers): Promise<void> {
|
||||
// Show loading message for partial blocks (though this tool probably won't have partials)
|
||||
await uiHelpers.say("load_mcp_documentation", "", undefined, undefined, true)
|
||||
await uiHelpers.say(this.name, "", undefined, undefined, true)
|
||||
}
|
||||
|
||||
async execute(config: TaskConfig, block: ToolUse): Promise<ToolResponse> {
|
||||
async execute(config: TaskConfig, _block: ToolUse): Promise<ToolResponse> {
|
||||
// Show loading message at start of execution (self-managed now)
|
||||
await config.callbacks.say("load_mcp_documentation", "", undefined, undefined, false)
|
||||
await config.callbacks.say(this.name, "", undefined, undefined, false)
|
||||
|
||||
config.taskState.consecutiveMistakeCount = 0
|
||||
|
||||
|
||||
@@ -2,14 +2,14 @@ import type { ToolUse } from "@core/assistant-message"
|
||||
import { formatResponse } from "@core/prompts/responses"
|
||||
import { processFilesIntoText } from "@integrations/misc/extract-text"
|
||||
import { showSystemNotification } from "@integrations/notifications"
|
||||
import { ClineDefaultTool } from "@/shared/tools"
|
||||
import type { ToolResponse } from "../../index"
|
||||
import type { IPartialBlockHandler, IToolHandler } from "../ToolExecutorCoordinator"
|
||||
import type { TaskConfig } from "../types/TaskConfig"
|
||||
import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
|
||||
|
||||
export class NewTaskHandler implements IToolHandler, IPartialBlockHandler {
|
||||
readonly name = "new_task"
|
||||
|
||||
readonly name = ClineDefaultTool.NEW_TASK
|
||||
constructor() {}
|
||||
|
||||
getDescription(block: ToolUse): string {
|
||||
@@ -21,7 +21,7 @@ export class NewTaskHandler implements IToolHandler, IPartialBlockHandler {
|
||||
*/
|
||||
async handlePartialBlock(block: ToolUse, uiHelpers: StronglyTypedUIHelpers): Promise<void> {
|
||||
const context = uiHelpers.removeClosingTag(block, "context", block.params.context)
|
||||
await uiHelpers.ask("new_task", context, true).catch(() => {})
|
||||
await uiHelpers.ask(this.name, context, true).catch(() => {})
|
||||
}
|
||||
|
||||
async execute(config: TaskConfig, block: ToolUse): Promise<ToolResponse> {
|
||||
@@ -44,7 +44,7 @@ export class NewTaskHandler implements IToolHandler, IPartialBlockHandler {
|
||||
}
|
||||
|
||||
// Ask user for response
|
||||
const { text, images, files: newTaskFiles } = await config.callbacks.ask("new_task", context, false)
|
||||
const { text, images, files: newTaskFiles } = await config.callbacks.ask(this.name, context, false)
|
||||
|
||||
// If the user provided a response, treat it as feedback
|
||||
if (text || (images && images.length > 0) || (newTaskFiles && newTaskFiles.length > 0)) {
|
||||
|
||||
@@ -3,13 +3,14 @@ import { formatResponse } from "@core/prompts/responses"
|
||||
import { findLast, parsePartialArrayString } from "@shared/array"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { ClinePlanModeResponse } from "@/shared/ExtensionMessage"
|
||||
import { ClineDefaultTool } from "@/shared/tools"
|
||||
import type { ToolResponse } from "../../index"
|
||||
import type { IPartialBlockHandler, IToolHandler } from "../ToolExecutorCoordinator"
|
||||
import type { TaskConfig } from "../types/TaskConfig"
|
||||
import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
|
||||
|
||||
export class PlanModeRespondHandler implements IToolHandler, IPartialBlockHandler {
|
||||
readonly name = "plan_mode_respond"
|
||||
readonly name = ClineDefaultTool.PLAN_MODE
|
||||
|
||||
constructor() {}
|
||||
|
||||
@@ -29,7 +30,7 @@ export class PlanModeRespondHandler implements IToolHandler, IPartialBlockHandle
|
||||
options: parsePartialArrayString(uiHelpers.removeClosingTag(block, "options", optionsRaw)),
|
||||
} satisfies ClinePlanModeResponse
|
||||
|
||||
await uiHelpers.ask("plan_mode_respond", JSON.stringify(sharedMessage), true).catch(() => {})
|
||||
await uiHelpers.ask(this.name, JSON.stringify(sharedMessage), true).catch(() => {})
|
||||
}
|
||||
|
||||
async execute(config: TaskConfig, block: ToolUse): Promise<ToolResponse> {
|
||||
@@ -68,7 +69,7 @@ export class PlanModeRespondHandler implements IToolHandler, IPartialBlockHandle
|
||||
text,
|
||||
images,
|
||||
files: planResponseFiles,
|
||||
} = await config.callbacks.ask("plan_mode_respond", JSON.stringify(sharedMessage), false)
|
||||
} = await config.callbacks.ask(this.name, JSON.stringify(sharedMessage), false)
|
||||
|
||||
config.taskState.isAwaitingPlanResponse = false
|
||||
|
||||
@@ -82,7 +83,7 @@ export class PlanModeRespondHandler implements IToolHandler, IPartialBlockHandle
|
||||
telemetryService.captureOptionSelected(config.ulid, options.length, "plan")
|
||||
// Valid option selected, don't show user message in UI
|
||||
// Update last plan message with selected option
|
||||
const lastPlanMessage = findLast(config.messageState.getClineMessages(), (m: any) => m.ask === "plan_mode_respond")
|
||||
const lastPlanMessage = findLast(config.messageState.getClineMessages(), (m: any) => m.ask === this.name)
|
||||
if (lastPlanMessage) {
|
||||
lastPlanMessage.text = JSON.stringify({
|
||||
...sharedMessage,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { ToolUse } from "@core/assistant-message"
|
||||
import { formatResponse } from "@core/prompts/responses"
|
||||
import { getWorkspaceBasename, resolveWorkspacePath } from "@core/workspace"
|
||||
import { extractFileContent } from "@integrations/misc/extract-file-content"
|
||||
import { getReadablePath, isLocatedInWorkspace } from "@utils/path"
|
||||
import * as path from "path"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { ClineSayTool } from "@/shared/ExtensionMessage"
|
||||
import { ClineDefaultTool } from "@/shared/tools"
|
||||
import type { ToolResponse } from "../../index"
|
||||
import { showNotificationForApprovalIfAutoApprovalEnabled } from "../../utils"
|
||||
import type { IFullyManagedTool } from "../ToolExecutorCoordinator"
|
||||
@@ -14,7 +15,7 @@ import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
|
||||
import { ToolResultUtils } from "../utils/ToolResultUtils"
|
||||
|
||||
export class ReadFileToolHandler implements IFullyManagedTool {
|
||||
readonly name = "read_file"
|
||||
readonly name = ClineDefaultTool.FILE_READ
|
||||
|
||||
constructor(private validator: ToolValidator) {}
|
||||
|
||||
@@ -54,7 +55,7 @@ export class ReadFileToolHandler implements IFullyManagedTool {
|
||||
const pathValidation = this.validator.assertRequiredParams(block, "path")
|
||||
if (!pathValidation.ok) {
|
||||
config.taskState.consecutiveMistakeCount++
|
||||
return await config.callbacks.sayAndCreateMissingParamError("read_file", "path")
|
||||
return await config.callbacks.sayAndCreateMissingParamError(this.name, "path")
|
||||
}
|
||||
|
||||
// Check clineignore access
|
||||
@@ -65,7 +66,7 @@ export class ReadFileToolHandler implements IFullyManagedTool {
|
||||
}
|
||||
|
||||
config.taskState.consecutiveMistakeCount = 0
|
||||
const absolutePath = path.resolve(config.cwd, relPath!)
|
||||
const absolutePath = resolveWorkspacePath(config.cwd, relPath!, "ReadFileToolHandler.execute")
|
||||
|
||||
// Handle approval flow
|
||||
const sharedMessageProps = {
|
||||
@@ -87,7 +88,7 @@ export class ReadFileToolHandler implements IFullyManagedTool {
|
||||
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, true, true)
|
||||
} else {
|
||||
// Manual approval flow
|
||||
const notificationMessage = `Cline wants to read ${path.basename(absolutePath)}`
|
||||
const notificationMessage = `Cline wants to read ${getWorkspaceBasename(absolutePath, "ReadFileToolHandler.notification")}`
|
||||
|
||||
// Show notification
|
||||
showNotificationForApprovalIfAutoApprovalEnabled(
|
||||
|
||||
@@ -2,17 +2,17 @@ import type { ToolUse } from "@core/assistant-message"
|
||||
import { formatResponse } from "@core/prompts/responses"
|
||||
import { processFilesIntoText } from "@integrations/misc/extract-text"
|
||||
import { showSystemNotification } from "@integrations/notifications"
|
||||
import { ClineAsk } from "@shared/ExtensionMessage"
|
||||
import { createAndOpenGitHubIssue } from "@utils/github-url-utils"
|
||||
import * as os from "os"
|
||||
import * as vscode from "vscode"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { ClineDefaultTool } from "@/shared/tools"
|
||||
import type { ToolResponse } from "../../index"
|
||||
import type { IPartialBlockHandler, IToolHandler } from "../ToolExecutorCoordinator"
|
||||
import type { TaskConfig } from "../types/TaskConfig"
|
||||
import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
|
||||
|
||||
export class ReportBugHandler implements IToolHandler, IPartialBlockHandler {
|
||||
readonly name = "report_bug"
|
||||
readonly name = ClineDefaultTool.REPORT_BUG
|
||||
|
||||
constructor() {}
|
||||
|
||||
@@ -29,7 +29,7 @@ export class ReportBugHandler implements IToolHandler, IPartialBlockHandler {
|
||||
additional_context: uiHelpers.removeClosingTag(block, "additional_context", block.params.additional_context),
|
||||
})
|
||||
|
||||
await uiHelpers.ask("report_bug" as ClineAsk, partialMessage, block.partial).catch(() => {})
|
||||
await uiHelpers.ask(this.name, partialMessage, block.partial).catch(() => {})
|
||||
}
|
||||
|
||||
async execute(config: TaskConfig, block: ToolUse): Promise<ToolResponse> {
|
||||
@@ -73,9 +73,10 @@ export class ReportBugHandler implements IToolHandler, IPartialBlockHandler {
|
||||
|
||||
// Derive system information values algorithmically
|
||||
const operatingSystem = os.platform() + " " + os.release()
|
||||
const clineVersion = vscode.extensions.getExtension("saoudrizwan.claude-dev")?.packageJSON.version || "Unknown"
|
||||
const systemInfo = `VSCode: ${vscode.version}, Node.js: ${process.version}, Architecture: ${os.arch()}`
|
||||
const currentMode = config.mode
|
||||
const clineVersion = config.context.extension.packageJSON.version
|
||||
const host = await HostProvider.env.getHostVersion({})
|
||||
const systemInfo = `${host.platform}: ${host.version}, Node.js: ${process.version}, Architecture: ${os.arch()}`
|
||||
const apiConfig = config.services.stateManager.getApiConfiguration()
|
||||
const apiProvider = currentMode === "plan" ? apiConfig.planModeApiProvider : apiConfig.actModeApiProvider
|
||||
const providerAndModel = `${apiProvider} / ${config.api.getModel().id}`
|
||||
@@ -94,7 +95,7 @@ export class ReportBugHandler implements IToolHandler, IPartialBlockHandler {
|
||||
cline_version: clineVersion,
|
||||
})
|
||||
|
||||
const { text, images, files: reportBugFiles } = await config.callbacks.ask("report_bug", bugReportData, false)
|
||||
const { text, images, files: reportBugFiles } = await config.callbacks.ask(this.name, bugReportData, false)
|
||||
|
||||
// If the user provided a response, treat it as feedback
|
||||
if (text || (images && images.length > 0) || (reportBugFiles && reportBugFiles.length > 0)) {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { ToolUse } from "@core/assistant-message"
|
||||
import { regexSearchFiles } from "@services/ripgrep"
|
||||
import { getReadablePath, isLocatedInWorkspace } from "@utils/path"
|
||||
import * as path from "path"
|
||||
import { formatResponse } from "@/core/prompts/responses"
|
||||
import { resolveWorkspacePath } from "@/core/workspace/WorkspaceResolver"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { ClineSayTool } from "@/shared/ExtensionMessage"
|
||||
import { ClineDefaultTool } from "@/shared/tools"
|
||||
import type { ToolResponse } from "../../index"
|
||||
import { showNotificationForApprovalIfAutoApprovalEnabled } from "../../utils"
|
||||
import type { IFullyManagedTool } from "../ToolExecutorCoordinator"
|
||||
@@ -14,7 +15,7 @@ import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
|
||||
import { ToolResultUtils } from "../utils/ToolResultUtils"
|
||||
|
||||
export class SearchFilesToolHandler implements IFullyManagedTool {
|
||||
readonly name = "search_files"
|
||||
readonly name = ClineDefaultTool.SEARCH
|
||||
|
||||
constructor(private validator: ToolValidator) {}
|
||||
|
||||
@@ -63,16 +64,17 @@ export class SearchFilesToolHandler implements IFullyManagedTool {
|
||||
const pathValidation = this.validator.assertRequiredParams(block, "path")
|
||||
if (!pathValidation.ok) {
|
||||
config.taskState.consecutiveMistakeCount++
|
||||
return await config.callbacks.sayAndCreateMissingParamError("search_files", "path")
|
||||
return await config.callbacks.sayAndCreateMissingParamError(this.name, "path")
|
||||
}
|
||||
|
||||
if (!regex) {
|
||||
config.taskState.consecutiveMistakeCount++
|
||||
return await config.callbacks.sayAndCreateMissingParamError("search_files", "regex")
|
||||
return await config.callbacks.sayAndCreateMissingParamError(this.name, "regex")
|
||||
}
|
||||
|
||||
config.taskState.consecutiveMistakeCount = 0
|
||||
const absolutePath = path.resolve(config.cwd, relDirPath!)
|
||||
const absolutePath = resolveWorkspacePath(config.cwd, relDirPath!, "SearchFilesTool.execute")
|
||||
|
||||
// Execute the actual regex search operation
|
||||
const results = await regexSearchFiles(
|
||||
config.cwd,
|
||||
|
||||
@@ -4,13 +4,14 @@ import { formatResponse } from "@core/prompts/responses"
|
||||
import { ensureTaskDirectoryExists } from "@core/storage/disk"
|
||||
import { ClineSayTool } from "@shared/ExtensionMessage"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { ClineDefaultTool } from "@/shared/tools"
|
||||
import type { ToolResponse } from "../../index"
|
||||
import type { IPartialBlockHandler, IToolHandler } from "../ToolExecutorCoordinator"
|
||||
import type { TaskConfig } from "../types/TaskConfig"
|
||||
import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
|
||||
|
||||
export class SummarizeTaskHandler implements IToolHandler, IPartialBlockHandler {
|
||||
readonly name = "summarize_task"
|
||||
readonly name = ClineDefaultTool.SUMMARIZE_TASK
|
||||
|
||||
constructor() {}
|
||||
|
||||
@@ -25,7 +26,7 @@ export class SummarizeTaskHandler implements IToolHandler, IPartialBlockHandler
|
||||
// Validate required parameters
|
||||
if (!context) {
|
||||
config.taskState.consecutiveMistakeCount++
|
||||
return await config.callbacks.sayAndCreateMissingParamError("summarize_task", "context")
|
||||
return await config.callbacks.sayAndCreateMissingParamError(this.name, "context")
|
||||
}
|
||||
|
||||
config.taskState.consecutiveMistakeCount = 0
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { ToolUse } from "@core/assistant-message"
|
||||
import { formatResponse } from "@core/prompts/responses"
|
||||
import { ClineAsk, ClineAskUseMcpServer } from "@shared/ExtensionMessage"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { ClineDefaultTool } from "@/shared/tools"
|
||||
import type { ToolResponse } from "../../index"
|
||||
import { showNotificationForApprovalIfAutoApprovalEnabled } from "../../utils"
|
||||
import type { IFullyManagedTool } from "../ToolExecutorCoordinator"
|
||||
@@ -10,9 +11,7 @@ import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
|
||||
import { ToolResultUtils } from "../utils/ToolResultUtils"
|
||||
|
||||
export class UseMcpToolHandler implements IFullyManagedTool {
|
||||
readonly name = "use_mcp_tool"
|
||||
|
||||
constructor() {}
|
||||
readonly name = ClineDefaultTool.MCP_USE
|
||||
|
||||
getDescription(block: ToolUse): string {
|
||||
return `[${block.name} for '${block.params.server_name}']`
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { UrlContentFetcher } from "@services/browser/UrlContentFetcher"
|
||||
import { ClineAsk, ClineSayTool } from "@shared/ExtensionMessage"
|
||||
import { ClineDefaultTool } from "@shared/tools"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { ToolUse, ToolUseName } from "../../../assistant-message"
|
||||
import { ToolUse } from "../../../assistant-message"
|
||||
import { formatResponse } from "../../../prompts/responses"
|
||||
import { ToolResponse } from "../.."
|
||||
import { showNotificationForApprovalIfAutoApprovalEnabled } from "../../utils"
|
||||
@@ -11,8 +12,7 @@ import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
|
||||
import { ToolResultUtils } from "../utils/ToolResultUtils"
|
||||
|
||||
export class WebFetchToolHandler implements IFullyManagedTool {
|
||||
name = "web_fetch"
|
||||
supportedTools: ToolUseName[] = ["web_fetch"]
|
||||
readonly name = ClineDefaultTool.WEB_FETCH
|
||||
|
||||
getDescription(block: ToolUse): string {
|
||||
return `[${block.name} for '${block.params.url}']`
|
||||
@@ -42,7 +42,7 @@ export class WebFetchToolHandler implements IFullyManagedTool {
|
||||
// Validate required parameter
|
||||
if (!url) {
|
||||
config.taskState.consecutiveMistakeCount++
|
||||
return await config.callbacks.sayAndCreateMissingParamError("web_fetch", "url")
|
||||
return await config.callbacks.sayAndCreateMissingParamError(this.name, "url")
|
||||
}
|
||||
config.taskState.consecutiveMistakeCount = 0
|
||||
|
||||
@@ -55,7 +55,7 @@ export class WebFetchToolHandler implements IFullyManagedTool {
|
||||
}
|
||||
const completeMessage = JSON.stringify(sharedMessageProps)
|
||||
|
||||
if (config.callbacks.shouldAutoApproveTool("web_fetch" as ToolUseName)) {
|
||||
if (config.callbacks.shouldAutoApproveTool(this.name)) {
|
||||
// Auto-approve flow
|
||||
await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "tool")
|
||||
await config.callbacks.say("tool", completeMessage, undefined, undefined, false)
|
||||
|
||||
@@ -2,13 +2,14 @@ import { setTimeout as setTimeoutPromise } from "node:timers/promises"
|
||||
import type { ToolUse } from "@core/assistant-message"
|
||||
import { constructNewFileContent } from "@core/assistant-message/diff"
|
||||
import { formatResponse } from "@core/prompts/responses"
|
||||
import { getWorkspaceBasename, resolveWorkspacePath } from "@core/workspace"
|
||||
import { processFilesIntoText } from "@integrations/misc/extract-text"
|
||||
import { ClineSayTool } from "@shared/ExtensionMessage"
|
||||
import { fileExistsAtPath } from "@utils/fs"
|
||||
import { getReadablePath, isLocatedInWorkspace } from "@utils/path"
|
||||
import { fixModelHtmlEscaping, removeInvalidChars } from "@utils/string"
|
||||
import * as path from "path"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { ClineDefaultTool } from "@/shared/tools"
|
||||
import type { ToolResponse } from "../../index"
|
||||
import { showNotificationForApprovalIfAutoApprovalEnabled } from "../../utils"
|
||||
import type { IFullyManagedTool } from "../ToolExecutorCoordinator"
|
||||
@@ -19,21 +20,12 @@ import { ToolDisplayUtils } from "../utils/ToolDisplayUtils"
|
||||
import { ToolResultUtils } from "../utils/ToolResultUtils"
|
||||
|
||||
export class WriteToFileToolHandler implements IFullyManagedTool {
|
||||
readonly name = "write_to_file" // This handler supports write_to_file, replace_in_file, and new_rule
|
||||
readonly name = ClineDefaultTool.FILE_NEW // This handler supports write_to_file, replace_in_file, and new_rule
|
||||
|
||||
constructor(private validator: ToolValidator) {}
|
||||
|
||||
getDescription(block: ToolUse): string {
|
||||
switch (block.name) {
|
||||
case "write_to_file":
|
||||
return `[${block.name} for '${block.params.path}']`
|
||||
case "replace_in_file":
|
||||
return `[${block.name} for '${block.params.path}']`
|
||||
case "new_rule":
|
||||
return `[${block.name} for '${block.params.path}']`
|
||||
default:
|
||||
return `[${block.name} for '${block.params.path}']`
|
||||
}
|
||||
return `[${block.name} for '${block.params.path}']`
|
||||
}
|
||||
|
||||
async handlePartialBlock(block: ToolUse, uiHelpers: StronglyTypedUIHelpers): Promise<void> {
|
||||
@@ -49,12 +41,13 @@ export class WriteToFileToolHandler implements IFullyManagedTool {
|
||||
|
||||
const config = uiHelpers.getConfig()
|
||||
|
||||
try {
|
||||
const result = await this.validateAndPrepareFileOperation(config, block, rawRelPath, rawDiff, rawContent)
|
||||
if (!result) {
|
||||
return
|
||||
}
|
||||
// Creates file if it doesn't exist, and opens editor to stream content in. We don't want to handle this in the try/catch below since the error handler for it resets the diff view, which wouldn't be open if this failed.
|
||||
const result = await this.validateAndPrepareFileOperation(config, block, rawRelPath, rawDiff, rawContent)
|
||||
if (!result) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const { relPath, fileExists, diff, content, newContent } = result
|
||||
|
||||
// Create and show partial UI message
|
||||
@@ -176,7 +169,7 @@ export class WriteToFileToolHandler implements IFullyManagedTool {
|
||||
await setTimeoutPromise(3_500)
|
||||
} else {
|
||||
// Manual approval flow with detailed feedback handling
|
||||
const notificationMessage = `Cline wants to ${fileExists ? "edit" : "create"} ${path.basename(relPath)}`
|
||||
const notificationMessage = `Cline wants to ${fileExists ? "edit" : "create"} ${getWorkspaceBasename(relPath, "WriteToFile.notification")}`
|
||||
|
||||
// Show notification
|
||||
showNotificationForApprovalIfAutoApprovalEnabled(
|
||||
@@ -331,7 +324,11 @@ export class WriteToFileToolHandler implements IFullyManagedTool {
|
||||
if (config.services.diffViewProvider.editType !== undefined) {
|
||||
fileExists = config.services.diffViewProvider.editType === "modify"
|
||||
} else {
|
||||
const absolutePath = path.resolve(config.cwd, relPath)
|
||||
const absolutePath = resolveWorkspacePath(
|
||||
config.cwd,
|
||||
relPath,
|
||||
"WriteToFileToolHandler.validateAndPrepareFileOperation",
|
||||
)
|
||||
fileExists = await fileExistsAtPath(absolutePath)
|
||||
config.services.diffViewProvider.editType = fileExists ? "modify" : "create"
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@ import type { BrowserSettings } from "@shared/BrowserSettings"
|
||||
import type { ClineAsk, ClineSay } from "@shared/ExtensionMessage"
|
||||
import type { FocusChainSettings } from "@shared/FocusChainSettings"
|
||||
import type { Mode } from "@shared/storage/types"
|
||||
import type { ClineDefaultTool } from "@shared/tools"
|
||||
import type { ClineAskResponse } from "@shared/WebviewMessage"
|
||||
import * as vscode from "vscode"
|
||||
import type { ToolUseName } from "../../../assistant-message"
|
||||
import type { ContextManager } from "../../../context/context-management/ContextManager"
|
||||
import type { StateManager } from "../../../storage/StateManager"
|
||||
import type { MessageStateHandler } from "../../message-state"
|
||||
@@ -87,7 +87,7 @@ export interface TaskCallbacks {
|
||||
|
||||
saveCheckpoint: (isAttemptCompletionMessage?: boolean, completionMessageTs?: number) => Promise<void>
|
||||
|
||||
sayAndCreateMissingParamError: (toolName: ToolUseName, paramName: string, relPath?: string) => Promise<any>
|
||||
sayAndCreateMissingParamError: (toolName: ClineDefaultTool, paramName: string, relPath?: string) => Promise<any>
|
||||
|
||||
removeLastPartialMessageIfExistsWithType: (type: "ask" | "say", askOrSay: ClineAsk | ClineSay) => Promise<void>
|
||||
|
||||
@@ -97,8 +97,8 @@ export interface TaskCallbacks {
|
||||
|
||||
updateFCListFromToolResponse: (taskProgress: string | undefined) => Promise<void>
|
||||
|
||||
shouldAutoApproveTool: (toolName: ToolUseName) => boolean | [boolean, boolean]
|
||||
shouldAutoApproveToolWithPath: (toolName: ToolUseName, path?: string) => Promise<boolean>
|
||||
shouldAutoApproveTool: (toolName: ClineDefaultTool) => boolean | [boolean, boolean]
|
||||
shouldAutoApproveToolWithPath: (toolName: ClineDefaultTool, path?: string) => Promise<boolean>
|
||||
|
||||
// Additional callbacks for task management
|
||||
postStateToWebview: () => Promise<void>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { ClineAsk, ClineSay } from "@shared/ExtensionMessage"
|
||||
import type { ClineDefaultTool } from "@shared/tools"
|
||||
import type { ClineAskResponse } from "@shared/WebviewMessage"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import type { ToolParamName, ToolUse, ToolUseName } from "../../../assistant-message"
|
||||
import type { ToolParamName, ToolUse } from "../../../assistant-message"
|
||||
import { showNotificationForApprovalIfAutoApprovalEnabled } from "../../utils"
|
||||
import { removeClosingTag } from "../utils/ToolConstants"
|
||||
import type { TaskConfig } from "./TaskConfig"
|
||||
@@ -29,12 +30,12 @@ export interface StronglyTypedUIHelpers {
|
||||
removeLastPartialMessageIfExistsWithType: (type: "ask" | "say", askOrSay: ClineAsk | ClineSay) => Promise<void>
|
||||
|
||||
// Approval methods
|
||||
shouldAutoApproveTool: (toolName: ToolUseName) => boolean | [boolean, boolean]
|
||||
shouldAutoApproveToolWithPath: (toolName: ToolUseName, path?: string) => Promise<boolean>
|
||||
shouldAutoApproveTool: (toolName: ClineDefaultTool) => boolean | [boolean, boolean]
|
||||
shouldAutoApproveToolWithPath: (toolName: ClineDefaultTool, path?: string) => Promise<boolean>
|
||||
askApproval: (messageType: ClineAsk, message: string) => Promise<boolean>
|
||||
|
||||
// Telemetry and notifications
|
||||
captureTelemetry: (toolName: ToolUseName, autoApproved: boolean, approved: boolean) => void
|
||||
captureTelemetry: (toolName: ClineDefaultTool, autoApproved: boolean, approved: boolean) => void
|
||||
showNotificationIfEnabled: (message: string) => void
|
||||
|
||||
// Config access - returns the proper typed config
|
||||
@@ -50,13 +51,13 @@ export function createUIHelpers(config: TaskConfig): StronglyTypedUIHelpers {
|
||||
ask: config.callbacks.ask,
|
||||
removeClosingTag: (block: ToolUse, tag: ToolParamName, text?: string) => removeClosingTag(block, tag, text),
|
||||
removeLastPartialMessageIfExistsWithType: config.callbacks.removeLastPartialMessageIfExistsWithType,
|
||||
shouldAutoApproveTool: (toolName: ToolUseName) => config.autoApprover.shouldAutoApproveTool(toolName),
|
||||
shouldAutoApproveTool: (toolName: ClineDefaultTool) => config.autoApprover.shouldAutoApproveTool(toolName),
|
||||
shouldAutoApproveToolWithPath: config.callbacks.shouldAutoApproveToolWithPath,
|
||||
askApproval: async (messageType: ClineAsk, message: string): Promise<boolean> => {
|
||||
const { response } = await config.callbacks.ask(messageType, message, false)
|
||||
return response === "yesButtonClicked"
|
||||
},
|
||||
captureTelemetry: (toolName: ToolUseName, autoApproved: boolean, approved: boolean) => {
|
||||
captureTelemetry: (toolName: ClineDefaultTool, autoApproved: boolean, approved: boolean) => {
|
||||
telemetryService.captureToolUsage(config.ulid, toolName, config.api.getModel().id, autoApproved, approved)
|
||||
},
|
||||
showNotificationIfEnabled: (message: string) => {
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* Handles formatting and presentation of workspace migration reports
|
||||
*
|
||||
* Separated from WorkspaceResolver to follow Single Responsibility Principle.
|
||||
* This class focuses purely on report generation and formatting.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Tracks path resolution usage for migration planning
|
||||
*/
|
||||
export interface UsageStats {
|
||||
count: number
|
||||
examples: string[]
|
||||
lastUsed: Date
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for report generation
|
||||
*/
|
||||
interface ReportOptions {
|
||||
includeExamples?: boolean
|
||||
includeHighUsage?: boolean
|
||||
highUsageThreshold?: number
|
||||
sortByUsage?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles generation and formatting of migration reports
|
||||
*/
|
||||
export class MigrationReporter {
|
||||
private readonly defaultOptions: Required<ReportOptions> = {
|
||||
includeExamples: true,
|
||||
includeHighUsage: true,
|
||||
highUsageThreshold: 100,
|
||||
sortByUsage: true,
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a comprehensive migration report from usage statistics
|
||||
* @param usageMap - Map of component names to their usage statistics
|
||||
* @param traceEnabled - Whether tracing is currently enabled
|
||||
* @param options - Optional configuration for report generation
|
||||
* @returns Formatted migration report string
|
||||
*/
|
||||
generateReport(usageMap: Map<string, UsageStats>, traceEnabled: boolean, options: ReportOptions = {}): string {
|
||||
const config = { ...this.defaultOptions, ...options }
|
||||
const entries = this.prepareEntries(usageMap, config.sortByUsage)
|
||||
|
||||
let report = this.generateHeader(entries.length, traceEnabled)
|
||||
report += this.generateComponentDetails(entries, config)
|
||||
report += this.generateSummary(entries)
|
||||
|
||||
if (config.includeHighUsage) {
|
||||
report += this.generateHighUsageSection(entries, config.highUsageThreshold)
|
||||
}
|
||||
|
||||
return report
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a simplified summary report
|
||||
* @param usageMap - Map of component names to their usage statistics
|
||||
* @returns Brief summary string
|
||||
*/
|
||||
generateSummary(entries: Array<[string, UsageStats]>): string {
|
||||
const totalCalls = entries.reduce((sum, [_, stats]) => sum + stats.count, 0)
|
||||
|
||||
let summary = `\n=== Summary ===\n`
|
||||
summary += `Total path resolution calls: ${totalCalls}\n`
|
||||
|
||||
return summary
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare and optionally sort the usage entries
|
||||
*/
|
||||
private prepareEntries(usageMap: Map<string, UsageStats>, sortByUsage: boolean): Array<[string, UsageStats]> {
|
||||
const entries = Array.from(usageMap.entries())
|
||||
|
||||
if (sortByUsage) {
|
||||
return entries.sort((a, b) => b[1].count - a[1].count)
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the report header section
|
||||
*/
|
||||
private generateHeader(componentCount: number, traceEnabled: boolean): string {
|
||||
let header = "=== Multi-Root Migration Report ===\n"
|
||||
header += `Total components using single-root: ${componentCount}\n`
|
||||
header += `Trace enabled: ${traceEnabled}\n\n`
|
||||
return header
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate detailed component usage information
|
||||
*/
|
||||
private generateComponentDetails(entries: Array<[string, UsageStats]>, config: Required<ReportOptions>): string {
|
||||
let details = ""
|
||||
|
||||
entries.forEach(([context, stats]) => {
|
||||
details += `${context}:\n`
|
||||
details += ` Calls: ${stats.count}\n`
|
||||
details += ` Last used: ${stats.lastUsed.toISOString()}\n`
|
||||
|
||||
if (config.includeExamples && stats.examples.length > 0) {
|
||||
details += ` Example paths:\n`
|
||||
stats.examples.forEach((ex) => {
|
||||
details += ` - "${ex}"\n`
|
||||
})
|
||||
}
|
||||
details += "\n"
|
||||
})
|
||||
|
||||
return details
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate high-usage components section
|
||||
*/
|
||||
private generateHighUsageSection(entries: Array<[string, UsageStats]>, threshold: number): string {
|
||||
const highUsageComponents = entries
|
||||
.filter(([_, stats]) => stats.count > threshold)
|
||||
.map(([context, stats]) => ({ context, count: stats.count }))
|
||||
|
||||
if (highUsageComponents.length === 0) {
|
||||
return ""
|
||||
}
|
||||
|
||||
let section = `\n=== High-Usage Components ===\n`
|
||||
section += `(Operations with >${threshold} calls)\n`
|
||||
highUsageComponents.forEach((h) => {
|
||||
section += ` - ${h.context}: ${h.count} calls\n`
|
||||
})
|
||||
|
||||
return section
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a JSON representation of the usage data
|
||||
* @param usageMap - Map of component names to their usage statistics
|
||||
* @returns JSON string representation
|
||||
*/
|
||||
generateJsonReport(usageMap: Map<string, UsageStats>): string {
|
||||
const data = Object.fromEntries(usageMap)
|
||||
return JSON.stringify(data, null, 2)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a CSV representation of the usage data
|
||||
* @param usageMap - Map of component names to their usage statistics
|
||||
* @returns CSV string representation
|
||||
*/
|
||||
generateCsvReport(usageMap: Map<string, UsageStats>): string {
|
||||
const entries = Array.from(usageMap.entries())
|
||||
let csv = "Component,Calls,LastUsed,ExamplePaths\n"
|
||||
|
||||
entries.forEach(([context, stats]) => {
|
||||
const examples = stats.examples.join("; ")
|
||||
csv += `"${context}",${stats.count},"${stats.lastUsed.toISOString()}","${examples}"\n`
|
||||
})
|
||||
|
||||
return csv
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
/**
|
||||
* Workspace path resolution with migration tracing for multi-workspace support
|
||||
*
|
||||
* Phase 0: Acts as a tracer to identify all single-root path operations
|
||||
* Phase 1+: Will handle multi-root path resolution
|
||||
*/
|
||||
|
||||
import { Logger } from "@services/logging/Logger"
|
||||
import * as path from "path"
|
||||
import { MigrationReporter, type UsageStats } from "./MigrationReporter"
|
||||
import { WorkspaceRoot } from "./WorkspaceRoot"
|
||||
|
||||
/**
|
||||
* Maximum number of example paths to store per component for debugging purposes.
|
||||
* This limit prevents excessive memory usage while providing enough examples
|
||||
* to understand usage patterns during migration analysis.
|
||||
*/
|
||||
const MAX_EXAMPLE_PATHS = 5
|
||||
|
||||
export class WorkspaceResolver {
|
||||
private usageMap = new Map<string, UsageStats>()
|
||||
private traceEnabled = process.env.MULTI_ROOT_TRACE === "true" || process.env.NODE_ENV === "development"
|
||||
|
||||
/**
|
||||
* Track usage statistics for a given context and path
|
||||
* @param context - Component/handler name for tracking usage
|
||||
* @param examplePath - The path to track as an example
|
||||
*/
|
||||
private trackUsage(context: string, examplePath: string): void {
|
||||
const stats = this.usageMap.get(context) || {
|
||||
count: 0,
|
||||
examples: [],
|
||||
lastUsed: new Date(),
|
||||
}
|
||||
|
||||
stats.count++
|
||||
stats.lastUsed = new Date()
|
||||
|
||||
// Keep up to MAX_EXAMPLE_PATHS example paths for debugging
|
||||
if (stats.examples.length < MAX_EXAMPLE_PATHS && !stats.examples.includes(examplePath)) {
|
||||
stats.examples.push(examplePath)
|
||||
}
|
||||
|
||||
this.usageMap.set(context, stats)
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 0: Traces single-root path resolution for migration planning
|
||||
* Phase 1+: Will resolve path against multiple workspace roots
|
||||
*
|
||||
* @param cwdOrRoots - Current working directory (Phase 0) or array of workspace roots (Phase 1+)
|
||||
* @param relativePath - The relative path to resolve
|
||||
* @param context - Component/handler name for tracking usage
|
||||
* @returns Absolute path (Phase 0) or object with path and root (Phase 1+)
|
||||
*/
|
||||
resolveWorkspacePath(
|
||||
cwdOrRoots: string | WorkspaceRoot[],
|
||||
relativePath: string,
|
||||
context?: string,
|
||||
): string | { absolutePath: string; root: WorkspaceRoot } {
|
||||
// Phase 0: Single-root tracer mode
|
||||
if (typeof cwdOrRoots === "string") {
|
||||
return this.resolveSingleRootPath(cwdOrRoots, relativePath, context)
|
||||
}
|
||||
|
||||
// Phase 1+: Multi-root resolution
|
||||
return this.resolveMultiRootPath(cwdOrRoots, relativePath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a path against a single workspace root (Phase 0)
|
||||
*
|
||||
* @param cwd - Current working directory
|
||||
* @param relativePath - The relative path to resolve
|
||||
* @param context - Component/handler name for tracking usage
|
||||
* @returns Absolute path
|
||||
*/
|
||||
private resolveSingleRootPath(cwd: string, relativePath: string, context?: string): string {
|
||||
// Track usage for migration planning
|
||||
if (context) {
|
||||
this.trackUsage(context, relativePath)
|
||||
|
||||
if (this.traceEnabled) {
|
||||
Logger.debug(`[MULTI-ROOT-TRACE] ${context}: resolving "${relativePath}" against "${cwd}"`)
|
||||
}
|
||||
}
|
||||
|
||||
return path.resolve(cwd, relativePath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a path against multiple workspace roots (Phase 1+)
|
||||
*
|
||||
* @param workspaceRoots - Array of workspace roots
|
||||
* @param relativePath - The relative path to resolve
|
||||
* @returns Object with absolute path and matching root
|
||||
*/
|
||||
private resolveMultiRootPath(
|
||||
workspaceRoots: WorkspaceRoot[],
|
||||
relativePath: string,
|
||||
): { absolutePath: string; root: WorkspaceRoot } {
|
||||
// Handle absolute paths
|
||||
if (path.isAbsolute(relativePath)) {
|
||||
return this.resolveAbsolutePath(workspaceRoots, relativePath)
|
||||
}
|
||||
|
||||
// Handle relative paths
|
||||
return this.resolveRelativePath(workspaceRoots, relativePath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves an absolute path against workspace roots
|
||||
*
|
||||
* @param workspaceRoots - Array of workspace roots
|
||||
* @param absolutePath - The absolute path to resolve
|
||||
* @returns Object with absolute path and matching root
|
||||
*/
|
||||
private resolveAbsolutePath(
|
||||
workspaceRoots: WorkspaceRoot[],
|
||||
absolutePath: string,
|
||||
): { absolutePath: string; root: WorkspaceRoot } {
|
||||
const matchingRoot = workspaceRoots.find((root) => absolutePath.startsWith(root.path))
|
||||
return {
|
||||
absolutePath,
|
||||
root: matchingRoot || workspaceRoots[0], // fallback to primary
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a relative path against workspace roots
|
||||
*
|
||||
* @param workspaceRoots - Array of workspace roots
|
||||
* @param relativePath - The relative path to resolve
|
||||
* @returns Object with absolute path and matching root
|
||||
*/
|
||||
private resolveRelativePath(
|
||||
workspaceRoots: WorkspaceRoot[],
|
||||
relativePath: string,
|
||||
): { absolutePath: string; root: WorkspaceRoot } {
|
||||
// Check which roots have this relative path
|
||||
const candidateRoots: WorkspaceRoot[] = []
|
||||
for (const root of workspaceRoots) {
|
||||
// const testPath = path.join(root.path, relativePath)
|
||||
// In Phase 1, check if path exists
|
||||
// For now, just add all roots as candidates
|
||||
candidateRoots.push(root)
|
||||
}
|
||||
|
||||
return this.selectBestRoot(workspaceRoots, candidateRoots, relativePath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the best root from candidate roots using disambiguation logic
|
||||
*
|
||||
* @param workspaceRoots - All available workspace roots
|
||||
* @param candidateRoots - Candidate roots that could contain the path
|
||||
* @param relativePath - The relative path being resolved
|
||||
* @returns Object with absolute path and selected root
|
||||
*/
|
||||
private selectBestRoot(
|
||||
workspaceRoots: WorkspaceRoot[],
|
||||
candidateRoots: WorkspaceRoot[],
|
||||
relativePath: string,
|
||||
): { absolutePath: string; root: WorkspaceRoot } {
|
||||
// Disambiguation logic (simplified for Phase 0)
|
||||
if (candidateRoots.length === 0) {
|
||||
// Path doesn't exist in any root, use primary
|
||||
return {
|
||||
absolutePath: path.resolve(workspaceRoots[0].path, relativePath),
|
||||
root: workspaceRoots[0],
|
||||
}
|
||||
}
|
||||
|
||||
if (candidateRoots.length === 1) {
|
||||
// Unambiguous
|
||||
return {
|
||||
absolutePath: path.resolve(candidateRoots[0].path, relativePath),
|
||||
root: candidateRoots[0],
|
||||
}
|
||||
}
|
||||
|
||||
// Multiple matches - need disambiguation
|
||||
// Phase 2: This will trigger UI picker
|
||||
// For now, use primary root if it's a candidate, otherwise first match
|
||||
const primaryRoot = workspaceRoots[0]
|
||||
const selectedRoot = candidateRoots.find((r) => r.path === primaryRoot.path) || candidateRoots[0]
|
||||
|
||||
return {
|
||||
absolutePath: path.resolve(selectedRoot.path, relativePath),
|
||||
root: selectedRoot,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get migration report showing all single-root usage patterns
|
||||
* Currently this function is mainly called by the vscode debugger
|
||||
*/
|
||||
getMigrationReport(): string {
|
||||
const reporter = new MigrationReporter()
|
||||
return reporter.generateReport(this.usageMap, this.traceEnabled)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get raw usage statistics for external analysis
|
||||
* @returns Map of component names to their usage statistics
|
||||
*/
|
||||
getUsageStats(): Map<string, UsageStats> {
|
||||
return new Map(this.usageMap)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear usage statistics (useful for testing)
|
||||
*/
|
||||
clearUsageStats(): void {
|
||||
this.usageMap.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Export usage data as JSON for analysis
|
||||
*/
|
||||
exportUsageData(): Record<string, UsageStats> {
|
||||
return Object.fromEntries(this.usageMap)
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 0: Instance method for getting basename with tracking
|
||||
* Phase 1+: Will handle basename for multi-workspace paths
|
||||
*
|
||||
* @param filePath - The file path to get basename from
|
||||
* @param context - Component/handler name for tracking usage
|
||||
* @returns The basename of the path
|
||||
*/
|
||||
getBasename(filePath: string, context?: string): string {
|
||||
// Track usage for migration planning
|
||||
if (!context?.length) {
|
||||
return path.basename(filePath)
|
||||
}
|
||||
this.trackUsage(context, filePath)
|
||||
|
||||
// Phase 0: Just wrap existing behavior
|
||||
const result = path.basename(filePath)
|
||||
|
||||
if (this.traceEnabled) {
|
||||
Logger.debug(`[MULTI-ROOT-TRACE] ${context}: getting basename for "${filePath}"`)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const workspaceResolver = new WorkspaceResolver()
|
||||
|
||||
/**
|
||||
* Phase 0: Convenience function for easy migration from path.resolve()
|
||||
* This is what we'll use to replace existing path.resolve(cwd, ...) calls
|
||||
*/
|
||||
export function resolveWorkspacePath(cwd: string, relativePath: string, context?: string): string {
|
||||
return workspaceResolver.resolveWorkspacePath(cwd, relativePath, context) as string
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to check if we're in trace mode
|
||||
*/
|
||||
export function isWorkspaceTraceEnabled(): boolean {
|
||||
return process.env.MULTI_ROOT_TRACE === "true" || process.env.NODE_ENV === "development"
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 0: Convenience function for path.basename with tracking
|
||||
* This is what we'll use to replace existing path.basename() calls
|
||||
*/
|
||||
export function getWorkspaceBasename(filePath: string, context?: string): string {
|
||||
return workspaceResolver.getBasename(filePath, context)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Workspace root types and interfaces for multi-workspace support
|
||||
*/
|
||||
|
||||
export enum VcsType {
|
||||
None = "none",
|
||||
Git = "git",
|
||||
Mercurial = "mercurial",
|
||||
}
|
||||
|
||||
export interface WorkspaceRoot {
|
||||
path: string // Absolute path to the workspace root
|
||||
name?: string // Optional display name for the workspace (auto-derived from path if not provided)
|
||||
vcs: VcsType // Version control system type for this root
|
||||
commitHash?: string // Optional latest commit hash/changeset ID for VCS tracking
|
||||
}
|
||||
|
||||
// Example usage:
|
||||
// const workspaceRoots: WorkspaceRoot[] = [
|
||||
// {
|
||||
// path: "/Users/dev/frontend",
|
||||
// name: "frontend",
|
||||
// vcs: VcsType.Git,
|
||||
// commitHash: "a1b2c3d4e5f6789"
|
||||
// },
|
||||
// {
|
||||
// path: "/Users/dev/backend",
|
||||
// name: "backend",
|
||||
// vcs: VcsType.Git,
|
||||
// commitHash: "f6e5d4c3b2a1987"
|
||||
// }
|
||||
// ]
|
||||
@@ -0,0 +1,261 @@
|
||||
/**
|
||||
* WorkspaceRootManager - Central manager for multi-workspace operations
|
||||
* This class handles workspace root resolution, path mapping, and workspace context
|
||||
*/
|
||||
|
||||
import { execa } from "execa"
|
||||
import * as path from "path"
|
||||
import { getLatestGitCommitHash } from "../../utils/git"
|
||||
import { VcsType, WorkspaceRoot } from "./WorkspaceRoot"
|
||||
|
||||
export interface WorkspaceContext {
|
||||
workspaceRoots: WorkspaceRoot[]
|
||||
primaryRoot: WorkspaceRoot
|
||||
currentRoot?: WorkspaceRoot
|
||||
}
|
||||
|
||||
export class WorkspaceRootManager {
|
||||
private roots: WorkspaceRoot[] = []
|
||||
private primaryIndex: number = 0
|
||||
|
||||
constructor(roots: WorkspaceRoot[] = [], primaryIndex: number = 0) {
|
||||
this.roots = roots
|
||||
this.primaryIndex = Math.min(primaryIndex, Math.max(0, roots.length - 1))
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize from a single cwd for backward compatibility
|
||||
*/
|
||||
static async fromLegacyCwd(cwd: string): Promise<WorkspaceRootManager> {
|
||||
const vcs = await WorkspaceRootManager.detectVcs(cwd)
|
||||
const gitHash = vcs === VcsType.Git ? await getLatestGitCommitHash(cwd) : null
|
||||
const commitHash = gitHash === null ? undefined : gitHash
|
||||
|
||||
const root: WorkspaceRoot = {
|
||||
path: cwd,
|
||||
name: path.basename(cwd),
|
||||
vcs,
|
||||
commitHash,
|
||||
}
|
||||
|
||||
return new WorkspaceRootManager([root], 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect version control system for a directory
|
||||
*/
|
||||
private static async detectVcs(dirPath: string): Promise<VcsType> {
|
||||
try {
|
||||
// Check for Git
|
||||
await execa("git", ["rev-parse", "--git-dir"], { cwd: dirPath })
|
||||
return VcsType.Git
|
||||
} catch {
|
||||
// Not a git repo
|
||||
}
|
||||
|
||||
try {
|
||||
// Check for Mercurial
|
||||
await execa("hg", ["root"], { cwd: dirPath })
|
||||
return VcsType.Mercurial
|
||||
} catch {
|
||||
// Not a mercurial repo
|
||||
}
|
||||
|
||||
return VcsType.None
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new workspace root
|
||||
*/
|
||||
async addRoot(rootPath: string, name?: string): Promise<void> {
|
||||
const vcs = await WorkspaceRootManager.detectVcs(rootPath)
|
||||
const gitHash = vcs === VcsType.Git ? await getLatestGitCommitHash(rootPath) : null
|
||||
const commitHash = gitHash === null ? undefined : gitHash
|
||||
|
||||
const root: WorkspaceRoot = {
|
||||
path: rootPath,
|
||||
name: name || path.basename(rootPath),
|
||||
vcs,
|
||||
commitHash,
|
||||
}
|
||||
|
||||
this.roots.push(root)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a workspace root by path
|
||||
*/
|
||||
removeRoot(path: string): boolean {
|
||||
const index = this.roots.findIndex((r) => r.path === path)
|
||||
if (index === -1) {
|
||||
return false
|
||||
}
|
||||
|
||||
this.roots.splice(index, 1)
|
||||
|
||||
// Adjust primary index if needed
|
||||
if (this.primaryIndex >= this.roots.length) {
|
||||
this.primaryIndex = Math.max(0, this.roots.length - 1)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all workspace roots
|
||||
*/
|
||||
getRoots(): WorkspaceRoot[] {
|
||||
return [...this.roots]
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the primary workspace root
|
||||
*/
|
||||
getPrimaryRoot(): WorkspaceRoot | undefined {
|
||||
return this.roots[this.primaryIndex]
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the primary workspace root by index
|
||||
*/
|
||||
setPrimaryIndex(index: number): void {
|
||||
if (index >= 0 && index < this.roots.length) {
|
||||
this.primaryIndex = index
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the workspace root that contains the given absolute path
|
||||
*/
|
||||
resolvePathToRoot(absolutePath: string): WorkspaceRoot | undefined {
|
||||
// Sort roots by path length (longest first) to handle nested workspaces
|
||||
const sortedRoots = [...this.roots].sort((a, b) => b.path.length - a.path.length)
|
||||
|
||||
for (const root of sortedRoots) {
|
||||
if (absolutePath.startsWith(root.path)) {
|
||||
return root
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Find workspace root by name
|
||||
*/
|
||||
getRootByName(name: string): WorkspaceRoot | undefined {
|
||||
return this.roots.find((r) => r.name === name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get workspace root by index
|
||||
*/
|
||||
getRootByIndex(index: number): WorkspaceRoot | undefined {
|
||||
return this.roots[index]
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a path is within any workspace root
|
||||
*/
|
||||
isPathInWorkspace(absolutePath: string): boolean {
|
||||
return this.resolvePathToRoot(absolutePath) !== undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Get relative path from workspace root
|
||||
*/
|
||||
getRelativePathFromRoot(absolutePath: string, root?: WorkspaceRoot): string | undefined {
|
||||
const targetRoot = root || this.resolvePathToRoot(absolutePath)
|
||||
if (!targetRoot) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return path.relative(targetRoot.path, absolutePath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create workspace context for tool execution
|
||||
*/
|
||||
createContext(currentRoot?: WorkspaceRoot): WorkspaceContext {
|
||||
return {
|
||||
workspaceRoots: this.getRoots(),
|
||||
primaryRoot: this.getPrimaryRoot()!,
|
||||
currentRoot: currentRoot || this.getPrimaryRoot(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize for storage
|
||||
*/
|
||||
toJSON(): { roots: WorkspaceRoot[]; primaryIndex: number } {
|
||||
return {
|
||||
roots: this.roots,
|
||||
primaryIndex: this.primaryIndex,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize from storage
|
||||
*/
|
||||
static fromJSON(data: { roots: WorkspaceRoot[]; primaryIndex: number }): WorkspaceRootManager {
|
||||
return new WorkspaceRootManager(data.roots, data.primaryIndex)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a summary string for display
|
||||
*/
|
||||
getSummary(): string {
|
||||
if (this.roots.length === 0) {
|
||||
return "No workspace roots configured"
|
||||
}
|
||||
|
||||
if (this.roots.length === 1) {
|
||||
return `Single workspace: ${this.roots[0].name || this.roots[0].path}`
|
||||
}
|
||||
|
||||
const primary = this.getPrimaryRoot()
|
||||
return `Multi-workspace (${this.roots.length} roots)\nPrimary: ${primary?.name || primary?.path}\nAdditional: ${this.roots
|
||||
.filter((_, i) => i !== this.primaryIndex)
|
||||
.map((r) => r.name || path.basename(r.path))
|
||||
.join(", ")}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a single-root workspace (for backward compatibility)
|
||||
*/
|
||||
isSingleRoot(): boolean {
|
||||
return this.roots.length === 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the single root if this is a single-root workspace
|
||||
* Throws if multiple roots exist
|
||||
*/
|
||||
getSingleRoot(): WorkspaceRoot {
|
||||
if (this.roots.length !== 1) {
|
||||
throw new Error(`Expected single root, but found ${this.roots.length} roots`)
|
||||
}
|
||||
return this.roots[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* Update commit hashes for all Git repositories
|
||||
*/
|
||||
async updateCommitHashes(): Promise<void> {
|
||||
for (const root of this.roots) {
|
||||
if (root.vcs === VcsType.Git) {
|
||||
const gitHash = await getLatestGitCommitHash(root.path)
|
||||
root.commitHash = gitHash === null ? undefined : gitHash
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in Task and Controller
|
||||
export function createLegacyWorkspaceRoot(cwd: string): WorkspaceRoot {
|
||||
return {
|
||||
path: cwd,
|
||||
name: path.basename(cwd),
|
||||
vcs: VcsType.None, // Will be detected properly during initialization
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* Unit tests for WorkspaceResolver
|
||||
* These tests ensure behavior preservation during refactoring
|
||||
*/
|
||||
|
||||
import { expect } from "chai"
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import * as path from "path"
|
||||
import * as sinon from "sinon"
|
||||
import { Logger } from "../../../services/logging/Logger"
|
||||
import { WorkspaceResolver } from "../WorkspaceResolver"
|
||||
import { VcsType, WorkspaceRoot } from "../WorkspaceRoot"
|
||||
|
||||
describe("WorkspaceResolver", () => {
|
||||
let resolver: WorkspaceResolver
|
||||
let loggerStub: sinon.SinonStub
|
||||
let originalEnv: string | undefined
|
||||
|
||||
beforeEach(() => {
|
||||
resolver = new WorkspaceResolver()
|
||||
loggerStub = sinon.stub(Logger, "debug")
|
||||
originalEnv = process.env.MULTI_ROOT_TRACE
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
loggerStub.restore()
|
||||
process.env.MULTI_ROOT_TRACE = originalEnv
|
||||
resolver.clearUsageStats()
|
||||
})
|
||||
|
||||
describe("resolveWorkspacePath - Single Root Mode", () => {
|
||||
const testCwd = "/test/workspace"
|
||||
const testRelativePath = "src/file.ts"
|
||||
const expectedAbsolutePath = path.resolve(testCwd, testRelativePath)
|
||||
|
||||
it("should resolve path without context", () => {
|
||||
const result = resolver.resolveWorkspacePath(testCwd, testRelativePath)
|
||||
expect(result).to.equal(expectedAbsolutePath)
|
||||
})
|
||||
|
||||
it("should resolve path with context and track usage", () => {
|
||||
const context = "TestComponent"
|
||||
const result = resolver.resolveWorkspacePath(testCwd, testRelativePath, context)
|
||||
|
||||
expect(result).to.equal(expectedAbsolutePath)
|
||||
|
||||
// Verify usage tracking
|
||||
const usageStats = resolver.getUsageStats()
|
||||
expect(usageStats.has(context)).to.be.true
|
||||
|
||||
const stats = usageStats.get(context)!
|
||||
expect(stats.count).to.equal(1)
|
||||
expect(stats.examples).to.include(testRelativePath)
|
||||
})
|
||||
|
||||
it("should track multiple calls to same context", () => {
|
||||
const context = "TestComponent"
|
||||
|
||||
resolver.resolveWorkspacePath(testCwd, "file1.ts", context)
|
||||
resolver.resolveWorkspacePath(testCwd, "file2.ts", context)
|
||||
resolver.resolveWorkspacePath(testCwd, "file1.ts", context) // duplicate
|
||||
|
||||
const usageStats = resolver.getUsageStats()
|
||||
const stats = usageStats.get(context)!
|
||||
|
||||
expect(stats.count).to.equal(3)
|
||||
expect(stats.examples).to.have.length(2) // no duplicates in examples
|
||||
expect(stats.examples).to.include("file1.ts")
|
||||
expect(stats.examples).to.include("file2.ts")
|
||||
})
|
||||
|
||||
it("should not log when tracing is disabled", () => {
|
||||
process.env.MULTI_ROOT_TRACE = "false"
|
||||
process.env.NODE_ENV = "production"
|
||||
|
||||
resolver.resolveWorkspacePath(testCwd, testRelativePath, "TestComponent")
|
||||
|
||||
expect(loggerStub.called).to.be.false
|
||||
})
|
||||
|
||||
it("should handle absolute paths correctly", () => {
|
||||
const absolutePath = "/absolute/path/file.ts"
|
||||
const result = resolver.resolveWorkspacePath(testCwd, absolutePath)
|
||||
|
||||
expect(result).to.equal(path.resolve(testCwd, absolutePath))
|
||||
})
|
||||
|
||||
it("should handle empty relative path", () => {
|
||||
const result = resolver.resolveWorkspacePath(testCwd, "")
|
||||
expect(result).to.equal(path.resolve(testCwd))
|
||||
})
|
||||
|
||||
it("should handle relative paths with .. navigation", () => {
|
||||
const relativePath = "../other/file.ts"
|
||||
const result = resolver.resolveWorkspacePath(testCwd, relativePath)
|
||||
expect(result).to.equal(path.resolve(testCwd, relativePath))
|
||||
})
|
||||
})
|
||||
|
||||
describe("resolveWorkspacePath - Multi Root Mode", () => {
|
||||
const workspaceRoots: WorkspaceRoot[] = [
|
||||
{ path: "/workspace/primary", name: "primary", vcs: VcsType.Git },
|
||||
{ path: "/workspace/secondary", name: "secondary", vcs: VcsType.Git },
|
||||
]
|
||||
|
||||
it("should handle absolute paths in multi-root mode", () => {
|
||||
const absolutePath = "/workspace/primary/src/file.ts"
|
||||
const result = resolver.resolveWorkspacePath(workspaceRoots, absolutePath)
|
||||
|
||||
expect(result).to.be.an("object")
|
||||
expect((result as any).absolutePath).to.equal(absolutePath)
|
||||
expect((result as any).root).to.equal(workspaceRoots[0])
|
||||
})
|
||||
|
||||
it("should fallback to primary root for unmatched absolute paths", () => {
|
||||
const absolutePath = "/other/path/file.ts"
|
||||
const result = resolver.resolveWorkspacePath(workspaceRoots, absolutePath)
|
||||
|
||||
expect(result).to.be.an("object")
|
||||
expect((result as any).absolutePath).to.equal(absolutePath)
|
||||
expect((result as any).root).to.equal(workspaceRoots[0])
|
||||
})
|
||||
|
||||
it("should resolve relative paths against primary root", () => {
|
||||
const relativePath = "src/file.ts"
|
||||
const result = resolver.resolveWorkspacePath(workspaceRoots, relativePath)
|
||||
|
||||
expect(result).to.be.an("object")
|
||||
expect((result as any).absolutePath).to.equal(path.resolve(workspaceRoots[0].path, relativePath))
|
||||
expect((result as any).root).to.equal(workspaceRoots[0])
|
||||
})
|
||||
|
||||
it("should handle empty workspace roots array", () => {
|
||||
// This should throw an error or handle gracefully
|
||||
expect(() => {
|
||||
resolver.resolveWorkspacePath([], "src/file.ts")
|
||||
}).to.throw()
|
||||
})
|
||||
})
|
||||
|
||||
describe("getBasename", () => {
|
||||
const testFilePath = "/path/to/file.ts"
|
||||
const expectedBasename = "file.ts"
|
||||
|
||||
it("should return basename without context", () => {
|
||||
const result = resolver.getBasename(testFilePath)
|
||||
expect(result).to.equal(expectedBasename)
|
||||
})
|
||||
|
||||
it("should return basename with context and track usage", () => {
|
||||
const context = "TestComponent"
|
||||
const result = resolver.getBasename(testFilePath, context)
|
||||
|
||||
expect(result).to.equal(expectedBasename)
|
||||
|
||||
// Verify usage tracking
|
||||
const usageStats = resolver.getUsageStats()
|
||||
expect(usageStats.has(context)).to.be.true
|
||||
|
||||
const stats = usageStats.get(context)!
|
||||
expect(stats.count).to.equal(1)
|
||||
expect(stats.examples).to.include(testFilePath)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Usage Statistics Management", () => {
|
||||
it("should track usage statistics correctly", () => {
|
||||
resolver.resolveWorkspacePath("/test", "file1.ts", "Component1")
|
||||
resolver.resolveWorkspacePath("/test", "file2.ts", "Component1")
|
||||
resolver.getBasename("/test/file3.ts", "Component2")
|
||||
|
||||
const usageStats = resolver.getUsageStats()
|
||||
expect(usageStats.size).to.equal(2)
|
||||
|
||||
const component1Stats = usageStats.get("Component1")!
|
||||
expect(component1Stats.count).to.equal(2)
|
||||
expect(component1Stats.examples).to.have.length(2)
|
||||
|
||||
const component2Stats = usageStats.get("Component2")!
|
||||
expect(component2Stats.count).to.equal(1)
|
||||
expect(component2Stats.examples).to.have.length(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Edge Cases", () => {
|
||||
it("should handle null/undefined inputs gracefully", () => {
|
||||
// These should not throw
|
||||
expect(() => resolver.resolveWorkspacePath("/test", "")).to.not.throw()
|
||||
expect(() => resolver.getBasename("")).to.not.throw()
|
||||
})
|
||||
|
||||
it("should handle special characters in paths", () => {
|
||||
const specialPath = "src/file with spaces & symbols!.ts"
|
||||
const result = resolver.resolveWorkspacePath("/test", specialPath, "Component")
|
||||
|
||||
expect(result).to.equal(path.resolve("/test", specialPath))
|
||||
})
|
||||
|
||||
it("should handle very long paths", () => {
|
||||
const longPath = "a/".repeat(100) + "file.ts"
|
||||
const result = resolver.resolveWorkspacePath("/test", longPath, "Component")
|
||||
|
||||
expect(result).to.equal(path.resolve("/test", longPath))
|
||||
})
|
||||
})
|
||||
})
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user