mirror of
https://github.com/cline/cline.git
synced 2026-09-06 12:28:08 +08:00
Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3ea552271d | |||
| eb91bfd738 | |||
| b8227c19c3 | |||
| 8fee09f09e | |||
| 1c026c26d2 | |||
| 5bc4e5a4a0 | |||
| 2cfce5734e | |||
| e2045bf5c3 | |||
| 0d933e804f | |||
| 16f73532f4 | |||
| 88bea8eeb4 | |||
| 1a570e98ba | |||
| d86b7dd036 | |||
| 0178c3fa90 | |||
| a107f45c6a | |||
| 3dd2ed9161 | |||
| 9a6603fdfb | |||
| 6d5c3e6aa4 | |||
| 24b9e821bb | |||
| 9960a3c57c | |||
| 88947592f0 | |||
| ef4d11df19 | |||
| 23dec509bc | |||
| 07ab6b19b8 | |||
| 0ddef94d1f | |||
| b9ae83b1cd | |||
| e4eaf34827 | |||
| 6d3ed43c74 | |||
| cbb67b48f2 | |||
| a5f6a97be8 | |||
| f309b062e7 | |||
| 768df130ab | |||
| 9980cb0938 | |||
| aca4f842fa | |||
| 3fc91e2afe | |||
| 5f4700ce95 | |||
| dbaf5e3ee3 | |||
| 576176c24f | |||
| c8abcbfdf9 | |||
| 8e984f2d98 | |||
| 81564faa4e | |||
| f8b5f1fd72 | |||
| 2eb57384ab | |||
| c80bae504a | |||
| 80f955be9e | |||
| 7435ffcd2f | |||
| a05d438612 | |||
| b4b7512d9f | |||
| e08c65618e | |||
| d653f1cc27 | |||
| c3a97c3eda | |||
| 0e56272d65 | |||
| fdc2e2655a | |||
| 6050413b8b | |||
| c54f0da737 | |||
| 6cbfb2b8b0 | |||
| 22788f0f12 | |||
| 708b785a97 | |||
| 099bc44d42 | |||
| b9f4678dba | |||
| f7d17384f6 | |||
| bb5a64afb3 | |||
| 61224734f8 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix input box positioning issue in chat view.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
DeepSeek R1 0528 support under Hugging Face
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fixed token counting when using VSCode LM API provider
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: only focus on editor panel that is visible and active to stop input field stealing issue
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
trim input value for URL fields
|
||||
@@ -716,7 +716,7 @@ The Controller class manages MCP servers through the McpHub service:
|
||||
class Controller {
|
||||
mcpHub?: McpHub
|
||||
|
||||
constructor(context: vscode.ExtensionContext, outputChannel: vscode.OutputChannel, webviewProvider: WebviewProvider) {
|
||||
constructor(context: vscode.ExtensionContext, webviewProvider: WebviewProvider) {
|
||||
this.mcpHub = new McpHub(this)
|
||||
}
|
||||
|
||||
|
||||
+12
-15
@@ -96,16 +96,15 @@ jobs:
|
||||
- name: Build Tests and Extension
|
||||
run: npm run pretest
|
||||
|
||||
# Unit Tests disabled due to module system conflicts between backend and webview-ui
|
||||
# - name: Unit Tests
|
||||
# run: npm run test:unit
|
||||
- name: Unit Tests
|
||||
run: npm run test:unit
|
||||
|
||||
# Run extension tests with coverage
|
||||
- name: Extension Tests with Coverage
|
||||
- name: Extension Integration Tests with Coverage
|
||||
id: extension_coverage
|
||||
continue-on-error: true
|
||||
run: |
|
||||
node ./scripts/test-ci.js > extension_coverage.txt 2>&1
|
||||
node ./scripts/test-ci.js 2>&1 | tee extension_coverage.txt
|
||||
# Default the encoding to UTF-8 - It's not the default on Windows
|
||||
PYTHONUTF8=1 PYTHONPATH=.github/scripts python -m coverage_check extract-coverage extension_coverage.txt --type=extension --github-output --verbose
|
||||
|
||||
@@ -117,7 +116,7 @@ jobs:
|
||||
cd webview-ui
|
||||
# Ensure coverage dependency is installed
|
||||
npm install --no-save @vitest/coverage-v8
|
||||
npm run test:coverage > webview_coverage.txt 2>&1
|
||||
npm run test:coverage 2>&1 | tee webview_coverage.txt
|
||||
cd ..
|
||||
# Default the encoding to UTF-8 - It's not the default on Windows
|
||||
PYTHONUTF8=1 PYTHONPATH=.github/scripts python -m coverage_check extract-coverage webview-ui/webview_coverage.txt --type=webview --github-output --verbose
|
||||
@@ -132,21 +131,19 @@ jobs:
|
||||
path: |
|
||||
extension_coverage.txt
|
||||
webview-ui/webview_coverage.txt
|
||||
retention-period: workflow # Artifacts are automatically deleted when the workflow completes
|
||||
|
||||
# Set the check as failed if any of the tests failed
|
||||
- name: Print test results and check for failures
|
||||
- name: Check for test failures
|
||||
run: |
|
||||
echo "Extension Tests Result: ${{ steps.extension_coverage.outcome }}"
|
||||
cat extension_coverage.txt
|
||||
|
||||
echo "Webview Tests Result: ${{ steps.webview_coverage.outcome }}"
|
||||
cat webview-ui/webview_coverage.txt
|
||||
|
||||
# Check if any of the test steps failed
|
||||
# https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs#steps-context
|
||||
if [ "${{ steps.extension_coverage.outcome }}" != "success" ]; then
|
||||
echo "Extension Integration Tests failed, see previous step for test output."
|
||||
fi
|
||||
if [ "${{ steps.webview_coverage.outcome }}" != "success" ]; then
|
||||
echo "Webview Tests failed, see previous step for test output."
|
||||
fi
|
||||
if [ "${{ steps.extension_coverage.outcome }}" != "success" ] || [ "${{ steps.webview_coverage.outcome }}" != "success" ]; then
|
||||
echo "Tests failed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,5 +1,38 @@
|
||||
# Changelog
|
||||
|
||||
## [3.20.8]
|
||||
|
||||
- Add navbar tooltips on hover
|
||||
|
||||
## [3.20.7]
|
||||
|
||||
- Fix circular dependency that affect the github workflow Tests / test (pull_request)
|
||||
|
||||
## [3.20.6]
|
||||
|
||||
- Fix login check on extension restart
|
||||
|
||||
## [3.20.5]
|
||||
|
||||
- Fix authentication persistence issues that could cause users to be logged out unexpectedly
|
||||
|
||||
## [3.20.4]
|
||||
|
||||
- Add new Cerebras models
|
||||
- Update rate limits for existing Cerebras models
|
||||
- Fix for delete task dialog
|
||||
|
||||
## [3.20.3]
|
||||
|
||||
- Add Huawei Cloud MaaS Provider (Thanks @ddling!)
|
||||
- Add Cerebras Qwen 3 235B instruct model (Thanks @kevint-cerebras!)
|
||||
- Add DeepSeek R1 0528 support under Hugging Face (Thanks @0ne0rZer0!)
|
||||
- Fix Global Rules directory documentation for Linux/WSL systems
|
||||
- Fix token counting when using VSCode LM API provider
|
||||
- Fix input field stealing focus issue by only focusing on visible and active editor panels
|
||||
- Fix duplicate tool registration for claude4-experimental
|
||||
- Trim input value for URL fields
|
||||
|
||||
## [3.20.2]
|
||||
|
||||
- Fixed issue with sap ai core client credentials storage
|
||||
|
||||
@@ -11,7 +11,19 @@ You can create a rule by clicking the `+` button in the Rules tab. This will ope
|
||||
Once you save the file:
|
||||
|
||||
- Your rule will be stored in the `.clinerules/` directory in your project (if it's a Workspace Rule)
|
||||
- Or in the `Documents/Cline/Rules` directory (if it's a Global Rule).
|
||||
- Or in the Global Rules directory (if it's a Global Rule):
|
||||
|
||||
### Global Rules Directory Location
|
||||
|
||||
The location of your Global Rules directory depends on your operating system:
|
||||
|
||||
| Operating System | Default Location | Notes |
|
||||
|------------------|------------------|-------|
|
||||
| **Windows** | `Documents\Cline\Rules` | Uses system Documents folder |
|
||||
| **macOS** | `~/Documents/Cline/Rules` | Uses user Documents folder |
|
||||
| **Linux/WSL** | `~/Documents/Cline/Rules` | May fall back to `~/Cline/Rules` on some systems |
|
||||
|
||||
> **Note for Linux/WSL users**: If you don't find your global rules in `~/Documents/Cline/Rules`, check `~/Cline/Rules` as the location may vary depending on your system configuration and whether the Documents directory exists.
|
||||
|
||||
You can also have Cline create a rule for you by using the [`/newrule` slash command](/features/slash-commands/new-rule) in the chat.
|
||||
|
||||
|
||||
@@ -4,17 +4,17 @@ title: "Telemetry"
|
||||
|
||||
### Overview
|
||||
|
||||
To help make Cline better for everyone, we collect anonymous usage data that helps us understand how developers are using our open-source AI coding agent. This feedback loop is crucial for improving Cline's capabilities and user experience.
|
||||
To help make Cline better for everyone, we collect usage data that helps us understand how developers are using our open-source AI coding agent. This feedback loop is crucial for improving Cline's capabilities and user experience.
|
||||
|
||||
We use PostHog, an open-source analytics platform, for data collection and analysis. Our telemetry implementation is fully transparent - you can review the [source code](https://github.com/cline/cline/blob/main/src/services/posthog/telemetry/TelemetryService.ts) to see exactly what we track.
|
||||
|
||||
### Tracking Policy
|
||||
|
||||
Privacy is our priority. All collected data is anonymized before being sent to PostHog, with no personally identifiable information (PII) included. Your code, prompts, and conversation content always remain private and are never collected.
|
||||
Privacy is our priority. By default, all collected data is anonymized. If you log in with a Cline account, your telemetry data will be associated with your account to help us improve the product and provide better support when you encounter issues. Your code, prompts, and conversation content always remain private and are never collected.
|
||||
|
||||
### What We Track
|
||||
|
||||
We collect basic anonymous usage data including:
|
||||
We collect basic usage data including:
|
||||
|
||||
**Task Interactions:** When tasks start and finish, conversation flow (without content)\
|
||||
**Mode and Tool Usage:** Switches between plan/act modes, which tools are being used\
|
||||
@@ -28,7 +28,7 @@ For complete transparency, you can inspect our [telemetry implementation](https:
|
||||
|
||||
Telemetry in Cline is entirely optional:
|
||||
|
||||
- When you update or install our VS Code extension, you'll see a message about our anonymous telemetry
|
||||
- When you update or install our VS Code extension, you'll see a message about our telemetry
|
||||
- You can change your preference anytime in settings
|
||||
|
||||
Cline also respects VS Code's global telemetry settings. If you've disabled telemetry at the VS Code level, Cline's telemetry will automatically be disabled as well.
|
||||
|
||||
@@ -143,7 +143,6 @@ const baseConfig = {
|
||||
format: "cjs",
|
||||
sourcesContent: false,
|
||||
platform: "node",
|
||||
define: { "import.meta.url": "_importMetaUrl" },
|
||||
banner: {
|
||||
js: "const _importMetaUrl=require('url').pathToFileURL(__filename)",
|
||||
},
|
||||
|
||||
@@ -34,25 +34,30 @@ directApiRuleTester.run("no-direct-vscode-api", noDirectVscodeApiRule, {
|
||||
},
|
||||
// Should allow other vscode API calls
|
||||
{
|
||||
code: `vscode.window.showInformationMessage("Hello")`,
|
||||
filename: "test.ts",
|
||||
code: `vscode.commands.registerCommand("Hello")`,
|
||||
filename: "/foo/bar.ts",
|
||||
},
|
||||
// Should allow postMessage calls on other objects
|
||||
{
|
||||
code: `window.postMessage({ type: "test" }, "*")`,
|
||||
filename: "test.ts",
|
||||
filename: "/foo/bar.ts",
|
||||
},
|
||||
// Should allow variables named vscode but not calling postMessage
|
||||
{
|
||||
code: `const vscode = { other: "method" }; vscode.other()`,
|
||||
filename: "test.ts",
|
||||
filename: "/foo/bar.ts",
|
||||
},
|
||||
// Should allow vscode.postMessage in test files
|
||||
{
|
||||
code: `vscode.postMessage({ type: "newTask", text: message.text })`,
|
||||
filename: "/foo/bar.test.ts",
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
// Should disallow vscode.postMessage in regular files
|
||||
{
|
||||
code: `vscode.postMessage({ type: "test", data: {} })`,
|
||||
filename: "test.ts",
|
||||
filename: "/foo/bar.ts",
|
||||
errors: [
|
||||
{
|
||||
messageId: "useGrpcClient",
|
||||
@@ -69,23 +74,13 @@ directApiRuleTester.run("no-direct-vscode-api", noDirectVscodeApiRule, {
|
||||
},
|
||||
],
|
||||
},
|
||||
// Should disallow vscode.postMessage in test files
|
||||
{
|
||||
code: `vscode.postMessage({ type: "newTask", text: message.text })`,
|
||||
filename: "test.test.ts",
|
||||
errors: [
|
||||
{
|
||||
messageId: "useGrpcClient",
|
||||
},
|
||||
],
|
||||
},
|
||||
// Should disallow property access for disallowed APIs
|
||||
{
|
||||
code: `const folders = vscode.workspace.workspaceFolders;`,
|
||||
filename: "workspace.ts",
|
||||
errors: [
|
||||
{
|
||||
messageId: "useHostBridge",
|
||||
messageId: "useHostBridgeWorkspace",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -35,18 +35,24 @@ const disallowedApis = {
|
||||
"vscode.env.openExternal": {
|
||||
messageId: "useUtils",
|
||||
},
|
||||
// "vscode.window.showWarningMessage": {
|
||||
// messageId: "useHostBridgeShowMessage",
|
||||
// },
|
||||
"vscode.window.showWarningMessage": {
|
||||
messageId: "useHostBridgeShowMessage",
|
||||
},
|
||||
"vscode.window.showOpenDialog": {
|
||||
messageId: "useHostBridgeShowMessage",
|
||||
},
|
||||
"vscode.window.showErrorMessage": {
|
||||
messageId: "useHostBridgeShowMessage",
|
||||
},
|
||||
// "vscode.window.showInformationMessage": {
|
||||
// messageId: "useHostBridgeShowMessage",
|
||||
// },
|
||||
"vscode.window.showInformationMessage": {
|
||||
messageId: "useHostBridgeShowMessage",
|
||||
},
|
||||
"vscode.window.showInputBox": {
|
||||
messageId: "useHostBridge",
|
||||
},
|
||||
"vscode.workspace.findFiles": {
|
||||
messageId: "useNative",
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = createRule({
|
||||
@@ -87,6 +93,10 @@ module.exports = createRule({
|
||||
"Use utilities in @/utils instead of calling vscode APIs directly.\n" +
|
||||
"This provides a consistent abstraction across VSCode and standalone environments.\n" +
|
||||
"Found: {{code}}",
|
||||
useNative:
|
||||
"Use a native Javascript API instead of calling the vscode API.\n" +
|
||||
"This provides a consistent abstraction across VSCode and standalone environments.\n" +
|
||||
"Found: {{code}}",
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
@@ -187,7 +197,7 @@ module.exports = createRule({
|
||||
if (filename.includes("/standalone/runtime-files/")) {
|
||||
return true
|
||||
}
|
||||
// Skip unit tests
|
||||
// Skip checking test files
|
||||
if (filename.endsWith(".test.ts")) {
|
||||
return true
|
||||
}
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-dev",
|
||||
"version": "3.20.1",
|
||||
"version": "3.20.5",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-dev",
|
||||
"version": "3.20.1",
|
||||
"version": "3.20.5",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.37.0",
|
||||
|
||||
+3
-2
@@ -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.20.2",
|
||||
"version": "3.20.8",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
@@ -119,7 +119,8 @@
|
||||
{
|
||||
"type": "webview",
|
||||
"id": "claude-dev.SidebarProvider",
|
||||
"name": ""
|
||||
"name": "",
|
||||
"icon": "assets/icons/icon.svg"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
+21
-6
@@ -1,17 +1,32 @@
|
||||
import { defineConfig } from "@playwright/test"
|
||||
|
||||
const isGitHubAction = !!process.env.CI
|
||||
const isCI = !!process?.env?.CI
|
||||
const isWindow = process?.platform?.startsWith("win")
|
||||
|
||||
export default defineConfig({
|
||||
workers: 1,
|
||||
retries: 1,
|
||||
testDir: "src/test/e2e",
|
||||
timeout: 20000,
|
||||
timeout: isCI || isWindow ? 40000 : 20000,
|
||||
expect: {
|
||||
timeout: 20000,
|
||||
timeout: isCI || isWindow ? 5000 : 2000,
|
||||
},
|
||||
fullyParallel: true,
|
||||
reporter: isGitHubAction ? [["github"], ["list"]] : [["list"]],
|
||||
globalSetup: require.resolve("./src/test/e2e/utils/setup"),
|
||||
globalTeardown: require.resolve("./src/test/e2e/utils/teardown"),
|
||||
reporter: isCI ? [["github"], ["list"]] : [["list"]],
|
||||
projects: [
|
||||
{
|
||||
name: "setup test environment",
|
||||
testMatch: /global\.setup\.ts/,
|
||||
},
|
||||
{
|
||||
name: "e2e tests",
|
||||
testMatch: /.*\.test\.ts/,
|
||||
dependencies: ["setup test environment"],
|
||||
},
|
||||
{
|
||||
name: "cleanup test environment",
|
||||
testMatch: /global\.teardown\.ts/,
|
||||
dependencies: ["e2e tests"],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
@@ -129,6 +129,7 @@ enum ApiProvider {
|
||||
CLAUDE_CODE = 26;
|
||||
MOONSHOT = 27;
|
||||
HUGGINGFACE = 28;
|
||||
HUAWEI_CLOUD_MAAS = 29;
|
||||
}
|
||||
|
||||
// Model info for OpenAI-compatible models
|
||||
@@ -229,6 +230,7 @@ message ModelsApiConfiguration {
|
||||
optional string cline_account_id = 58;
|
||||
optional string groq_api_key = 59;
|
||||
optional string hugging_face_api_key = 60;
|
||||
optional string huawei_cloud_maas_api_key = 61;
|
||||
|
||||
// Plan mode configurations
|
||||
optional ApiProvider plan_mode_api_provider = 100;
|
||||
@@ -255,6 +257,8 @@ message ModelsApiConfiguration {
|
||||
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;
|
||||
|
||||
// Act mode configurations
|
||||
optional ApiProvider act_mode_api_provider = 200;
|
||||
@@ -281,6 +285,8 @@ message ModelsApiConfiguration {
|
||||
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;
|
||||
|
||||
repeated string favorited_model_ids = 300;
|
||||
}
|
||||
|
||||
+27
-17
@@ -6,14 +6,14 @@ option java_multiple_files = true;
|
||||
|
||||
service StateService {
|
||||
rpc getLatestState(EmptyRequest) returns (State);
|
||||
rpc updateTerminalConnectionTimeout(Int64Request) returns (Int64);
|
||||
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);
|
||||
rpc resetState(ResetStateRequest) returns (Empty);
|
||||
rpc togglePlanActMode(TogglePlanActModeRequest) returns (Boolean);
|
||||
rpc togglePlanActModeProto(TogglePlanActModeRequest) returns (Boolean);
|
||||
rpc updateAutoApprovalSettings(AutoApprovalSettingsRequest) returns (Empty);
|
||||
rpc updateSettings(UpdateSettingsRequest) returns (Empty);
|
||||
rpc updateTelemetrySetting(TelemetrySettingRequest) returns (Empty);
|
||||
@@ -43,7 +43,7 @@ message TerminalProfileUpdateResponse {
|
||||
|
||||
message TogglePlanActModeRequest {
|
||||
Metadata metadata = 1;
|
||||
ChatSettings chat_settings = 2;
|
||||
PlanActMode mode = 2;
|
||||
optional ChatContent chat_content = 3;
|
||||
}
|
||||
|
||||
@@ -52,12 +52,6 @@ enum PlanActMode {
|
||||
ACT = 1;
|
||||
}
|
||||
|
||||
message ChatSettings {
|
||||
PlanActMode mode = 1;
|
||||
optional string preferred_language = 2;
|
||||
optional string open_ai_reasoning_effort = 3;
|
||||
}
|
||||
|
||||
message ChatContent {
|
||||
optional string message = 1;
|
||||
repeated string images = 2;
|
||||
@@ -108,12 +102,15 @@ message UpdateSettingsRequest {
|
||||
optional bool plan_act_separate_models_setting = 4;
|
||||
optional bool enable_checkpoints_setting = 5;
|
||||
optional bool mcp_marketplace_enabled = 6;
|
||||
optional ChatSettings chat_settings = 7;
|
||||
optional int64 shell_integration_timeout = 8;
|
||||
optional int32 shell_integration_timeout = 8;
|
||||
optional bool terminal_reuse_enabled = 9;
|
||||
optional bool mcp_responses_collapsed = 10;
|
||||
optional string mcp_display_mode = 11;
|
||||
optional int64 terminal_output_line_limit = 12;
|
||||
optional int32 terminal_output_line_limit = 12;
|
||||
optional PlanActMode mode = 13;
|
||||
optional string preferred_language = 14;
|
||||
optional string openai_reasoning_effort = 15;
|
||||
optional bool strict_plan_mode_enabled = 16;
|
||||
}
|
||||
|
||||
// Complete API Configuration message
|
||||
@@ -153,8 +150,8 @@ message ApiConfiguration {
|
||||
optional string requesty_api_key = 32;
|
||||
optional string together_api_key = 33;
|
||||
optional string fireworks_api_key = 34;
|
||||
optional int64 fireworks_model_max_completion_tokens = 35;
|
||||
optional int64 fireworks_model_max_tokens = 36;
|
||||
optional int32 fireworks_model_max_completion_tokens = 35;
|
||||
optional int32 fireworks_model_max_tokens = 36;
|
||||
optional string qwen_api_key = 37;
|
||||
optional string doubao_api_key = 38;
|
||||
optional string mistral_api_key = 39;
|
||||
@@ -166,7 +163,7 @@ message ApiConfiguration {
|
||||
optional string xai_api_key = 45;
|
||||
optional string sambanova_api_key = 46;
|
||||
optional string cerebras_api_key = 47;
|
||||
optional int64 request_timeout_ms = 48;
|
||||
optional int32 request_timeout_ms = 48;
|
||||
optional string sap_ai_core_client_id = 49;
|
||||
optional string sap_ai_core_client_secret = 50;
|
||||
optional string sap_ai_resource_group = 51;
|
||||
@@ -174,11 +171,12 @@ message ApiConfiguration {
|
||||
optional string sap_ai_core_base_url = 53;
|
||||
optional string moonshot_api_key = 54;
|
||||
optional string moonshot_api_line = 55;
|
||||
optional string huawei_cloud_maas_api_key = 56;
|
||||
|
||||
// Plan mode configurations
|
||||
optional string plan_mode_api_provider = 100;
|
||||
optional string plan_mode_api_model_id = 101;
|
||||
optional int64 plan_mode_thinking_budget_tokens = 102;
|
||||
optional int32 plan_mode_thinking_budget_tokens = 102;
|
||||
optional string plan_mode_reasoning_effort = 103;
|
||||
optional string plan_mode_vscode_lm_model_selector = 104; // JSON string
|
||||
optional bool plan_mode_aws_bedrock_custom_selected = 105;
|
||||
@@ -196,11 +194,13 @@ message ApiConfiguration {
|
||||
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_huawei_cloud_maas_model_id = 120;
|
||||
optional string plan_mode_huawei_cloud_maas_model_info = 121;
|
||||
|
||||
// Act mode configurations
|
||||
optional string act_mode_api_provider = 200;
|
||||
optional string act_mode_api_model_id = 201;
|
||||
optional int64 act_mode_thinking_budget_tokens = 202;
|
||||
optional int32 act_mode_thinking_budget_tokens = 202;
|
||||
optional string act_mode_reasoning_effort = 203;
|
||||
optional string act_mode_vscode_lm_model_selector = 204; // JSON string
|
||||
optional bool act_mode_aws_bedrock_custom_selected = 205;
|
||||
@@ -218,6 +218,8 @@ message ApiConfiguration {
|
||||
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_huawei_cloud_maas_model_id = 220;
|
||||
optional string act_mode_huawei_cloud_maas_model_info = 221;
|
||||
|
||||
// Favorited model IDs
|
||||
repeated string favorited_model_ids = 300;
|
||||
@@ -228,3 +230,11 @@ message ApiConfiguration {
|
||||
|
||||
optional string cline_account_id = 303;
|
||||
}
|
||||
|
||||
message UpdateTerminalConnectionTimeoutRequest {
|
||||
optional int32 timeout_ms = 1;
|
||||
}
|
||||
|
||||
message UpdateTerminalConnectionTimeoutResponse {
|
||||
optional int32 timeout_ms = 1;
|
||||
}
|
||||
|
||||
@@ -84,6 +84,8 @@ message ShowSaveDialogRequest {
|
||||
|
||||
message ShowSaveDialogOptions {
|
||||
optional string default_path = 1;
|
||||
// A map of file types to extensions, e.g
|
||||
// "Text Files": { "extensions": ["txt", "md"] }
|
||||
map<string, FileExtensionList> filters = 2;
|
||||
}
|
||||
|
||||
@@ -92,6 +94,7 @@ message FileExtensionList {
|
||||
}
|
||||
|
||||
message ShowSaveDialogResponse {
|
||||
// If the user cancelled the dialog, this will be empty.
|
||||
optional string selected_path = 1;
|
||||
}
|
||||
|
||||
@@ -129,4 +132,4 @@ message GetVisibleTabsRequest {
|
||||
|
||||
message GetVisibleTabsResponse {
|
||||
repeated string paths = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,14 @@ package host;
|
||||
option java_package = "bot.cline.host.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "cline/common.proto";
|
||||
|
||||
// Provides methods for working with workspaces/projects.
|
||||
service WorkspaceService {
|
||||
// Returns a list of the top level directories of the workspace.
|
||||
rpc getWorkspacePaths(GetWorkspacePathsRequest) returns (GetWorkspacePathsResponse);
|
||||
// Saves an open document if it's dirty
|
||||
rpc saveOpenDocumentIfDirty(SaveOpenDocumentIfDirtyRequest) returns (cline.Empty);
|
||||
// Saves an open document if it's open in the editor and has unsaved changes.
|
||||
// Returns true if the document was saved, returns false if the document was not found, or did not
|
||||
// need to be saved.
|
||||
rpc saveOpenDocumentIfDirty(SaveOpenDocumentIfDirtyRequest) returns (SaveOpenDocumentIfDirtyResponse);
|
||||
}
|
||||
|
||||
message GetWorkspacePathsRequest {
|
||||
@@ -28,6 +28,9 @@ message GetWorkspacePathsResponse {
|
||||
}
|
||||
|
||||
message SaveOpenDocumentIfDirtyRequest {
|
||||
cline.Metadata metadata = 1;
|
||||
string file_path = 2;
|
||||
optional string file_path = 2;
|
||||
}
|
||||
message SaveOpenDocumentIfDirtyResponse {
|
||||
// Returns true if the document was saved.
|
||||
optional bool was_saved = 1;
|
||||
}
|
||||
|
||||
Regular → Executable
+1
@@ -1,3 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
const { execSync } = require("child_process")
|
||||
const esbuild = require("esbuild")
|
||||
|
||||
|
||||
Regular → Executable
+10
-1
@@ -29,8 +29,9 @@ import { SapAiCoreHandler } from "./providers/sapaicore"
|
||||
import { ClaudeCodeHandler } from "./providers/claude-code"
|
||||
import { MoonshotHandler } from "./providers/moonshot"
|
||||
import { GroqHandler } from "./providers/groq"
|
||||
import { Mode } from "../shared/ChatSettings"
|
||||
import { Mode } from "@shared/storage/types"
|
||||
import { HuggingFaceHandler } from "./providers/huggingface"
|
||||
import { HuaweiCloudMaaSHandler } from "./providers/huawei-cloud-maas"
|
||||
|
||||
export interface ApiHandler {
|
||||
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream
|
||||
@@ -272,6 +273,14 @@ function createHandlerForProvider(
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
})
|
||||
case "huawei-cloud-maas":
|
||||
return new HuaweiCloudMaaSHandler({
|
||||
huaweiCloudMaasApiKey: options.huaweiCloudMaasApiKey,
|
||||
huaweiCloudMaasModelId:
|
||||
mode === "plan" ? options.planModeHuaweiCloudMaasModelId : options.actModeHuaweiCloudMaasModelId,
|
||||
huaweiCloudMaasModelInfo:
|
||||
mode === "plan" ? options.planModeHuaweiCloudMaasModelInfo : options.actModeHuaweiCloudMaasModelInfo,
|
||||
})
|
||||
default:
|
||||
return new AnthropicHandler({
|
||||
apiKey: options.apiKey,
|
||||
|
||||
@@ -612,101 +612,102 @@ describe("AwsBedrockHandler", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("getModelId", () => {
|
||||
it("should return raw model ID for custom models", async () => {
|
||||
const customOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
actModeAwsBedrockCustomSelected: true,
|
||||
actModeApiModelId:
|
||||
"arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
|
||||
}
|
||||
const customHandler = new AwsBedrockHandler(customOptions)
|
||||
// TODO: Re-enable or remove these tests.
|
||||
// describe("getModelId", () => {
|
||||
// it("should return raw model ID for custom models", async () => {
|
||||
// const customOptions: ApiHandlerOptions = {
|
||||
// ...mockOptions,
|
||||
// actModeAwsBedrockCustomSelected: true,
|
||||
// actModeApiModelId:
|
||||
// "arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
|
||||
// }
|
||||
// const customHandler = new AwsBedrockHandler(customOptions)
|
||||
|
||||
const modelId = await customHandler.getModelId()
|
||||
modelId.should.equal(
|
||||
"arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
|
||||
)
|
||||
})
|
||||
// const modelId = await customHandler.getModelId()
|
||||
// modelId.should.equal(
|
||||
// "arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
|
||||
// )
|
||||
// })
|
||||
|
||||
it("should not encode custom model IDs with slashes", async () => {
|
||||
const customOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
actModeAwsBedrockCustomSelected: true,
|
||||
actModeApiModelId: "my-namespace/my-custom-model",
|
||||
}
|
||||
const customHandler = new AwsBedrockHandler(customOptions)
|
||||
// it("should not encode custom model IDs with slashes", async () => {
|
||||
// const customOptions: ApiHandlerOptions = {
|
||||
// ...mockOptions,
|
||||
// actModeAwsBedrockCustomSelected: true,
|
||||
// actModeApiModelId: "my-namespace/my-custom-model",
|
||||
// }
|
||||
// const customHandler = new AwsBedrockHandler(customOptions)
|
||||
|
||||
const modelId = await customHandler.getModelId()
|
||||
modelId.should.equal("my-namespace/my-custom-model")
|
||||
modelId.should.not.match(/%2F/)
|
||||
})
|
||||
// const modelId = await customHandler.getModelId()
|
||||
// modelId.should.equal("my-namespace/my-custom-model")
|
||||
// modelId.should.not.match(/%2F/)
|
||||
// })
|
||||
|
||||
it("should apply cross-region prefix for non-custom models when enabled", async () => {
|
||||
const crossRegionOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
awsUseCrossRegionInference: true,
|
||||
awsRegion: "us-west-2",
|
||||
}
|
||||
const crossRegionHandler = new AwsBedrockHandler(crossRegionOptions)
|
||||
// it("should apply cross-region prefix for non-custom models when enabled", async () => {
|
||||
// const crossRegionOptions: ApiHandlerOptions = {
|
||||
// ...mockOptions,
|
||||
// awsUseCrossRegionInference: true,
|
||||
// awsRegion: "us-west-2",
|
||||
// }
|
||||
// const crossRegionHandler = new AwsBedrockHandler(crossRegionOptions)
|
||||
|
||||
const modelId = await crossRegionHandler.getModelId()
|
||||
modelId.should.equal("us.anthropic.claude-3-7-sonnet-20250219-v1:0")
|
||||
})
|
||||
// const modelId = await crossRegionHandler.getModelId()
|
||||
// modelId.should.equal("us.anthropic.claude-3-7-sonnet-20250219-v1:0")
|
||||
// })
|
||||
|
||||
it("should apply EU cross-region prefix", async () => {
|
||||
const euOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
awsUseCrossRegionInference: true,
|
||||
awsRegion: "eu-central-1",
|
||||
}
|
||||
const euHandler = new AwsBedrockHandler(euOptions)
|
||||
// it("should apply EU cross-region prefix", async () => {
|
||||
// const euOptions: ApiHandlerOptions = {
|
||||
// ...mockOptions,
|
||||
// awsUseCrossRegionInference: true,
|
||||
// awsRegion: "eu-central-1",
|
||||
// }
|
||||
// const euHandler = new AwsBedrockHandler(euOptions)
|
||||
|
||||
const modelId = await euHandler.getModelId()
|
||||
modelId.should.equal("eu.anthropic.claude-3-7-sonnet-20250219-v1:0")
|
||||
})
|
||||
// const modelId = await euHandler.getModelId()
|
||||
// modelId.should.equal("eu.anthropic.claude-3-7-sonnet-20250219-v1:0")
|
||||
// })
|
||||
|
||||
it("should apply APAC cross-region prefix", async () => {
|
||||
const apacOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
awsUseCrossRegionInference: true,
|
||||
awsRegion: "ap-northeast-1",
|
||||
}
|
||||
const apacHandler = new AwsBedrockHandler(apacOptions)
|
||||
// it("should apply APAC cross-region prefix", async () => {
|
||||
// const apacOptions: ApiHandlerOptions = {
|
||||
// ...mockOptions,
|
||||
// awsUseCrossRegionInference: true,
|
||||
// awsRegion: "ap-northeast-1",
|
||||
// }
|
||||
// const apacHandler = new AwsBedrockHandler(apacOptions)
|
||||
|
||||
const modelId = await apacHandler.getModelId()
|
||||
modelId.should.equal("apac.anthropic.claude-3-7-sonnet-20250219-v1:0")
|
||||
})
|
||||
// const modelId = await apacHandler.getModelId()
|
||||
// modelId.should.equal("apac.anthropic.claude-3-7-sonnet-20250219-v1:0")
|
||||
// })
|
||||
|
||||
it("should not apply cross-region prefix for custom models even when enabled", async () => {
|
||||
const customCrossRegionOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
actModeAwsBedrockCustomSelected: true,
|
||||
actModeApiModelId: "arn:aws:bedrock:us-west-2:123456789012:custom-model/my-model",
|
||||
awsUseCrossRegionInference: true,
|
||||
}
|
||||
const customCrossRegionHandler = new AwsBedrockHandler(customCrossRegionOptions)
|
||||
// it("should not apply cross-region prefix for custom models even when enabled", async () => {
|
||||
// const customCrossRegionOptions: ApiHandlerOptions = {
|
||||
// ...mockOptions,
|
||||
// actModeAwsBedrockCustomSelected: true,
|
||||
// actModeApiModelId: "arn:aws:bedrock:us-west-2:123456789012:custom-model/my-model",
|
||||
// awsUseCrossRegionInference: true,
|
||||
// }
|
||||
// const customCrossRegionHandler = new AwsBedrockHandler(customCrossRegionOptions)
|
||||
|
||||
const modelId = await customCrossRegionHandler.getModelId()
|
||||
modelId.should.equal("arn:aws:bedrock:us-west-2:123456789012:custom-model/my-model")
|
||||
})
|
||||
// const modelId = await customCrossRegionHandler.getModelId()
|
||||
// modelId.should.equal("arn:aws:bedrock:us-west-2:123456789012:custom-model/my-model")
|
||||
// })
|
||||
|
||||
it("should handle UltraThink model ARN correctly", async () => {
|
||||
const ultraThinkOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
actModeAwsBedrockCustomSelected: true,
|
||||
actModeApiModelId:
|
||||
"arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
|
||||
actModeAwsBedrockCustomModelBaseId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
|
||||
}
|
||||
const ultraThinkHandler = new AwsBedrockHandler(ultraThinkOptions)
|
||||
// it("should handle UltraThink model ARN correctly", async () => {
|
||||
// const ultraThinkOptions: ApiHandlerOptions = {
|
||||
// ...mockOptions,
|
||||
// actModeAwsBedrockCustomSelected: true,
|
||||
// actModeApiModelId:
|
||||
// "arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
|
||||
// actModeAwsBedrockCustomModelBaseId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
|
||||
// }
|
||||
// const ultraThinkHandler = new AwsBedrockHandler(ultraThinkOptions)
|
||||
|
||||
const modelId = await ultraThinkHandler.getModelId()
|
||||
// Should return the raw ARN without any encoding
|
||||
modelId.should.equal(
|
||||
"arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
|
||||
)
|
||||
modelId.should.not.match(/%2F/)
|
||||
modelId.should.not.match(/%3A/)
|
||||
})
|
||||
})
|
||||
// const modelId = await ultraThinkHandler.getModelId()
|
||||
// // Should return the raw ARN without any encoding
|
||||
// modelId.should.equal(
|
||||
// "arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
|
||||
// )
|
||||
// modelId.should.not.match(/%2F/)
|
||||
// modelId.should.not.match(/%3A/)
|
||||
// })
|
||||
// })
|
||||
})
|
||||
|
||||
@@ -102,6 +102,7 @@ export class CerebrasHandler implements ApiHandler {
|
||||
messages: cerebrasMessages,
|
||||
temperature: 0,
|
||||
stream: true,
|
||||
max_tokens: this.getModel().info.maxTokens,
|
||||
})
|
||||
|
||||
// Handle streaming response
|
||||
@@ -175,9 +176,15 @@ export class CerebrasHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
getModel(): { id: string; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
if (modelId && modelId in cerebrasModels) {
|
||||
const id = modelId as CerebrasModelId
|
||||
const originalModelId = this.options.apiModelId
|
||||
let apiModelId = originalModelId
|
||||
if (originalModelId === "qwen-3-coder-480b-free") {
|
||||
apiModelId = "qwen-3-coder-480b"
|
||||
return { id: apiModelId, info: cerebrasModels[originalModelId as CerebrasModelId] }
|
||||
}
|
||||
|
||||
if (originalModelId && originalModelId in cerebrasModels) {
|
||||
const id = originalModelId as CerebrasModelId
|
||||
return { id, info: cerebrasModels[id] }
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// Mock for @google/genai module to avoid ESM compatibility issues in tests
|
||||
|
||||
export class GoogleGenAI {
|
||||
constructor(options: any) {
|
||||
// Mock constructor
|
||||
}
|
||||
|
||||
models = {
|
||||
generateContentStream: async (params: any) => {
|
||||
// Mock implementation that returns an async iterator
|
||||
return {
|
||||
async *[Symbol.asyncIterator]() {
|
||||
yield {
|
||||
text: "Mock response",
|
||||
candidates: [],
|
||||
usageMetadata: {
|
||||
promptTokenCount: 100,
|
||||
candidatesTokenCount: 50,
|
||||
thoughtsTokenCount: 0,
|
||||
cachedContentTokenCount: 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
countTokens: async (params: any) => {
|
||||
// Mock token counting
|
||||
return {
|
||||
totalTokens: 100,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Export mock types
|
||||
export interface GenerateContentConfig {
|
||||
httpOptions?: any
|
||||
systemInstruction?: string
|
||||
temperature?: number
|
||||
thinkingConfig?: any
|
||||
}
|
||||
|
||||
export interface GenerateContentResponseUsageMetadata {
|
||||
promptTokenCount?: number
|
||||
candidatesTokenCount?: number
|
||||
thoughtsTokenCount?: number
|
||||
cachedContentTokenCount?: number
|
||||
}
|
||||
|
||||
export interface Part {
|
||||
thought?: boolean
|
||||
text?: string
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import { ApiHandler } from ".."
|
||||
import { huaweiCloudMaasDefaultModelId, HuaweiCloudMaasModelId, huaweiCloudMaasModels, ModelInfo } from "@shared/api"
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { withRetry } from "../retry"
|
||||
|
||||
interface HuaweiCloudMaaSHandlerOptions {
|
||||
huaweiCloudMaasApiKey?: string
|
||||
huaweiCloudMaasModelId?: string
|
||||
huaweiCloudMaasModelInfo?: ModelInfo
|
||||
}
|
||||
|
||||
export class HuaweiCloudMaaSHandler implements ApiHandler {
|
||||
private options: HuaweiCloudMaaSHandlerOptions
|
||||
private client: OpenAI | undefined
|
||||
constructor(options: HuaweiCloudMaaSHandlerOptions) {
|
||||
this.options = options
|
||||
}
|
||||
|
||||
private ensureClient(): OpenAI {
|
||||
if (!this.client) {
|
||||
if (!this.options.huaweiCloudMaasApiKey) {
|
||||
throw new Error("Huawei Cloud MaaS API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.modelarts-maas.com/v1/",
|
||||
apiKey: this.options.huaweiCloudMaasApiKey,
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Huawei Cloud MaaS client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
|
||||
getModel(): { id: HuaweiCloudMaasModelId; info: ModelInfo } {
|
||||
// First priority: huaweiCloudMaasModelId and huaweiCloudMaasModelInfo (like Groq does)
|
||||
const huaweiCloudMaasModelId = this.options.huaweiCloudMaasModelId
|
||||
const huaweiCloudMaasModelInfo = this.options.huaweiCloudMaasModelInfo
|
||||
if (huaweiCloudMaasModelId && huaweiCloudMaasModelInfo) {
|
||||
return { id: huaweiCloudMaasModelId as HuaweiCloudMaasModelId, info: huaweiCloudMaasModelInfo }
|
||||
}
|
||||
|
||||
// Second priority: huaweiCloudMaasModelId with static model info
|
||||
if (huaweiCloudMaasModelId && huaweiCloudMaasModelId in huaweiCloudMaasModels) {
|
||||
const id = huaweiCloudMaasModelId as HuaweiCloudMaasModelId
|
||||
return { id, info: huaweiCloudMaasModels[id] }
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return {
|
||||
id: huaweiCloudMaasDefaultModelId,
|
||||
info: huaweiCloudMaasModels[huaweiCloudMaasDefaultModelId],
|
||||
}
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const model = this.getModel()
|
||||
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
const stream = await client.chat.completions.create({
|
||||
model: model.id,
|
||||
max_completion_tokens: model.info.maxTokens,
|
||||
messages: openAiMessages,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
temperature: 0,
|
||||
})
|
||||
|
||||
let reasoning: string | null = null
|
||||
let didOutputUsage: boolean = false
|
||||
let finalUsage: any = null
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
|
||||
// Handle reasoning content detection
|
||||
if (delta?.content) {
|
||||
if (reasoning || delta.content.includes("<think>")) {
|
||||
reasoning = (reasoning || "") + delta.content
|
||||
} else if (!reasoning) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle reasoning output
|
||||
if (reasoning || (delta && "reasoning_content" in delta && delta.reasoning_content)) {
|
||||
const reasoningContent = delta?.content || ((delta as any)?.reasoning_content as string | undefined) || ""
|
||||
if (reasoningContent.trim()) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: reasoningContent,
|
||||
}
|
||||
}
|
||||
|
||||
// Check if reasoning is complete
|
||||
if (reasoning?.includes("</think>")) {
|
||||
reasoning = null
|
||||
}
|
||||
}
|
||||
|
||||
// Store usage information for later output
|
||||
if (chunk.usage) {
|
||||
finalUsage = chunk.usage
|
||||
}
|
||||
|
||||
// Output usage when stream is finished
|
||||
if (!didOutputUsage && chunk.choices?.[0]?.finish_reason) {
|
||||
if (finalUsage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: finalUsage.prompt_tokens || 0,
|
||||
outputTokens: finalUsage.completion_tokens || 0,
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
}
|
||||
}
|
||||
didOutputUsage = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -252,77 +252,19 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
}
|
||||
|
||||
private async countTokens(text: string | vscode.LanguageModelChatMessage): Promise<number> {
|
||||
// For Claude models, use character-to-token ratio instead of VSCode LM's inaccurate counting
|
||||
if (this.isClaudeModel()) {
|
||||
const textContent = typeof text === "string" ? text : this.extractTextFromMessage(text)
|
||||
// Use 4 character-to-token ratio for Claude models
|
||||
return Math.ceil(textContent.length / 4)
|
||||
}
|
||||
|
||||
// Check for required dependencies
|
||||
if (!this.client) {
|
||||
console.warn("Cline <Language Model API>: No client available for token counting")
|
||||
return 0
|
||||
}
|
||||
|
||||
if (!this.currentRequestCancellation) {
|
||||
console.warn("Cline <Language Model API>: No cancellation token available for token counting")
|
||||
return 0
|
||||
}
|
||||
|
||||
// Validate input
|
||||
if (!text) {
|
||||
console.debug("Cline <Language Model API>: Empty text provided for token counting")
|
||||
return 0
|
||||
}
|
||||
|
||||
try {
|
||||
// Handle different input types
|
||||
let tokenCount: number
|
||||
|
||||
if (typeof text === "string") {
|
||||
tokenCount = await this.client.countTokens(text, this.currentRequestCancellation.token)
|
||||
} else if (text instanceof vscode.LanguageModelChatMessage) {
|
||||
// For chat messages, ensure we have content
|
||||
if (!text.content || (Array.isArray(text.content) && text.content.length === 0)) {
|
||||
console.debug("Cline <Language Model API>: Empty chat message content")
|
||||
return 0
|
||||
}
|
||||
tokenCount = await this.client.countTokens(text, this.currentRequestCancellation.token)
|
||||
} else {
|
||||
console.warn("Cline <Language Model API>: Invalid input type for token counting")
|
||||
return 0
|
||||
}
|
||||
|
||||
// Validate the result
|
||||
if (typeof tokenCount !== "number") {
|
||||
console.warn("Cline <Language Model API>: Non-numeric token count received:", tokenCount)
|
||||
return 0
|
||||
}
|
||||
|
||||
if (tokenCount < 0) {
|
||||
console.warn("Cline <Language Model API>: Negative token count received:", tokenCount)
|
||||
return 0
|
||||
}
|
||||
|
||||
return tokenCount
|
||||
} catch (error) {
|
||||
// Handle specific error types
|
||||
if (error instanceof vscode.CancellationError) {
|
||||
console.debug("Cline <Language Model API>: Token counting cancelled by user")
|
||||
return 0
|
||||
}
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error"
|
||||
console.warn("Cline <Language Model API>: Token counting failed:", errorMessage)
|
||||
|
||||
// Log additional error details if available
|
||||
if (error instanceof Error && error.stack) {
|
||||
console.debug("Token counting error stack:", error.stack)
|
||||
}
|
||||
|
||||
return 0 // Fallback to prevent stream interruption
|
||||
}
|
||||
/**
|
||||
* NOTE (intentional trade-off):
|
||||
* We use a coarse chars/4 heuristic here instead of a real tokenizer (e.g., js-tiktoken with o200k_base).
|
||||
* Rationale:
|
||||
* - Avoid pulling multi‑MB rank files and increasing the extension install/download size.
|
||||
* - Eliminate encoder lifecycle/memory concerns in long-running sessions.
|
||||
* Consequences:
|
||||
* - This is not model-accurate and can under/over-estimate tokens, especially with tool/function calls.
|
||||
* - It is “good enough” for budgeting/context checks, and we accept the inaccuracy by design.
|
||||
* If precise accounting becomes a requirement, reintroduce a tokenizer behind a feature flag or backend-only path.
|
||||
*/
|
||||
const textContent = typeof text === "string" ? text : this.extractTextFromMessage(text)
|
||||
return Math.ceil((textContent || "").length / 4)
|
||||
}
|
||||
|
||||
private async calculateTotalInputTokens(vsCodeLmMessages: vscode.LanguageModelChatMessage[]): Promise<number> {
|
||||
|
||||
@@ -60,6 +60,8 @@ describe("FileContextTracker", () => {
|
||||
((_) => {}) as WebviewProviderCreator,
|
||||
(() => {}) as DiffViewProviderCreator,
|
||||
vscodeHostBridgeClient,
|
||||
(_) => {},
|
||||
async () => "",
|
||||
)
|
||||
|
||||
// Create tracker instance
|
||||
|
||||
+134
-142
@@ -13,7 +13,7 @@ import { ClineAccountService } from "@services/account/ClineAccountService"
|
||||
import { McpHub } from "@services/mcp/McpHub"
|
||||
import { ApiProvider, ModelInfo } from "@shared/api"
|
||||
import { ChatContent } from "@shared/ChatContent"
|
||||
import { ChatSettings, Mode, StoredChatSettings } from "@shared/ChatSettings"
|
||||
import { Mode } from "@shared/storage/types"
|
||||
import { ClineRulesToggles } from "@shared/cline-rules"
|
||||
import { ExtensionMessage, ExtensionState, Platform } from "@shared/ExtensionMessage"
|
||||
import { HistoryItem } from "@shared/HistoryItem"
|
||||
@@ -30,11 +30,13 @@ import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { ensureMcpServersDirectoryExists, ensureSettingsDirectoryExists, GlobalFileNames } from "../storage/disk"
|
||||
import { getAllExtensionState, getGlobalState, getWorkspaceState, storeSecret, updateGlobalState } from "../storage/state"
|
||||
import { CacheService, PersistenceErrorEvent } from "../storage/CacheService"
|
||||
import { Task } from "../task"
|
||||
import { handleGrpcRequest, handleGrpcRequestCancel } from "./grpc-handler"
|
||||
import { sendMcpMarketplaceCatalogEvent } from "./mcp/subscribeToMcpMarketplaceCatalog"
|
||||
import { sendStateUpdate } from "./state/subscribeToState"
|
||||
import { sendAddToInputEvent } from "./ui/subscribeToAddToInput"
|
||||
import { getLatestAnnouncementId } from "@/utils/announcements"
|
||||
|
||||
/*
|
||||
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
||||
@@ -52,20 +54,49 @@ export class Controller {
|
||||
workspaceTracker: WorkspaceTracker
|
||||
mcpHub: McpHub
|
||||
accountService: ClineAccountService
|
||||
authService: AuthService
|
||||
get latestAnnouncementId(): string {
|
||||
return this.context.extension?.packageJSON?.version?.split(".").slice(0, 2).join(".") ?? ""
|
||||
}
|
||||
readonly cacheService: CacheService
|
||||
|
||||
constructor(
|
||||
readonly context: vscode.ExtensionContext,
|
||||
private readonly outputChannel: vscode.OutputChannel,
|
||||
postMessage: (message: ExtensionMessage) => Thenable<boolean> | undefined,
|
||||
id: string,
|
||||
) {
|
||||
this.id = id
|
||||
this.outputChannel.appendLine("ClineProvider instantiated")
|
||||
|
||||
HostProvider.get().logToChannel("ClineProvider instantiated")
|
||||
this.postMessage = postMessage
|
||||
this.accountService = ClineAccountService.getInstance()
|
||||
this.cacheService = new CacheService(context)
|
||||
const authService = AuthService.getInstance(this)
|
||||
|
||||
// Initialize cache service asynchronously - critical for extension functionality
|
||||
this.cacheService
|
||||
.initialize()
|
||||
.then(() => {
|
||||
authService.restoreRefreshTokenAndRetrieveAuthInfo()
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("CRITICAL: Failed to initialize CacheService - extension may not function properly:", error)
|
||||
})
|
||||
|
||||
// Set up persistence error recovery
|
||||
this.cacheService.onPersistenceError = async ({ error }: PersistenceErrorEvent) => {
|
||||
console.error("Cache persistence failed, recovering:", error)
|
||||
try {
|
||||
await this.cacheService.reInitialize()
|
||||
await this.postStateToWebview()
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.WARNING,
|
||||
message: "Saving settings to storage failed.",
|
||||
})
|
||||
} catch (recoveryError) {
|
||||
console.error("Cache recovery failed:", recoveryError)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Failed to save settings. Please restart the extension.",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.workspaceTracker = new WorkspaceTracker()
|
||||
this.mcpHub = new McpHub(
|
||||
@@ -74,12 +105,9 @@ export class Controller {
|
||||
(msg) => this.postMessageToWebview(msg),
|
||||
this.context.extension?.packageJSON?.version ?? "1.0.0",
|
||||
)
|
||||
this.accountService = ClineAccountService.getInstance()
|
||||
this.authService = AuthService.getInstance(context)
|
||||
this.authService.restoreRefreshTokenAndRetrieveAuthInfo()
|
||||
|
||||
// Clean up legacy checkpoints
|
||||
cleanupLegacyCheckpoints(this.context.globalStorageUri.fsPath, this.outputChannel).catch((error) => {
|
||||
cleanupLegacyCheckpoints(this.context.globalStorageUri.fsPath).catch((error) => {
|
||||
console.error("Failed to cleanup legacy checkpoints:", error)
|
||||
})
|
||||
}
|
||||
@@ -111,12 +139,18 @@ export class Controller {
|
||||
async handleSignOut() {
|
||||
try {
|
||||
// TODO: update to clineAccountId and then move clineApiKey to a clear function.
|
||||
await storeSecret(this.context, "clineAccountId", undefined)
|
||||
this.cacheService.setSecret("clineAccountId", undefined)
|
||||
await updateGlobalState(this.context, "userInfo", undefined)
|
||||
await Promise.all([
|
||||
updateGlobalState(this.context, "planModeApiProvider", "openrouter"),
|
||||
updateGlobalState(this.context, "actModeApiProvider", "openrouter"),
|
||||
])
|
||||
|
||||
// Update API providers through cache service
|
||||
const apiConfiguration = this.cacheService.getApiConfiguration()
|
||||
const updatedConfig = {
|
||||
...apiConfiguration,
|
||||
planModeApiProvider: "openrouter" as ApiProvider,
|
||||
actModeApiProvider: "openrouter" as ApiProvider,
|
||||
}
|
||||
this.cacheService.setApiConfiguration(updatedConfig)
|
||||
|
||||
await this.postStateToWebview()
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
@@ -136,11 +170,16 @@ export class Controller {
|
||||
|
||||
async initTask(task?: string, images?: string[], files?: string[], historyItem?: HistoryItem) {
|
||||
await this.clearTask() // ensures that an existing task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one
|
||||
|
||||
// Get API configuration from cache for immediate access
|
||||
const apiConfiguration = this.cacheService.getApiConfiguration()
|
||||
|
||||
const {
|
||||
apiConfiguration,
|
||||
autoApprovalSettings,
|
||||
browserSettings,
|
||||
chatSettings: storedChatSettings,
|
||||
preferredLanguage,
|
||||
openaiReasoningEffort,
|
||||
mode,
|
||||
shellIntegrationTimeout,
|
||||
terminalReuseEnabled,
|
||||
terminalOutputLineLimit,
|
||||
@@ -148,17 +187,9 @@ export class Controller {
|
||||
enableCheckpointsSetting,
|
||||
isNewUser,
|
||||
taskHistory,
|
||||
strictPlanModeEnabled,
|
||||
} = await getAllExtensionState(this.context)
|
||||
|
||||
// Get current mode using helper function
|
||||
const currentMode = await this.getCurrentMode()
|
||||
|
||||
// Reconstruct ChatSettings with mode from global state and stored preferences
|
||||
const chatSettings: ChatSettings = {
|
||||
...storedChatSettings, // Spread stored preferences (preferredLanguage, openAIReasoningEffort)
|
||||
mode: currentMode, // Use mode from global state
|
||||
}
|
||||
|
||||
const NEW_USER_TASK_COUNT_THRESHOLD = 10
|
||||
|
||||
// Check if the user has completed enough tasks to no longer be considered a "new user"
|
||||
@@ -185,13 +216,17 @@ export class Controller {
|
||||
apiConfiguration,
|
||||
autoApprovalSettings,
|
||||
browserSettings,
|
||||
chatSettings,
|
||||
preferredLanguage,
|
||||
openaiReasoningEffort,
|
||||
mode,
|
||||
strictPlanModeEnabled ?? false,
|
||||
shellIntegrationTimeout,
|
||||
terminalReuseEnabled ?? true,
|
||||
terminalOutputLineLimit ?? 500,
|
||||
defaultTerminalProfile ?? "default",
|
||||
enableCheckpointsSetting ?? true,
|
||||
await getCwd(getDesktopDir()),
|
||||
this.cacheService,
|
||||
task,
|
||||
images,
|
||||
files,
|
||||
@@ -219,10 +254,6 @@ export class Controller {
|
||||
*/
|
||||
async handleWebviewMessage(message: WebviewMessage) {
|
||||
switch (message.type) {
|
||||
case "fetchMcpMarketplace": {
|
||||
await this.fetchMcpMarketplace(message.bool)
|
||||
break
|
||||
}
|
||||
case "grpc_request": {
|
||||
if (message.grpc_request) {
|
||||
await handleGrpcRequest(this, message.grpc_request)
|
||||
@@ -235,9 +266,9 @@ export class Controller {
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// Add more switch case statements here as more webview message commands
|
||||
// are created within the webview context (i.e. inside media/main.js)
|
||||
default: {
|
||||
console.error("Received unhandled WebviewMessage type:", JSON.stringify(message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,28 +279,25 @@ export class Controller {
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
|
||||
async togglePlanActModeWithChatSettings(chatSettings: ChatSettings, chatContent?: ChatContent): Promise<boolean> {
|
||||
const didSwitchToActMode = chatSettings.mode === "act"
|
||||
async togglePlanActMode(modeToSwitchTo: Mode, chatContent?: ChatContent): Promise<boolean> {
|
||||
const didSwitchToActMode = modeToSwitchTo === "act"
|
||||
|
||||
// Store mode to global state
|
||||
await updateGlobalState(this.context, "mode", chatSettings.mode)
|
||||
await updateGlobalState(this.context, "mode", modeToSwitchTo)
|
||||
|
||||
// Capture mode switch telemetry | Capture regardless of if we know the taskId
|
||||
telemetryService.captureModeSwitch(this.task?.taskId ?? "0", chatSettings.mode)
|
||||
telemetryService.captureModeSwitch(this.task?.taskId ?? "0", modeToSwitchTo)
|
||||
|
||||
// Update API handler with new mode (buildApiHandler now selects provider based on mode)
|
||||
if (this.task) {
|
||||
const { apiConfiguration } = await getAllExtensionState(this.context)
|
||||
this.task.api = buildApiHandler({ ...apiConfiguration, taskId: this.task.taskId }, chatSettings.mode)
|
||||
const apiConfiguration = this.cacheService.getApiConfiguration()
|
||||
this.task.api = buildApiHandler({ ...apiConfiguration, taskId: this.task.taskId }, modeToSwitchTo)
|
||||
}
|
||||
|
||||
// Save only non-mode properties to global storage
|
||||
const { mode, ...persistentChatSettings }: { mode: string } & StoredChatSettings = chatSettings
|
||||
await updateGlobalState(this.context, "chatSettings", persistentChatSettings)
|
||||
await this.postStateToWebview()
|
||||
|
||||
if (this.task) {
|
||||
this.task.chatSettings = chatSettings
|
||||
this.task.updateMode(modeToSwitchTo)
|
||||
if (this.task.taskState.isAwaitingPlanResponse && didSwitchToActMode) {
|
||||
this.task.taskState.didRespondToPlanAskBySwitchingMode = true
|
||||
// Use chatContent if provided, otherwise use default message
|
||||
@@ -321,7 +349,7 @@ export class Controller {
|
||||
|
||||
async handleAuthCallback(customToken: string, provider: string | null = null) {
|
||||
try {
|
||||
await this.authService.handleAuthCallback(customToken, provider ? provider : "google")
|
||||
await AuthService.getInstance(this).handleAuthCallback(customToken, provider ? provider : "google")
|
||||
|
||||
const clineProvider: ApiProvider = "cline"
|
||||
|
||||
@@ -329,27 +357,26 @@ export class Controller {
|
||||
const { planActSeparateModelsSetting } = await getAllExtensionState(this.context)
|
||||
const currentMode = await this.getCurrentMode()
|
||||
|
||||
// Get current API configuration from cache
|
||||
const currentApiConfiguration = this.cacheService.getApiConfiguration()
|
||||
|
||||
let updatedConfig = { ...currentApiConfiguration }
|
||||
|
||||
if (planActSeparateModelsSetting) {
|
||||
// Only update the current mode's provider
|
||||
if (currentMode === "plan") {
|
||||
await updateGlobalState(this.context, "planModeApiProvider", clineProvider)
|
||||
updatedConfig.planModeApiProvider = clineProvider
|
||||
} else {
|
||||
await updateGlobalState(this.context, "actModeApiProvider", clineProvider)
|
||||
updatedConfig.actModeApiProvider = clineProvider
|
||||
}
|
||||
} else {
|
||||
// Update both modes to keep them in sync
|
||||
await Promise.all([
|
||||
updateGlobalState(this.context, "planModeApiProvider", clineProvider),
|
||||
updateGlobalState(this.context, "actModeApiProvider", clineProvider),
|
||||
])
|
||||
updatedConfig.planModeApiProvider = clineProvider
|
||||
updatedConfig.actModeApiProvider = clineProvider
|
||||
}
|
||||
|
||||
// Get the updated API configuration (now includes the updated providers)
|
||||
const { apiConfiguration } = await getAllExtensionState(this.context)
|
||||
const updatedConfig = {
|
||||
...apiConfiguration,
|
||||
apiProvider: clineProvider,
|
||||
}
|
||||
// Update the API configuration through cache service
|
||||
this.cacheService.setApiConfiguration(updatedConfig)
|
||||
|
||||
// Mark welcome view as completed since user has successfully logged in
|
||||
await updateGlobalState(this.context, "welcomeViewCompleted", true)
|
||||
@@ -468,31 +495,6 @@ export class Controller {
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchMcpMarketplace(forceRefresh: boolean = false) {
|
||||
try {
|
||||
// Check if we have cached data
|
||||
const cachedCatalog = (await getGlobalState(this.context, "mcpMarketplaceCatalog")) as
|
||||
| McpMarketplaceCatalog
|
||||
| undefined
|
||||
if (!forceRefresh && cachedCatalog?.items) {
|
||||
await sendMcpMarketplaceCatalogEvent(cachedCatalog)
|
||||
return
|
||||
}
|
||||
|
||||
const catalog = await this.fetchMcpMarketplaceFromApi(false)
|
||||
if (catalog) {
|
||||
await sendMcpMarketplaceCatalogEvent(catalog)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to handle cached MCP marketplace:", error)
|
||||
const errorMessage = error instanceof Error ? error.message : "Failed to handle cached MCP marketplace"
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: errorMessage,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// OpenRouter
|
||||
|
||||
async handleOpenRouterCallback(code: string) {
|
||||
@@ -511,21 +513,20 @@ export class Controller {
|
||||
|
||||
const openrouter: ApiProvider = "openrouter"
|
||||
const currentMode = await this.getCurrentMode()
|
||||
await Promise.all([
|
||||
updateGlobalState(this.context, "planModeApiProvider", openrouter),
|
||||
updateGlobalState(this.context, "actModeApiProvider", openrouter),
|
||||
])
|
||||
await storeSecret(this.context, "openRouterApiKey", apiKey)
|
||||
|
||||
// Update API configuration through cache service
|
||||
const currentApiConfiguration = this.cacheService.getApiConfiguration()
|
||||
const updatedConfig = {
|
||||
...currentApiConfiguration,
|
||||
planModeApiProvider: openrouter,
|
||||
actModeApiProvider: openrouter,
|
||||
openRouterApiKey: apiKey,
|
||||
}
|
||||
this.cacheService.setApiConfiguration(updatedConfig)
|
||||
|
||||
await this.postStateToWebview()
|
||||
if (this.task) {
|
||||
// Get the updated API configuration (now includes the updated providers)
|
||||
const { apiConfiguration } = await getAllExtensionState(this.context)
|
||||
const updatedConfig = {
|
||||
...apiConfiguration,
|
||||
openRouterApiKey: apiKey,
|
||||
taskId: this.task.taskId,
|
||||
}
|
||||
this.task.api = buildApiHandler(updatedConfig, currentMode)
|
||||
this.task.api = buildApiHandler({ ...updatedConfig, taskId: this.task.taskId }, currentMode)
|
||||
}
|
||||
// await this.postMessageToWebview({ type: "action", action: "settingsButtonClicked" }) // bad ux if user is on welcome
|
||||
}
|
||||
@@ -699,13 +700,18 @@ export class Controller {
|
||||
}
|
||||
|
||||
async getStateToPostToWebview(): Promise<ExtensionState> {
|
||||
// Get API configuration from cache for immediate access
|
||||
const apiConfiguration = this.cacheService.getApiConfiguration()
|
||||
|
||||
const {
|
||||
apiConfiguration,
|
||||
lastShownAnnouncementId,
|
||||
taskHistory,
|
||||
autoApprovalSettings,
|
||||
browserSettings,
|
||||
chatSettings: storedChatSettings,
|
||||
preferredLanguage,
|
||||
openaiReasoningEffort,
|
||||
mode,
|
||||
strictPlanModeEnabled,
|
||||
userInfo,
|
||||
mcpMarketplaceEnabled,
|
||||
mcpDisplayMode,
|
||||
@@ -721,51 +727,51 @@ export class Controller {
|
||||
welcomeViewCompleted,
|
||||
mcpResponsesCollapsed,
|
||||
terminalOutputLineLimit,
|
||||
localClineRulesToggles,
|
||||
localWindsurfRulesToggles,
|
||||
localCursorRulesToggles,
|
||||
localWorkflowToggles,
|
||||
} = await getAllExtensionState(this.context)
|
||||
|
||||
// Get current mode using helper function
|
||||
const currentMode = await this.getCurrentMode()
|
||||
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() || []
|
||||
|
||||
// Reconstruct ChatSettings with mode from global state and stored preferences
|
||||
const chatSettings: ChatSettings = {
|
||||
...storedChatSettings, // Spread stored preferences (preferredLanguage, openAIReasoningEffort)
|
||||
mode: currentMode, // Use mode from global state
|
||||
}
|
||||
const processedTaskHistory = (taskHistory || [])
|
||||
.filter((item) => item.ts && item.task)
|
||||
.sort((a, b) => b.ts - a.ts)
|
||||
.slice(0, 100) // for now we're only getting the latest 100 tasks, but a better solution here is to only pass in 3 for recent task history, and then get the full task history on demand when going to the task history view (maybe with pagination?)
|
||||
|
||||
const localClineRulesToggles =
|
||||
((await getWorkspaceState(this.context, "localClineRulesToggles")) as ClineRulesToggles) || {}
|
||||
|
||||
const localWindsurfRulesToggles =
|
||||
((await getWorkspaceState(this.context, "localWindsurfRulesToggles")) as ClineRulesToggles) || {}
|
||||
|
||||
const localCursorRulesToggles =
|
||||
((await getWorkspaceState(this.context, "localCursorRulesToggles")) as ClineRulesToggles) || {}
|
||||
|
||||
const localWorkflowToggles = ((await getWorkspaceState(this.context, "workflowToggles")) as ClineRulesToggles) || {}
|
||||
const latestAnnouncementId = getLatestAnnouncementId(this.context)
|
||||
const shouldShowAnnouncement = lastShownAnnouncementId !== latestAnnouncementId
|
||||
const platform = process.platform as Platform
|
||||
const distinctId = telemetryService.distinctId
|
||||
const version = this.context.extension?.packageJSON?.version ?? ""
|
||||
const uriScheme = vscode.env.uriScheme
|
||||
|
||||
return {
|
||||
version: this.context.extension?.packageJSON?.version ?? "",
|
||||
version,
|
||||
apiConfiguration,
|
||||
uriScheme: vscode.env.uriScheme,
|
||||
currentTaskItem: this.task?.taskId ? (taskHistory || []).find((item) => item.id === this.task?.taskId) : undefined,
|
||||
checkpointTrackerErrorMessage: this.task?.taskState.checkpointTrackerErrorMessage,
|
||||
clineMessages: this.task?.messageStateHandler.getClineMessages() || [],
|
||||
taskHistory: (taskHistory || [])
|
||||
.filter((item) => item.ts && item.task)
|
||||
.sort((a, b) => b.ts - a.ts)
|
||||
.slice(0, 100), // for now we're only getting the latest 100 tasks, but a better solution here is to only pass in 3 for recent task history, and then get the full task history on demand when going to the task history view (maybe with pagination?)
|
||||
shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId,
|
||||
platform: process.platform as Platform,
|
||||
uriScheme,
|
||||
currentTaskItem,
|
||||
checkpointTrackerErrorMessage,
|
||||
clineMessages,
|
||||
taskHistory: processedTaskHistory,
|
||||
shouldShowAnnouncement,
|
||||
platform,
|
||||
autoApprovalSettings,
|
||||
browserSettings,
|
||||
chatSettings,
|
||||
preferredLanguage,
|
||||
openaiReasoningEffort,
|
||||
mode,
|
||||
strictPlanModeEnabled,
|
||||
userInfo,
|
||||
mcpMarketplaceEnabled,
|
||||
mcpDisplayMode,
|
||||
telemetrySetting,
|
||||
planActSeparateModelsSetting,
|
||||
enableCheckpointsSetting: enableCheckpointsSetting ?? true,
|
||||
distinctId: telemetryService.distinctId,
|
||||
distinctId,
|
||||
globalClineRulesToggles: globalClineRulesToggles || {},
|
||||
localClineRulesToggles: localClineRulesToggles || {},
|
||||
localWindsurfRulesToggles: localWindsurfRulesToggles || {},
|
||||
@@ -840,18 +846,4 @@ export class Controller {
|
||||
await updateGlobalState(this.context, "taskHistory", history)
|
||||
return history
|
||||
}
|
||||
|
||||
// private async clearState() {
|
||||
// this.context.workspaceState.keys().forEach((key) => {
|
||||
// this.context.workspaceState.update(key, undefined)
|
||||
// })
|
||||
// this.context.globalState.keys().forEach((key) => {
|
||||
// this.context.globalState.update(key, undefined)
|
||||
// })
|
||||
// this.context.secrets.delete("apiKey")
|
||||
// }
|
||||
|
||||
// secrets
|
||||
|
||||
// dev
|
||||
}
|
||||
|
||||
@@ -66,9 +66,9 @@ export async function downloadMcp(controller: Controller, request: StringRequest
|
||||
- Once installed, demonstrate the server's capabilities by using one of its tools.
|
||||
Here is the project's README to help you get started:\n\n${mcpDetails.readmeContent}\n${mcpDetails.llmsInstallationContent}`
|
||||
|
||||
const { chatSettings } = await controller.getStateToPostToWebview()
|
||||
if (chatSettings.mode === "plan") {
|
||||
await controller.togglePlanActModeWithChatSettings({ mode: "act" })
|
||||
const { mode } = await controller.getStateToPostToWebview()
|
||||
if (mode === "plan") {
|
||||
await controller.togglePlanActMode("act")
|
||||
}
|
||||
|
||||
// Initialize task and show chat view
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Controller } from "../index"
|
||||
import { Empty } from "@shared/proto/cline/common"
|
||||
import { UpdateApiConfigurationRequest } from "@shared/proto/cline/models"
|
||||
import { updateApiConfiguration } from "../../storage/state"
|
||||
import { buildApiHandler } from "@api/index"
|
||||
import { convertProtoToApiConfiguration } from "@shared/proto-conversions/models/api-configuration-conversion"
|
||||
|
||||
@@ -25,7 +24,7 @@ export async function updateApiConfigurationProto(
|
||||
const appApiConfiguration = convertProtoToApiConfiguration(request.apiConfiguration)
|
||||
|
||||
// Update the API configuration in storage
|
||||
await updateApiConfiguration(controller.context, appApiConfiguration)
|
||||
controller.cacheService.setApiConfiguration(appApiConfiguration)
|
||||
|
||||
// Update the task's API handler if there's an active task
|
||||
if (controller.task) {
|
||||
|
||||
@@ -19,13 +19,13 @@ export async function resetState(controller: Controller, request: ResetStateRequ
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Resetting global state...",
|
||||
})
|
||||
await resetGlobalState(controller.context)
|
||||
await resetGlobalState(controller)
|
||||
} else {
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Resetting workspace state...",
|
||||
})
|
||||
await resetWorkspaceState(controller.context)
|
||||
await resetWorkspaceState(controller)
|
||||
}
|
||||
|
||||
if (controller.task) {
|
||||
|
||||
@@ -16,11 +16,7 @@ export async function toggleFavoriteModel(controller: Controller, request: Strin
|
||||
}
|
||||
|
||||
const modelId = request.value
|
||||
const { apiConfiguration } = await controller.getStateToPostToWebview()
|
||||
|
||||
if (!apiConfiguration) {
|
||||
throw new Error("API configuration not found")
|
||||
}
|
||||
const apiConfiguration = controller.cacheService.getApiConfiguration()
|
||||
|
||||
const favoritedModelIds = apiConfiguration.favoritedModelIds || []
|
||||
|
||||
@@ -29,7 +25,12 @@ export async function toggleFavoriteModel(controller: Controller, request: Strin
|
||||
? favoritedModelIds.filter((id) => id !== modelId)
|
||||
: [...favoritedModelIds, modelId]
|
||||
|
||||
await updateGlobalState(controller.context, "favoritedModelIds", updatedFavorites)
|
||||
// Update the complete API configuration through cache service
|
||||
const updatedApiConfiguration = {
|
||||
...apiConfiguration,
|
||||
favoritedModelIds: updatedFavorites,
|
||||
}
|
||||
controller.cacheService.setApiConfiguration(updatedApiConfiguration)
|
||||
|
||||
// Capture telemetry for model favorite toggle
|
||||
const isFavorited = !favoritedModelIds.includes(modelId)
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { Controller } from ".."
|
||||
import { Boolean } from "@shared/proto/cline/common"
|
||||
import { TogglePlanActModeRequest } from "@shared/proto/cline/state"
|
||||
import {
|
||||
convertProtoChatContentToChatContent,
|
||||
convertProtoChatSettingsToChatSettings,
|
||||
} from "@shared/proto-conversions/state/chat-settings-conversion"
|
||||
|
||||
/**
|
||||
* Toggles between Plan and Act modes
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing the chat settings and optional chat content
|
||||
* @returns An empty response
|
||||
*/
|
||||
export async function togglePlanActMode(controller: Controller, request: TogglePlanActModeRequest): Promise<Boolean> {
|
||||
try {
|
||||
if (!request.chatSettings) {
|
||||
throw new Error("Chat settings are required")
|
||||
}
|
||||
|
||||
const chatSettings = convertProtoChatSettingsToChatSettings(request.chatSettings)
|
||||
const chatContent = request.chatContent ? convertProtoChatContentToChatContent(request.chatContent) : undefined
|
||||
|
||||
// Call the existing controller implementation
|
||||
const sentMessage = await controller.togglePlanActModeWithChatSettings(chatSettings, chatContent)
|
||||
|
||||
return Boolean.create({
|
||||
value: sentMessage,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to toggle Plan/Act mode:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Controller } from ".."
|
||||
import { Boolean } from "@shared/proto/cline/common"
|
||||
import { TogglePlanActModeRequest, PlanActMode } from "@shared/proto/cline/state"
|
||||
import { Mode } from "@shared/storage/types"
|
||||
|
||||
/**
|
||||
* Toggles between Plan and Act modes
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing the chat settings and optional chat content
|
||||
* @returns An empty response
|
||||
*/
|
||||
export async function togglePlanActModeProto(controller: Controller, request: TogglePlanActModeRequest): Promise<Boolean> {
|
||||
try {
|
||||
let mode: Mode
|
||||
if (request.mode === PlanActMode.PLAN) {
|
||||
mode = "plan"
|
||||
} else if (request.mode === PlanActMode.ACT) {
|
||||
mode = "act"
|
||||
} else {
|
||||
throw new Error(`Invalid mode value: ${request.mode}`)
|
||||
}
|
||||
const chatContent = request.chatContent
|
||||
|
||||
// Call the existing controller implementation
|
||||
const sentMessage = await controller.togglePlanActMode(mode, chatContent)
|
||||
|
||||
return Boolean.create({
|
||||
value: sentMessage,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to toggle Plan/Act mode:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Controller } from ".."
|
||||
import { Empty } from "@shared/proto/cline/common"
|
||||
import { UpdateSettingsRequest } from "@shared/proto/cline/state"
|
||||
import { updateApiConfiguration } from "../../storage/state"
|
||||
import { PlanActMode, UpdateSettingsRequest } from "@shared/proto/cline/state"
|
||||
import { buildApiHandler } from "../../../api"
|
||||
import { convertProtoApiConfigurationToApiConfiguration } from "../../../shared/proto-conversions/state/settings-conversion"
|
||||
import { convertProtoChatSettingsToChatSettings } from "../../../shared/proto-conversions/state/chat-settings-conversion"
|
||||
import { TelemetrySetting } from "@/shared/TelemetrySetting"
|
||||
import { OpenaiReasoningEffort } from "@/shared/storage/types"
|
||||
|
||||
/**
|
||||
* Updates multiple extension settings in a single request
|
||||
@@ -18,7 +17,7 @@ export async function updateSettings(controller: Controller, request: UpdateSett
|
||||
// Update API configuration
|
||||
if (request.apiConfiguration) {
|
||||
const apiConfiguration = convertProtoApiConfigurationToApiConfiguration(request.apiConfiguration)
|
||||
await updateApiConfiguration(controller.context, apiConfiguration)
|
||||
controller.cacheService.setApiConfiguration(apiConfiguration)
|
||||
|
||||
if (controller.task) {
|
||||
const currentMode = await controller.getCurrentMode()
|
||||
@@ -56,22 +55,26 @@ export async function updateSettings(controller: Controller, request: UpdateSett
|
||||
await controller.context.globalState.update("mcpDisplayMode", request.mcpDisplayMode)
|
||||
}
|
||||
|
||||
// Update chat settings
|
||||
if (request.chatSettings) {
|
||||
const chatSettings = convertProtoChatSettingsToChatSettings(request.chatSettings)
|
||||
|
||||
// Store mode to global state
|
||||
if (chatSettings.mode !== undefined) {
|
||||
await controller.context.globalState.update("mode", chatSettings.mode)
|
||||
}
|
||||
|
||||
// Store chat settings (excluding mode) to global state
|
||||
const { mode, ...globalChatSettings } = chatSettings
|
||||
await controller.context.globalState.update("chatSettings", globalChatSettings)
|
||||
|
||||
if (request.mode !== undefined) {
|
||||
const mode = request.mode === PlanActMode.PLAN ? "plan" : "act"
|
||||
if (controller.task) {
|
||||
controller.task.chatSettings = chatSettings
|
||||
controller.task.updateMode(mode)
|
||||
}
|
||||
await controller.context.globalState.update("mode", request.mode)
|
||||
}
|
||||
|
||||
if (request.openaiReasoningEffort !== undefined) {
|
||||
if (controller.task) {
|
||||
controller.task.openaiReasoningEffort = request.openaiReasoningEffort as OpenaiReasoningEffort
|
||||
}
|
||||
await controller.context.globalState.update("openaiReasoningEffort", request.openaiReasoningEffort)
|
||||
}
|
||||
|
||||
if (request.preferredLanguage !== undefined) {
|
||||
if (controller.task) {
|
||||
controller.task.preferredLanguage = request.preferredLanguage
|
||||
}
|
||||
await controller.context.globalState.update("preferredLanguage", request.preferredLanguage)
|
||||
}
|
||||
|
||||
// Update terminal timeout setting
|
||||
@@ -89,6 +92,14 @@ export async function updateSettings(controller: Controller, request: UpdateSett
|
||||
await controller.context.globalState.update("terminalOutputLineLimit", Number(request.terminalOutputLineLimit))
|
||||
}
|
||||
|
||||
// Update strict plan mode setting
|
||||
if (request.strictPlanModeEnabled !== undefined) {
|
||||
if (controller.task) {
|
||||
controller.task.updateStrictPlanMode(request.strictPlanModeEnabled)
|
||||
}
|
||||
await controller.context.globalState.update("strictPlanModeEnabled", request.strictPlanModeEnabled)
|
||||
}
|
||||
|
||||
// Post updated state to webview
|
||||
await controller.postStateToWebview()
|
||||
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { Controller } from "../index"
|
||||
import * as proto from "@/shared/proto"
|
||||
import { UpdateTerminalConnectionTimeoutRequest, UpdateTerminalConnectionTimeoutResponse } from "@shared/proto/cline/state"
|
||||
import { updateGlobalState } from "../../storage/state"
|
||||
|
||||
export async function updateTerminalConnectionTimeout(
|
||||
controller: Controller,
|
||||
request: proto.cline.Int64Request,
|
||||
): Promise<proto.cline.Int64> {
|
||||
const timeoutValue = request.value
|
||||
request: UpdateTerminalConnectionTimeoutRequest,
|
||||
): Promise<UpdateTerminalConnectionTimeoutResponse> {
|
||||
const timeoutMs = request.timeoutMs
|
||||
|
||||
// Update the terminal connection timeout setting in the state
|
||||
await updateGlobalState(controller.context, "shellIntegrationTimeout", timeoutValue)
|
||||
await updateGlobalState(controller.context, "shellIntegrationTimeout", timeoutMs)
|
||||
|
||||
// Broadcast state update to all webviews
|
||||
await controller.postStateToWebview()
|
||||
|
||||
return proto.cline.Int64.create({ value: timeoutValue })
|
||||
return { timeoutMs }
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export async function deleteTasksWithIds(controller: Controller, request: String
|
||||
options: { modal: true, items: ["Delete"] },
|
||||
})
|
||||
|
||||
if (userChoice === undefined) {
|
||||
if (userChoice.selectedOption !== "Delete") {
|
||||
return Empty.create()
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,8 @@ export async function initializeWebview(controller: Controller, request: EmptyRe
|
||||
refreshOpenRouterModels(controller, EmptyRequest.create()).then(async (response) => {
|
||||
if (response && response.models) {
|
||||
// Update model info in state (this needs to be done here since we don't want to update state while settings is open, and we may refresh models there)
|
||||
const { apiConfiguration, planActSeparateModelsSetting } = await getAllExtensionState(controller.context)
|
||||
const apiConfiguration = controller.cacheService.getApiConfiguration()
|
||||
const { planActSeparateModelsSetting } = await getAllExtensionState(controller.context)
|
||||
const currentMode = await controller.getCurrentMode()
|
||||
|
||||
if (planActSeparateModelsSetting) {
|
||||
@@ -42,26 +43,32 @@ export async function initializeWebview(controller: Controller, request: EmptyRe
|
||||
const modelId = apiConfiguration[modelIdField]
|
||||
|
||||
if (modelId && response.models[modelId]) {
|
||||
await updateGlobalState(controller.context, modelInfoField, response.models[modelId])
|
||||
const updatedConfig = {
|
||||
...apiConfiguration,
|
||||
[modelInfoField]: response.models[modelId],
|
||||
}
|
||||
controller.cacheService.setApiConfiguration(updatedConfig)
|
||||
await controller.postStateToWebview()
|
||||
}
|
||||
} else {
|
||||
// Shared models: update both plan and act modes
|
||||
const planModelId = apiConfiguration.planModeOpenRouterModelId
|
||||
const actModelId = apiConfiguration.actModeOpenRouterModelId
|
||||
let updatedConfig = { ...apiConfiguration }
|
||||
|
||||
// Update plan mode model info if we have a model ID
|
||||
if (planModelId && response.models[planModelId]) {
|
||||
await updateGlobalState(controller.context, "planModeOpenRouterModelInfo", response.models[planModelId])
|
||||
updatedConfig.planModeOpenRouterModelInfo = response.models[planModelId]
|
||||
}
|
||||
|
||||
// Update act mode model info if we have a model ID
|
||||
if (actModelId && response.models[actModelId]) {
|
||||
await updateGlobalState(controller.context, "actModeOpenRouterModelInfo", response.models[actModelId])
|
||||
updatedConfig.actModeOpenRouterModelInfo = response.models[actModelId]
|
||||
}
|
||||
|
||||
// Post state update if we updated any model info
|
||||
if ((planModelId && response.models[planModelId]) || (actModelId && response.models[actModelId])) {
|
||||
controller.cacheService.setApiConfiguration(updatedConfig)
|
||||
await controller.postStateToWebview()
|
||||
}
|
||||
}
|
||||
@@ -71,7 +78,8 @@ export async function initializeWebview(controller: Controller, request: EmptyRe
|
||||
refreshGroqModels(controller, EmptyRequest.create()).then(async (response) => {
|
||||
if (response && response.models) {
|
||||
// Update model info in state for Groq (this needs to be done here since we don't want to update state while settings is open, and we may refresh models there)
|
||||
const { apiConfiguration, planActSeparateModelsSetting } = await getAllExtensionState(controller.context)
|
||||
const apiConfiguration = controller.cacheService.getApiConfiguration()
|
||||
const { planActSeparateModelsSetting } = await getAllExtensionState(controller.context)
|
||||
const currentMode = await controller.getCurrentMode()
|
||||
|
||||
if (planActSeparateModelsSetting) {
|
||||
@@ -81,26 +89,32 @@ export async function initializeWebview(controller: Controller, request: EmptyRe
|
||||
const modelId = apiConfiguration[modelIdField]
|
||||
|
||||
if (modelId && response.models[modelId]) {
|
||||
await updateGlobalState(controller.context, modelInfoField, response.models[modelId])
|
||||
const updatedConfig = {
|
||||
...apiConfiguration,
|
||||
[modelInfoField]: response.models[modelId],
|
||||
}
|
||||
controller.cacheService.setApiConfiguration(updatedConfig)
|
||||
await controller.postStateToWebview()
|
||||
}
|
||||
} else {
|
||||
// Shared models: update both plan and act modes
|
||||
const planModelId = apiConfiguration.planModeGroqModelId
|
||||
const actModelId = apiConfiguration.actModeGroqModelId
|
||||
let updatedConfig = { ...apiConfiguration }
|
||||
|
||||
// Update plan mode model info if we have a model ID
|
||||
if (planModelId && response.models[planModelId]) {
|
||||
await updateGlobalState(controller.context, "planModeGroqModelInfo", response.models[planModelId])
|
||||
updatedConfig.planModeGroqModelInfo = response.models[planModelId]
|
||||
}
|
||||
|
||||
// Update act mode model info if we have a model ID
|
||||
if (actModelId && response.models[actModelId]) {
|
||||
await updateGlobalState(controller.context, "actModeGroqModelInfo", response.models[actModelId])
|
||||
updatedConfig.actModeGroqModelInfo = response.models[actModelId]
|
||||
}
|
||||
|
||||
// Post state update if we updated any model info
|
||||
if ((planModelId && response.models[planModelId]) || (actModelId && response.models[actModelId])) {
|
||||
controller.cacheService.setApiConfiguration(updatedConfig)
|
||||
await controller.postStateToWebview()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { Boolean } from "@shared/proto/cline/common"
|
||||
import type { Controller } from "../index"
|
||||
import { updateGlobalState } from "../../storage/state"
|
||||
import { getLatestAnnouncementId } from "@/utils/announcements"
|
||||
|
||||
/**
|
||||
* Marks the current announcement as shown
|
||||
@@ -12,8 +13,9 @@ import { updateGlobalState } from "../../storage/state"
|
||||
*/
|
||||
export async function onDidShowAnnouncement(controller: Controller, _request: EmptyRequest): Promise<Boolean> {
|
||||
try {
|
||||
const latestAnnouncementId = getLatestAnnouncementId(controller.context)
|
||||
// Update the lastShownAnnouncementId to the current latestAnnouncementId
|
||||
await updateGlobalState(controller.context, "lastShownAnnouncementId", controller.latestAnnouncementId)
|
||||
await updateGlobalState(controller.context, "lastShownAnnouncementId", latestAnnouncementId)
|
||||
return Boolean.create({ value: false })
|
||||
} catch (error) {
|
||||
console.error("Failed to acknowledge announcement:", error)
|
||||
|
||||
@@ -0,0 +1,422 @@
|
||||
import type { FileContextTracker } from "@core/context/context-tracking/FileContextTracker"
|
||||
import * as extractTextModule from "@integrations/misc/extract-text"
|
||||
import * as terminalModule from "@integrations/terminal/get-latest-output"
|
||||
import type { UrlContentFetcher } from "@services/browser/UrlContentFetcher"
|
||||
import * as gitModule from "@utils/git"
|
||||
import { expect } from "chai"
|
||||
import * as fs from "fs"
|
||||
import * as isBinaryFileModule from "isbinaryfile"
|
||||
import * as path from "path"
|
||||
import * as sinon from "sinon"
|
||||
import { type DiffViewProviderCreator, HostProvider, type WebviewProviderCreator } from "@/hosts/host-provider"
|
||||
import { vscodeHostBridgeClient } from "@/hosts/vscode/hostbridge/client/host-grpc-client"
|
||||
import { parseMentions } from "../index"
|
||||
|
||||
describe("parseMentions", () => {
|
||||
let sandbox: sinon.SinonSandbox
|
||||
let urlContentFetcherStub: sinon.SinonStubbedInstance<UrlContentFetcher>
|
||||
let fileContextTrackerStub: sinon.SinonStubbedInstance<FileContextTracker>
|
||||
let fsStatStub: sinon.SinonStub
|
||||
let fsReaddirStub: sinon.SinonStub
|
||||
let extractTextStub: sinon.SinonStub
|
||||
let isBinaryFileStub: sinon.SinonStub
|
||||
let getLatestTerminalOutputStub: sinon.SinonStub
|
||||
let getWorkingStateStub: sinon.SinonStub
|
||||
let getCommitInfoStub: sinon.SinonStub
|
||||
let showMessageStub: sinon.SinonStub
|
||||
|
||||
const cwd = "/test/project"
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
HostProvider.reset()
|
||||
HostProvider.initialize(
|
||||
((_) => {}) as WebviewProviderCreator,
|
||||
(() => {}) as DiffViewProviderCreator,
|
||||
vscodeHostBridgeClient,
|
||||
(_) => {},
|
||||
async () => "http://localhost:3000/callback",
|
||||
)
|
||||
// Create stubs for dependencies
|
||||
urlContentFetcherStub = {
|
||||
launchBrowser: sandbox.stub().resolves(),
|
||||
closeBrowser: sandbox.stub().resolves(),
|
||||
urlToMarkdown: sandbox.stub().resolves("# Example Website\n\nContent here"),
|
||||
} as any
|
||||
|
||||
fileContextTrackerStub = {
|
||||
trackFileContext: sandbox.stub().resolves(),
|
||||
} as any
|
||||
|
||||
// Stub file system operations using fs.promises
|
||||
fsStatStub = sandbox.stub(fs.promises, "stat")
|
||||
fsReaddirStub = sandbox.stub(fs.promises, "readdir")
|
||||
|
||||
// Stub other modules
|
||||
extractTextStub = sandbox.stub(extractTextModule, "extractTextFromFile")
|
||||
isBinaryFileStub = sandbox.stub(isBinaryFileModule, "isBinaryFile")
|
||||
getLatestTerminalOutputStub = sandbox.stub(terminalModule, "getLatestTerminalOutput")
|
||||
getWorkingStateStub = sandbox.stub(gitModule, "getWorkingState")
|
||||
getCommitInfoStub = sandbox.stub(gitModule, "getCommitInfo")
|
||||
showMessageStub = sandbox.stub(HostProvider.window, "showMessage")
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore()
|
||||
})
|
||||
|
||||
describe("File mentions", () => {
|
||||
it("should handle simple file mention", async () => {
|
||||
const text = "Check @/src/index.ts for details"
|
||||
|
||||
fsStatStub.resolves({ isFile: () => true, isDirectory: () => false })
|
||||
isBinaryFileStub.resolves(false)
|
||||
extractTextStub.resolves("console.log('Hello World');")
|
||||
|
||||
const result = await parseMentions(text, cwd, urlContentFetcherStub, fileContextTrackerStub)
|
||||
|
||||
const expectedOutput = `Check 'src/index.ts' (see below for file content) for details
|
||||
|
||||
<file_content path="src/index.ts">
|
||||
console.log('Hello World');
|
||||
</file_content>`
|
||||
|
||||
expect(result).to.equal(expectedOutput)
|
||||
expect(fileContextTrackerStub.trackFileContext.calledWith("src/index.ts", "file_mentioned")).to.be.true
|
||||
})
|
||||
|
||||
it("should handle quoted file paths with spaces", async () => {
|
||||
const text = 'Open @"/path with spaces/file.txt"'
|
||||
|
||||
fsStatStub.resolves({ isFile: () => true, isDirectory: () => false })
|
||||
isBinaryFileStub.resolves(false)
|
||||
extractTextStub.resolves("console.log('Hello World');")
|
||||
|
||||
const result = await parseMentions(text, cwd, urlContentFetcherStub)
|
||||
|
||||
const expectedOutput = `Open 'path with spaces/file.txt' (see below for file content)
|
||||
|
||||
<file_content path="path with spaces/file.txt">
|
||||
console.log('Hello World');
|
||||
</file_content>`
|
||||
|
||||
expect(result).to.equal(expectedOutput)
|
||||
})
|
||||
|
||||
it("should handle binary files", async () => {
|
||||
const text = "Check @/image.png"
|
||||
|
||||
fsStatStub.resolves({ isFile: () => true, isDirectory: () => false })
|
||||
isBinaryFileStub.resolves(true)
|
||||
|
||||
const result = await parseMentions(text, cwd, urlContentFetcherStub)
|
||||
|
||||
const expectedOutput = `Check 'image.png' (see below for file content)
|
||||
|
||||
<file_content path="image.png">
|
||||
(Binary file, unable to display content)
|
||||
</file_content>`
|
||||
|
||||
expect(result).to.equal(expectedOutput)
|
||||
})
|
||||
|
||||
it("should handle file read errors", async () => {
|
||||
const text = "Check @/missing.txt"
|
||||
|
||||
fsStatStub.rejects(new Error("ENOENT: no such file or directory"))
|
||||
|
||||
const result = await parseMentions(text, cwd, urlContentFetcherStub)
|
||||
|
||||
const expectedOutput = `Check 'missing.txt' (see below for file content)
|
||||
|
||||
<file_content path="missing.txt">
|
||||
Error fetching content: Failed to access path "missing.txt": ENOENT: no such file or directory
|
||||
</file_content>`
|
||||
|
||||
expect(result).to.equal(expectedOutput)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Folder mentions", () => {
|
||||
it("should handle folder mention", async () => {
|
||||
const text = "Look in @/src/ folder"
|
||||
|
||||
fsStatStub.resolves({ isFile: () => false, isDirectory: () => true })
|
||||
fsReaddirStub.resolves([
|
||||
{ name: "index.ts", isFile: () => true, isDirectory: () => false },
|
||||
{ name: "utils", isFile: () => false, isDirectory: () => true },
|
||||
{ name: "README.md", isFile: () => true, isDirectory: () => false },
|
||||
])
|
||||
|
||||
// Set up file content stubs
|
||||
isBinaryFileStub.resolves(false)
|
||||
extractTextStub.withArgs(path.resolve(cwd, "src/index.ts")).resolves("export const main = () => {};")
|
||||
extractTextStub.withArgs(path.resolve(cwd, "src/README.md")).resolves("# Source Code")
|
||||
|
||||
const result = await parseMentions(text, cwd, urlContentFetcherStub)
|
||||
|
||||
const expectedOutput = `Look in 'src/' (see below for folder content) folder
|
||||
|
||||
<folder_content path="src/">
|
||||
├── index.ts
|
||||
├── utils/
|
||||
└── README.md
|
||||
|
||||
<file_content path="src/index.ts">
|
||||
export const main = () => {};
|
||||
</file_content>
|
||||
|
||||
<file_content path="src/README.md">
|
||||
# Source Code
|
||||
</file_content>
|
||||
</folder_content>`
|
||||
|
||||
expect(result).to.equal(expectedOutput)
|
||||
})
|
||||
})
|
||||
|
||||
describe("URL mentions", () => {
|
||||
it("should handle URL mention", async () => {
|
||||
const text = "Visit @https://example.com for info"
|
||||
|
||||
const result = await parseMentions(text, cwd, urlContentFetcherStub)
|
||||
|
||||
const expectedOutput = `Visit 'https://example.com' (see below for site content) for info
|
||||
|
||||
<url_content url="https://example.com">
|
||||
# Example Website
|
||||
|
||||
Content here
|
||||
</url_content>`
|
||||
|
||||
expect(result).to.equal(expectedOutput)
|
||||
expect(urlContentFetcherStub.launchBrowser.called).to.be.true
|
||||
expect(urlContentFetcherStub.urlToMarkdown.calledWith("https://example.com")).to.be.true
|
||||
expect(urlContentFetcherStub.closeBrowser.called).to.be.true
|
||||
})
|
||||
|
||||
it("should handle browser launch errors", async () => {
|
||||
const text = "Visit @https://example.com"
|
||||
|
||||
urlContentFetcherStub.launchBrowser.rejects(new Error("Browser launch failed"))
|
||||
|
||||
const result = await parseMentions(text, cwd, urlContentFetcherStub)
|
||||
|
||||
const expectedOutput = `Visit 'https://example.com' (see below for site content)
|
||||
|
||||
<url_content url="https://example.com">
|
||||
Error fetching content: Browser launch failed
|
||||
</url_content>`
|
||||
|
||||
expect(result).to.equal(expectedOutput)
|
||||
expect(showMessageStub.called).to.be.true
|
||||
})
|
||||
|
||||
it("should handle URL fetch errors", async () => {
|
||||
const text = "Visit @https://example.com"
|
||||
|
||||
urlContentFetcherStub.urlToMarkdown.rejects(new Error("Network error"))
|
||||
|
||||
const result = await parseMentions(text, cwd, urlContentFetcherStub)
|
||||
|
||||
const expectedOutput = `Visit 'https://example.com' (see below for site content)
|
||||
|
||||
<url_content url="https://example.com">
|
||||
Error fetching content: Network error
|
||||
</url_content>`
|
||||
|
||||
expect(result).to.equal(expectedOutput)
|
||||
expect(showMessageStub.called).to.be.true
|
||||
})
|
||||
})
|
||||
|
||||
describe("Special mentions", () => {
|
||||
it("should handle @terminal mention", async () => {
|
||||
const text = "See @terminal output"
|
||||
|
||||
getLatestTerminalOutputStub.resolves("$ npm test\nAll tests passed!")
|
||||
|
||||
const result = await parseMentions(text, cwd, urlContentFetcherStub)
|
||||
|
||||
const expectedOutput = `See Terminal Output (see below for output) output
|
||||
|
||||
<terminal_output>
|
||||
$ npm test
|
||||
All tests passed!
|
||||
</terminal_output>`
|
||||
|
||||
expect(result).to.equal(expectedOutput)
|
||||
})
|
||||
|
||||
it("should handle @git-changes mention", async () => {
|
||||
const text = "Review @git-changes"
|
||||
|
||||
getWorkingStateStub.resolves("M src/index.ts\nA src/new-file.ts")
|
||||
|
||||
const result = await parseMentions(text, cwd, urlContentFetcherStub)
|
||||
|
||||
const expectedOutput = `Review Working directory changes (see below for details)
|
||||
|
||||
<git_working_state>
|
||||
M src/index.ts
|
||||
A src/new-file.ts
|
||||
</git_working_state>`
|
||||
|
||||
expect(result).to.equal(expectedOutput)
|
||||
})
|
||||
|
||||
it("should handle git commit hash mention", async () => {
|
||||
const text = "See commit @abcdef1234567890"
|
||||
|
||||
getCommitInfoStub.resolves("commit abcdef1234567890\nAuthor: Test\nDate: 2024-01-01\n\nInitial commit")
|
||||
|
||||
const result = await parseMentions(text, cwd, urlContentFetcherStub)
|
||||
|
||||
const expectedOutput = `See commit Git commit 'abcdef1234567890' (see below for commit info)
|
||||
|
||||
<git_commit hash="abcdef1234567890">
|
||||
commit abcdef1234567890
|
||||
Author: Test
|
||||
Date: 2024-01-01
|
||||
|
||||
Initial commit
|
||||
</git_commit>`
|
||||
|
||||
expect(result).to.equal(expectedOutput)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Multiple mentions", () => {
|
||||
it("should handle multiple mentions in order", async () => {
|
||||
const text = "Check @/file1.txt and @/file2.txt"
|
||||
|
||||
fsStatStub.resolves({ isFile: () => true, isDirectory: () => false })
|
||||
isBinaryFileStub.resolves(false)
|
||||
extractTextStub.withArgs(path.resolve(cwd, "file1.txt")).resolves("Content 1")
|
||||
extractTextStub.withArgs(path.resolve(cwd, "file2.txt")).resolves("Content 2")
|
||||
|
||||
const result = await parseMentions(text, cwd, urlContentFetcherStub)
|
||||
|
||||
const expectedOutput = `Check 'file1.txt' (see below for file content) and 'file2.txt' (see below for file content)
|
||||
|
||||
<file_content path="file1.txt">
|
||||
Content 1
|
||||
</file_content>
|
||||
|
||||
<file_content path="file2.txt">
|
||||
Content 2
|
||||
</file_content>`
|
||||
|
||||
expect(result).to.equal(expectedOutput)
|
||||
})
|
||||
|
||||
it("should handle duplicate mentions only once", async () => {
|
||||
const text = "Check @/file.txt and again @/file.txt"
|
||||
|
||||
fsStatStub.resolves({ isFile: () => true, isDirectory: () => false })
|
||||
isBinaryFileStub.resolves(false)
|
||||
extractTextStub.resolves("Content")
|
||||
|
||||
const result = await parseMentions(text, cwd, urlContentFetcherStub)
|
||||
|
||||
const expectedOutput = `Check 'file.txt' (see below for file content) and again 'file.txt' (see below for file content)
|
||||
|
||||
<file_content path="file.txt">
|
||||
Content
|
||||
</file_content>`
|
||||
|
||||
expect(result).to.equal(expectedOutput)
|
||||
})
|
||||
|
||||
it("should handle mixed mention types", async () => {
|
||||
const text = "Check @/file.txt, and @https://example.com"
|
||||
|
||||
fsStatStub.resolves({ isFile: () => true, isDirectory: () => false })
|
||||
isBinaryFileStub.resolves(false)
|
||||
extractTextStub.resolves("File content")
|
||||
|
||||
const result = await parseMentions(text, cwd, urlContentFetcherStub)
|
||||
|
||||
const expectedOutput = `Check 'file.txt' (see below for file content), and 'https://example.com' (see below for site content)
|
||||
|
||||
<file_content path="file.txt">
|
||||
File content
|
||||
</file_content>
|
||||
|
||||
<url_content url="https://example.com">
|
||||
# Example Website
|
||||
|
||||
Content here
|
||||
</url_content>`
|
||||
|
||||
expect(result).to.equal(expectedOutput)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Error handling", () => {
|
||||
it("should handle errors for each mention type gracefully", async () => {
|
||||
const text = "@/error.txt @terminal @git-changes @abc1234567"
|
||||
|
||||
fsStatStub.rejects(new Error("File error"))
|
||||
getLatestTerminalOutputStub.rejects(new Error("Terminal error"))
|
||||
getWorkingStateStub.rejects(new Error("Git state error"))
|
||||
getCommitInfoStub.rejects(new Error("Commit error"))
|
||||
|
||||
const result = await parseMentions(text, cwd, urlContentFetcherStub)
|
||||
|
||||
const expectedOutput = `'error.txt' (see below for file content) Terminal Output (see below for output) Working directory changes (see below for details) Git commit 'abc1234567' (see below for commit info)
|
||||
|
||||
<file_content path="error.txt">
|
||||
Error fetching content: Failed to access path "error.txt": File error
|
||||
</file_content>
|
||||
|
||||
<terminal_output>
|
||||
Error fetching terminal output: Terminal error
|
||||
</terminal_output>
|
||||
|
||||
<git_working_state>
|
||||
Error fetching working state: Git state error
|
||||
</git_working_state>
|
||||
|
||||
<git_commit hash="abc1234567">
|
||||
Error fetching commit info: Commit error
|
||||
</git_commit>`
|
||||
|
||||
expect(result).to.equal(expectedOutput)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Edge cases", () => {
|
||||
it("should handle text with no mentions", async () => {
|
||||
const text = "This is plain text without any mentions"
|
||||
|
||||
const result = await parseMentions(text, cwd, urlContentFetcherStub)
|
||||
|
||||
expect(result).to.equal(text)
|
||||
})
|
||||
|
||||
it("should handle empty text", async () => {
|
||||
const result = await parseMentions("", cwd, urlContentFetcherStub)
|
||||
|
||||
expect(result).to.equal("")
|
||||
})
|
||||
|
||||
it("should handle mentions with trailing punctuation", async () => {
|
||||
const text = "Check @/file.txt!"
|
||||
|
||||
fsStatStub.resolves({ isFile: () => true, isDirectory: () => false })
|
||||
isBinaryFileStub.resolves(false)
|
||||
extractTextStub.resolves("Content")
|
||||
|
||||
const result = await parseMentions(text, cwd, urlContentFetcherStub)
|
||||
|
||||
const expectedOutput = `Check 'file.txt' (see below for file content)!
|
||||
|
||||
<file_content path="file.txt">
|
||||
Content
|
||||
</file_content>`
|
||||
|
||||
expect(result).to.equal(expectedOutput)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -14,7 +14,7 @@ import { FileContextTracker } from "../context/context-tracking/FileContextTrack
|
||||
import { getCwd } from "@/utils/path"
|
||||
import { openExternal } from "@utils/env"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { ShowMessageType } from "@/shared/proto/host/window"
|
||||
|
||||
export async function openMention(mention?: string): Promise<void> {
|
||||
if (!mention) {
|
||||
@@ -26,8 +26,8 @@ export async function openMention(mention?: string): Promise<void> {
|
||||
return
|
||||
}
|
||||
|
||||
if (mention.startsWith("/")) {
|
||||
const relPath = mention.slice(1)
|
||||
if (isFileMention(mention)) {
|
||||
const relPath = getFilePathFromMention(mention)
|
||||
const absPath = path.resolve(cwd, relPath)
|
||||
if (mention.endsWith("/")) {
|
||||
vscode.commands.executeCommand("revealInExplorer", vscode.Uri.file(absPath))
|
||||
@@ -54,8 +54,8 @@ export async function parseMentions(
|
||||
mentions.add(mention)
|
||||
if (mention.startsWith("http")) {
|
||||
return `'${mention}' (see below for site content)`
|
||||
} else if (mention.startsWith("/")) {
|
||||
const mentionPath = mention.slice(1) // Remove the leading '/'
|
||||
} else if (isFileMention(mention)) {
|
||||
const mentionPath = getFilePathFromMention(mention)
|
||||
return mentionPath.endsWith("/")
|
||||
? `'${mentionPath}' (see below for folder content)`
|
||||
: `'${mentionPath}' (see below for file content)`
|
||||
@@ -106,8 +106,8 @@ export async function parseMentions(
|
||||
}
|
||||
}
|
||||
parsedText += `\n\n<url_content url="${mention}">\n${result}\n</url_content>`
|
||||
} else if (mention.startsWith("/")) {
|
||||
const mentionPath = mention.slice(1)
|
||||
} else if (isFileMention(mention)) {
|
||||
const mentionPath = getFilePathFromMention(mention)
|
||||
try {
|
||||
const content = await getFileOrFolderContent(mentionPath, cwd)
|
||||
if (mention.endsWith("/")) {
|
||||
@@ -232,3 +232,15 @@ async function getWorkspaceProblems(): Promise<string> {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function isFileMention(mention: string): boolean {
|
||||
return mention.startsWith("/") || mention.startsWith('"/')
|
||||
}
|
||||
|
||||
function getFilePathFromMention(mention: string): string {
|
||||
// Remove quotes
|
||||
const match = mention.match(/^"(.*)"$/)
|
||||
const filePath = match ? match[1] : mention
|
||||
// Remove leading slash
|
||||
return filePath.slice(1)
|
||||
}
|
||||
|
||||
@@ -337,7 +337,6 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
|
||||
accessMcpResourceToolDefinition,
|
||||
loadMcpDocumentationTool,
|
||||
newTaskToolDefinition,
|
||||
editToolDefinition,
|
||||
]
|
||||
if (supportsBrowserUse) {
|
||||
tools.push(browserActionTool)
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import * as diff from "diff"
|
||||
import * as path from "path"
|
||||
import { ClineIgnoreController, LOCK_TEXT_SYMBOL } from "../ignore/ClineIgnoreController"
|
||||
import { Mode } from "@/shared/ChatSettings"
|
||||
import { Mode } from "@/shared/storage/types"
|
||||
|
||||
export const formatResponse = {
|
||||
duplicateFileReadNotice: () =>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { McpHub } from "@services/mcp/McpHub"
|
||||
import { BrowserSettings } from "@shared/BrowserSettings"
|
||||
import { SYSTEM_PROMPT_CLAUDE4_EXPERIMENTAL } from "@core/prompts/model_prompts/claude4-experimental"
|
||||
import { SYSTEM_PROMPT_CLAUDE4 } from "@core/prompts/model_prompts/claude4"
|
||||
import { USE_EXPERIMENTAL_CLAUDE4_FEATURES } from "@core/task/index";
|
||||
import { USE_EXPERIMENTAL_CLAUDE4_FEATURES } from "@core/task/index"
|
||||
|
||||
export const SYSTEM_PROMPT = async (
|
||||
cwd: string,
|
||||
@@ -14,14 +14,13 @@ export const SYSTEM_PROMPT = async (
|
||||
browserSettings: BrowserSettings,
|
||||
isNextGenModel: boolean = false,
|
||||
) => {
|
||||
|
||||
if (isNextGenModel && USE_EXPERIMENTAL_CLAUDE4_FEATURES) {
|
||||
return SYSTEM_PROMPT_CLAUDE4_EXPERIMENTAL(cwd, supportsBrowserUse, mcpHub, browserSettings)
|
||||
}
|
||||
|
||||
if (isNextGenModel) {
|
||||
return SYSTEM_PROMPT_CLAUDE4(cwd, supportsBrowserUse, mcpHub, browserSettings)
|
||||
}
|
||||
if (isNextGenModel) {
|
||||
return SYSTEM_PROMPT_CLAUDE4(cwd, supportsBrowserUse, mcpHub, browserSettings)
|
||||
}
|
||||
|
||||
return `You are Cline, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.
|
||||
|
||||
@@ -650,7 +649,6 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
|
||||
5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.`
|
||||
}
|
||||
|
||||
|
||||
export function addUserInstructions(
|
||||
globalClineRulesFileInstructions?: string,
|
||||
localClineRulesFileInstructions?: string,
|
||||
|
||||
@@ -0,0 +1,935 @@
|
||||
import { ApiConfiguration } from "@shared/api"
|
||||
import { updateGlobalState, updateWorkspaceState, getAllExtensionState, storeSecret } from "./state"
|
||||
import { SecretKey, GlobalStateKey, LocalStateKey } from "./state-keys"
|
||||
import { CACHE_SERVICE_NOT_INITIALIZED } from "./error-messages"
|
||||
import type { ExtensionContext } from "vscode"
|
||||
|
||||
/**
|
||||
* Interface for persistence error event data
|
||||
*/
|
||||
export interface PersistenceErrorEvent {
|
||||
error: Error
|
||||
}
|
||||
|
||||
/**
|
||||
* In-memory cache service for fast state access
|
||||
* Provides immediate reads/writes with async disk persistence
|
||||
*/
|
||||
export class CacheService {
|
||||
private globalStateCache: Map<GlobalStateKey, any> = new Map()
|
||||
private secretsCache: Map<SecretKey, string | undefined> = new Map()
|
||||
private workspaceStateCache: Map<LocalStateKey, any> = new Map()
|
||||
private context: ExtensionContext
|
||||
private isInitialized = false
|
||||
|
||||
// Debounced persistence state
|
||||
private pendingGlobalState = new Set<GlobalStateKey>()
|
||||
private pendingSecrets = new Set<SecretKey>()
|
||||
private pendingWorkspaceState = new Set<LocalStateKey>()
|
||||
private persistenceTimeout: NodeJS.Timeout | null = null
|
||||
private readonly PERSISTENCE_DELAY_MS = 500
|
||||
|
||||
// Callback for persistence errors
|
||||
onPersistenceError?: (event: PersistenceErrorEvent) => void
|
||||
|
||||
constructor(context: ExtensionContext) {
|
||||
this.context = context
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the cache by loading data from disk
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
try {
|
||||
// Load API configuration and populate cache with component keys
|
||||
const { apiConfiguration } = await getAllExtensionState(this.context)
|
||||
if (apiConfiguration) {
|
||||
// Populate the caches with the API configuration component keys
|
||||
// Use populate method to avoid triggering persistence during initialization
|
||||
this.populateApiConfigurationCache(apiConfiguration)
|
||||
}
|
||||
|
||||
this.isInitialized = true
|
||||
console.log("CacheService initialized successfully")
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize CacheService:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set method for global state keys - updates cache immediately and schedules debounced persistence
|
||||
*/
|
||||
setGlobalState<T>(key: GlobalStateKey, value: T): void {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error(CACHE_SERVICE_NOT_INITIALIZED)
|
||||
}
|
||||
|
||||
// Update cache immediately for instant access
|
||||
this.globalStateCache.set(key, value)
|
||||
|
||||
// Add to pending persistence set and schedule debounced write
|
||||
this.pendingGlobalState.add(key)
|
||||
this.scheduleDebouncedPersistence()
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch set method for global state keys - updates cache immediately and schedules debounced persistence
|
||||
*/
|
||||
setGlobalStateBatch(updates: Partial<Record<GlobalStateKey, any>>): void {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error(CACHE_SERVICE_NOT_INITIALIZED)
|
||||
}
|
||||
|
||||
// Update cache immediately for all keys
|
||||
Object.entries(updates).forEach(([key, value]) => {
|
||||
this.globalStateCache.set(key as GlobalStateKey, value)
|
||||
this.pendingGlobalState.add(key as GlobalStateKey)
|
||||
})
|
||||
|
||||
// Schedule debounced persistence
|
||||
this.scheduleDebouncedPersistence()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set method for secret keys - updates cache immediately and schedules debounced persistence
|
||||
*/
|
||||
setSecret(key: SecretKey, value: string | undefined): void {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error(CACHE_SERVICE_NOT_INITIALIZED)
|
||||
}
|
||||
|
||||
// Update cache immediately for instant access
|
||||
this.secretsCache.set(key, value)
|
||||
|
||||
// Add to pending persistence set and schedule debounced write
|
||||
this.pendingSecrets.add(key)
|
||||
this.scheduleDebouncedPersistence()
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch set method for secret keys - updates cache immediately and schedules debounced persistence
|
||||
*/
|
||||
setSecretsBatch(updates: Partial<Record<SecretKey, string | undefined>>): void {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error(CACHE_SERVICE_NOT_INITIALIZED)
|
||||
}
|
||||
|
||||
// Update cache immediately for all keys
|
||||
Object.entries(updates).forEach(([key, value]) => {
|
||||
this.secretsCache.set(key as SecretKey, value)
|
||||
this.pendingSecrets.add(key as SecretKey)
|
||||
})
|
||||
|
||||
// Schedule debounced persistence
|
||||
this.scheduleDebouncedPersistence()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set method for workspace state keys - updates cache immediately and schedules debounced persistence
|
||||
*/
|
||||
setWorkspaceState<T>(key: LocalStateKey, value: T): void {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error(CACHE_SERVICE_NOT_INITIALIZED)
|
||||
}
|
||||
|
||||
// Update cache immediately for instant access
|
||||
this.workspaceStateCache.set(key, value)
|
||||
|
||||
// Add to pending persistence set and schedule debounced write
|
||||
this.pendingWorkspaceState.add(key)
|
||||
this.scheduleDebouncedPersistence()
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch set method for workspace state keys - updates cache immediately and schedules debounced persistence
|
||||
*/
|
||||
setWorkspaceStateBatch(updates: Partial<Record<LocalStateKey, any>>): void {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error(CACHE_SERVICE_NOT_INITIALIZED)
|
||||
}
|
||||
|
||||
// Update cache immediately for all keys
|
||||
Object.entries(updates).forEach(([key, value]) => {
|
||||
this.workspaceStateCache.set(key as LocalStateKey, value)
|
||||
this.pendingWorkspaceState.add(key as LocalStateKey)
|
||||
})
|
||||
|
||||
// Schedule debounced persistence
|
||||
this.scheduleDebouncedPersistence()
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for getting API configuration
|
||||
* Ensures cache is initialized if not already done
|
||||
*/
|
||||
getApiConfiguration(): ApiConfiguration {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error(CACHE_SERVICE_NOT_INITIALIZED)
|
||||
}
|
||||
|
||||
// Construct API configuration from cached component keys
|
||||
return this.constructApiConfigurationFromCache()
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for setting API configuration
|
||||
*/
|
||||
setApiConfiguration(apiConfiguration: ApiConfiguration): void {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error(CACHE_SERVICE_NOT_INITIALIZED)
|
||||
}
|
||||
|
||||
const {
|
||||
apiKey,
|
||||
openRouterApiKey,
|
||||
awsAccessKey,
|
||||
awsSecretKey,
|
||||
awsSessionToken,
|
||||
awsRegion,
|
||||
awsUseCrossRegionInference,
|
||||
awsBedrockUsePromptCache,
|
||||
awsBedrockEndpoint,
|
||||
awsBedrockApiKey,
|
||||
awsProfile,
|
||||
awsUseProfile,
|
||||
awsAuthentication,
|
||||
vertexProjectId,
|
||||
vertexRegion,
|
||||
openAiBaseUrl,
|
||||
openAiApiKey,
|
||||
openAiHeaders,
|
||||
ollamaBaseUrl,
|
||||
ollamaApiOptionsCtxNum,
|
||||
lmStudioBaseUrl,
|
||||
anthropicBaseUrl,
|
||||
geminiApiKey,
|
||||
geminiBaseUrl,
|
||||
openAiNativeApiKey,
|
||||
deepSeekApiKey,
|
||||
requestyApiKey,
|
||||
togetherApiKey,
|
||||
qwenApiKey,
|
||||
doubaoApiKey,
|
||||
mistralApiKey,
|
||||
azureApiVersion,
|
||||
openRouterProviderSorting,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmApiKey,
|
||||
liteLlmUsePromptCache,
|
||||
qwenApiLine,
|
||||
moonshotApiLine,
|
||||
asksageApiKey,
|
||||
asksageApiUrl,
|
||||
xaiApiKey,
|
||||
clineAccountId,
|
||||
sambanovaApiKey,
|
||||
cerebrasApiKey,
|
||||
groqApiKey,
|
||||
moonshotApiKey,
|
||||
nebiusApiKey,
|
||||
favoritedModelIds,
|
||||
fireworksApiKey,
|
||||
fireworksModelMaxCompletionTokens,
|
||||
fireworksModelMaxTokens,
|
||||
sapAiCoreClientId,
|
||||
sapAiCoreClientSecret,
|
||||
sapAiCoreBaseUrl,
|
||||
sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup,
|
||||
claudeCodePath,
|
||||
huggingFaceApiKey,
|
||||
requestTimeoutMs,
|
||||
// Plan mode configurations
|
||||
planModeApiProvider,
|
||||
planModeApiModelId,
|
||||
planModeThinkingBudgetTokens,
|
||||
planModeReasoningEffort,
|
||||
planModeVsCodeLmModelSelector,
|
||||
planModeAwsBedrockCustomSelected,
|
||||
planModeAwsBedrockCustomModelBaseId,
|
||||
planModeOpenRouterModelId,
|
||||
planModeOpenRouterModelInfo,
|
||||
planModeOpenAiModelId,
|
||||
planModeOpenAiModelInfo,
|
||||
planModeOllamaModelId,
|
||||
planModeLmStudioModelId,
|
||||
planModeLiteLlmModelId,
|
||||
planModeLiteLlmModelInfo,
|
||||
planModeRequestyModelId,
|
||||
planModeRequestyModelInfo,
|
||||
planModeTogetherModelId,
|
||||
planModeFireworksModelId,
|
||||
planModeSapAiCoreModelId,
|
||||
planModeGroqModelId,
|
||||
planModeGroqModelInfo,
|
||||
planModeHuggingFaceModelId,
|
||||
planModeHuggingFaceModelInfo,
|
||||
// Act mode configurations
|
||||
actModeApiProvider,
|
||||
actModeApiModelId,
|
||||
actModeThinkingBudgetTokens,
|
||||
actModeReasoningEffort,
|
||||
actModeVsCodeLmModelSelector,
|
||||
actModeAwsBedrockCustomSelected,
|
||||
actModeAwsBedrockCustomModelBaseId,
|
||||
actModeOpenRouterModelId,
|
||||
actModeOpenRouterModelInfo,
|
||||
actModeOpenAiModelId,
|
||||
actModeOpenAiModelInfo,
|
||||
actModeOllamaModelId,
|
||||
actModeLmStudioModelId,
|
||||
actModeLiteLlmModelId,
|
||||
actModeLiteLlmModelInfo,
|
||||
actModeRequestyModelId,
|
||||
actModeRequestyModelInfo,
|
||||
actModeTogetherModelId,
|
||||
actModeFireworksModelId,
|
||||
actModeSapAiCoreModelId,
|
||||
actModeGroqModelId,
|
||||
actModeGroqModelInfo,
|
||||
actModeHuggingFaceModelId,
|
||||
actModeHuggingFaceModelInfo,
|
||||
} = apiConfiguration
|
||||
|
||||
// Batch update global state keys
|
||||
this.setGlobalStateBatch({
|
||||
// Plan mode configuration updates
|
||||
planModeApiProvider,
|
||||
planModeApiModelId,
|
||||
planModeThinkingBudgetTokens,
|
||||
planModeReasoningEffort,
|
||||
planModeVsCodeLmModelSelector,
|
||||
planModeAwsBedrockCustomSelected,
|
||||
planModeAwsBedrockCustomModelBaseId,
|
||||
planModeOpenRouterModelId,
|
||||
planModeOpenRouterModelInfo,
|
||||
planModeOpenAiModelId,
|
||||
planModeOpenAiModelInfo,
|
||||
planModeOllamaModelId,
|
||||
planModeLmStudioModelId,
|
||||
planModeLiteLlmModelId,
|
||||
planModeLiteLlmModelInfo,
|
||||
planModeRequestyModelId,
|
||||
planModeRequestyModelInfo,
|
||||
planModeTogetherModelId,
|
||||
planModeFireworksModelId,
|
||||
planModeSapAiCoreModelId,
|
||||
planModeGroqModelId,
|
||||
planModeGroqModelInfo,
|
||||
planModeHuggingFaceModelId,
|
||||
planModeHuggingFaceModelInfo,
|
||||
|
||||
// Act mode configuration updates
|
||||
actModeApiProvider,
|
||||
actModeApiModelId,
|
||||
actModeThinkingBudgetTokens,
|
||||
actModeReasoningEffort,
|
||||
actModeVsCodeLmModelSelector,
|
||||
actModeAwsBedrockCustomSelected,
|
||||
actModeAwsBedrockCustomModelBaseId,
|
||||
actModeOpenRouterModelId,
|
||||
actModeOpenRouterModelInfo,
|
||||
actModeOpenAiModelId,
|
||||
actModeOpenAiModelInfo,
|
||||
actModeOllamaModelId,
|
||||
actModeLmStudioModelId,
|
||||
actModeLiteLlmModelId,
|
||||
actModeLiteLlmModelInfo,
|
||||
actModeRequestyModelId,
|
||||
actModeRequestyModelInfo,
|
||||
actModeTogetherModelId,
|
||||
actModeFireworksModelId,
|
||||
actModeSapAiCoreModelId,
|
||||
actModeGroqModelId,
|
||||
actModeGroqModelInfo,
|
||||
actModeHuggingFaceModelId,
|
||||
actModeHuggingFaceModelInfo,
|
||||
|
||||
// Global state updates
|
||||
awsRegion,
|
||||
awsUseCrossRegionInference,
|
||||
awsBedrockUsePromptCache,
|
||||
awsBedrockEndpoint,
|
||||
awsProfile,
|
||||
awsUseProfile,
|
||||
awsAuthentication,
|
||||
vertexProjectId,
|
||||
vertexRegion,
|
||||
openAiBaseUrl,
|
||||
openAiHeaders,
|
||||
ollamaBaseUrl,
|
||||
ollamaApiOptionsCtxNum,
|
||||
lmStudioBaseUrl,
|
||||
anthropicBaseUrl,
|
||||
geminiBaseUrl,
|
||||
azureApiVersion,
|
||||
openRouterProviderSorting,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmUsePromptCache,
|
||||
qwenApiLine,
|
||||
moonshotApiLine,
|
||||
asksageApiUrl,
|
||||
favoritedModelIds,
|
||||
requestTimeoutMs,
|
||||
fireworksModelMaxCompletionTokens,
|
||||
fireworksModelMaxTokens,
|
||||
sapAiCoreBaseUrl,
|
||||
sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup,
|
||||
claudeCodePath,
|
||||
})
|
||||
|
||||
// Batch update secrets
|
||||
this.setSecretsBatch({
|
||||
apiKey,
|
||||
openRouterApiKey,
|
||||
clineAccountId,
|
||||
awsAccessKey,
|
||||
awsSecretKey,
|
||||
awsSessionToken,
|
||||
awsBedrockApiKey,
|
||||
openAiApiKey,
|
||||
geminiApiKey,
|
||||
openAiNativeApiKey,
|
||||
deepSeekApiKey,
|
||||
requestyApiKey,
|
||||
togetherApiKey,
|
||||
qwenApiKey,
|
||||
doubaoApiKey,
|
||||
mistralApiKey,
|
||||
liteLlmApiKey,
|
||||
fireworksApiKey,
|
||||
asksageApiKey,
|
||||
xaiApiKey,
|
||||
sambanovaApiKey,
|
||||
cerebrasApiKey,
|
||||
groqApiKey,
|
||||
moonshotApiKey,
|
||||
nebiusApiKey,
|
||||
sapAiCoreClientId,
|
||||
sapAiCoreClientSecret,
|
||||
huggingFaceApiKey,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get method for global state keys - reads from in-memory cache
|
||||
*/
|
||||
getGlobalStateKey<T>(key: GlobalStateKey): T | undefined {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error(CACHE_SERVICE_NOT_INITIALIZED)
|
||||
}
|
||||
return this.globalStateCache.get(key) as T | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Get method for secret keys - reads from in-memory cache
|
||||
*/
|
||||
getSecretKey(key: SecretKey): string | undefined {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error(CACHE_SERVICE_NOT_INITIALIZED)
|
||||
}
|
||||
return this.secretsCache.get(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get method for workspace state keys - reads from in-memory cache
|
||||
*/
|
||||
getWorkspaceStateKey<T>(key: LocalStateKey): T | undefined {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error(CACHE_SERVICE_NOT_INITIALIZED)
|
||||
}
|
||||
return this.workspaceStateCache.get(key) as T | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Reinitialize the cache service by clearing all state and reloading from disk
|
||||
* Used for error recovery when write operations fail
|
||||
*/
|
||||
async reInitialize(): Promise<void> {
|
||||
// Clear all cached data and pending state
|
||||
this.dispose()
|
||||
|
||||
// Reinitialize from disk
|
||||
await this.initialize()
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose of the cache service
|
||||
*/
|
||||
private dispose(): void {
|
||||
if (this.persistenceTimeout) {
|
||||
clearTimeout(this.persistenceTimeout)
|
||||
this.persistenceTimeout = null
|
||||
}
|
||||
|
||||
this.pendingGlobalState.clear()
|
||||
this.pendingSecrets.clear()
|
||||
this.pendingWorkspaceState.clear()
|
||||
|
||||
this.globalStateCache.clear()
|
||||
this.secretsCache.clear()
|
||||
this.workspaceStateCache.clear()
|
||||
|
||||
this.isInitialized = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule debounced persistence - simple timeout-based persistence
|
||||
*/
|
||||
private scheduleDebouncedPersistence(): void {
|
||||
// Clear existing timeout if one is pending
|
||||
if (this.persistenceTimeout) {
|
||||
clearTimeout(this.persistenceTimeout)
|
||||
}
|
||||
|
||||
// Schedule a new timeout to persist pending changes
|
||||
this.persistenceTimeout = setTimeout(async () => {
|
||||
try {
|
||||
await Promise.all([
|
||||
this.persistGlobalStateBatch(this.pendingGlobalState),
|
||||
this.persistSecretsBatch(this.pendingSecrets),
|
||||
this.persistWorkspaceStateBatch(this.pendingWorkspaceState),
|
||||
])
|
||||
|
||||
// Clear pending sets on successful persistence
|
||||
this.pendingGlobalState.clear()
|
||||
this.pendingSecrets.clear()
|
||||
this.pendingWorkspaceState.clear()
|
||||
this.persistenceTimeout = null
|
||||
} catch (error) {
|
||||
console.error("Failed to persist pending changes:", error)
|
||||
this.persistenceTimeout = null
|
||||
|
||||
// Call persistence error callback for error recovery
|
||||
this.onPersistenceError?.({ error: error as Error })
|
||||
}
|
||||
}, this.PERSISTENCE_DELAY_MS)
|
||||
}
|
||||
|
||||
/**
|
||||
* Private method to batch persist global state keys with Promise.all
|
||||
*/
|
||||
private async persistGlobalStateBatch(keys: Set<GlobalStateKey>): Promise<void> {
|
||||
try {
|
||||
await Promise.all(
|
||||
Array.from(keys).map((key) => {
|
||||
const value = this.globalStateCache.get(key)
|
||||
return this.context.globalState.update(key, value)
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Failed to persist global state batch:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Private method to batch persist secrets with Promise.all
|
||||
*/
|
||||
private async persistSecretsBatch(keys: Set<SecretKey>): Promise<void> {
|
||||
try {
|
||||
await Promise.all(
|
||||
Array.from(keys).map((key) => {
|
||||
const value = this.secretsCache.get(key)
|
||||
if (value) {
|
||||
return this.context.secrets.store(key, value)
|
||||
} else {
|
||||
return this.context.secrets.delete(key)
|
||||
}
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Failed to persist secrets batch:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Private method to batch persist workspace state keys with Promise.all
|
||||
*/
|
||||
private async persistWorkspaceStateBatch(keys: Set<LocalStateKey>): Promise<void> {
|
||||
try {
|
||||
await Promise.all(
|
||||
Array.from(keys).map((key) => {
|
||||
const value = this.workspaceStateCache.get(key)
|
||||
return this.context.workspaceState.update(key, value)
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Failed to persist workspace state batch:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Private method to populate API configuration cache without triggering persistence
|
||||
* Used during initialization
|
||||
*/
|
||||
private populateApiConfigurationCache(apiConfiguration: ApiConfiguration): void {
|
||||
const {
|
||||
apiKey,
|
||||
openRouterApiKey,
|
||||
awsAccessKey,
|
||||
awsSecretKey,
|
||||
awsSessionToken,
|
||||
awsRegion,
|
||||
awsUseCrossRegionInference,
|
||||
awsBedrockUsePromptCache,
|
||||
awsBedrockEndpoint,
|
||||
awsBedrockApiKey,
|
||||
awsProfile,
|
||||
awsUseProfile,
|
||||
awsAuthentication,
|
||||
vertexProjectId,
|
||||
vertexRegion,
|
||||
openAiBaseUrl,
|
||||
openAiApiKey,
|
||||
openAiHeaders,
|
||||
ollamaBaseUrl,
|
||||
ollamaApiOptionsCtxNum,
|
||||
lmStudioBaseUrl,
|
||||
anthropicBaseUrl,
|
||||
geminiApiKey,
|
||||
geminiBaseUrl,
|
||||
openAiNativeApiKey,
|
||||
deepSeekApiKey,
|
||||
requestyApiKey,
|
||||
togetherApiKey,
|
||||
qwenApiKey,
|
||||
doubaoApiKey,
|
||||
mistralApiKey,
|
||||
azureApiVersion,
|
||||
openRouterProviderSorting,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmApiKey,
|
||||
liteLlmUsePromptCache,
|
||||
qwenApiLine,
|
||||
moonshotApiLine,
|
||||
asksageApiKey,
|
||||
asksageApiUrl,
|
||||
xaiApiKey,
|
||||
clineAccountId,
|
||||
sambanovaApiKey,
|
||||
cerebrasApiKey,
|
||||
groqApiKey,
|
||||
moonshotApiKey,
|
||||
nebiusApiKey,
|
||||
favoritedModelIds,
|
||||
fireworksApiKey,
|
||||
fireworksModelMaxCompletionTokens,
|
||||
fireworksModelMaxTokens,
|
||||
sapAiCoreClientId,
|
||||
sapAiCoreClientSecret,
|
||||
sapAiCoreBaseUrl,
|
||||
sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup,
|
||||
claudeCodePath,
|
||||
huggingFaceApiKey,
|
||||
requestTimeoutMs,
|
||||
// Plan mode configurations
|
||||
planModeApiProvider,
|
||||
planModeApiModelId,
|
||||
planModeThinkingBudgetTokens,
|
||||
planModeReasoningEffort,
|
||||
planModeVsCodeLmModelSelector,
|
||||
planModeAwsBedrockCustomSelected,
|
||||
planModeAwsBedrockCustomModelBaseId,
|
||||
planModeOpenRouterModelId,
|
||||
planModeOpenRouterModelInfo,
|
||||
planModeOpenAiModelId,
|
||||
planModeOpenAiModelInfo,
|
||||
planModeOllamaModelId,
|
||||
planModeLmStudioModelId,
|
||||
planModeLiteLlmModelId,
|
||||
planModeLiteLlmModelInfo,
|
||||
planModeRequestyModelId,
|
||||
planModeRequestyModelInfo,
|
||||
planModeTogetherModelId,
|
||||
planModeFireworksModelId,
|
||||
planModeSapAiCoreModelId,
|
||||
planModeGroqModelId,
|
||||
planModeGroqModelInfo,
|
||||
planModeHuggingFaceModelId,
|
||||
planModeHuggingFaceModelInfo,
|
||||
// Act mode configurations
|
||||
actModeApiProvider,
|
||||
actModeApiModelId,
|
||||
actModeThinkingBudgetTokens,
|
||||
actModeReasoningEffort,
|
||||
actModeVsCodeLmModelSelector,
|
||||
actModeAwsBedrockCustomSelected,
|
||||
actModeAwsBedrockCustomModelBaseId,
|
||||
actModeOpenRouterModelId,
|
||||
actModeOpenRouterModelInfo,
|
||||
actModeOpenAiModelId,
|
||||
actModeOpenAiModelInfo,
|
||||
actModeOllamaModelId,
|
||||
actModeLmStudioModelId,
|
||||
actModeLiteLlmModelId,
|
||||
actModeLiteLlmModelInfo,
|
||||
actModeRequestyModelId,
|
||||
actModeRequestyModelInfo,
|
||||
actModeTogetherModelId,
|
||||
actModeFireworksModelId,
|
||||
actModeSapAiCoreModelId,
|
||||
actModeGroqModelId,
|
||||
actModeGroqModelInfo,
|
||||
actModeHuggingFaceModelId,
|
||||
actModeHuggingFaceModelInfo,
|
||||
} = apiConfiguration
|
||||
|
||||
// Directly populate global state cache without triggering persistence
|
||||
const globalStateUpdates = {
|
||||
// Plan mode configuration updates
|
||||
planModeApiProvider,
|
||||
planModeApiModelId,
|
||||
planModeThinkingBudgetTokens,
|
||||
planModeReasoningEffort,
|
||||
planModeVsCodeLmModelSelector,
|
||||
planModeAwsBedrockCustomSelected,
|
||||
planModeAwsBedrockCustomModelBaseId,
|
||||
planModeOpenRouterModelId,
|
||||
planModeOpenRouterModelInfo,
|
||||
planModeOpenAiModelId,
|
||||
planModeOpenAiModelInfo,
|
||||
planModeOllamaModelId,
|
||||
planModeLmStudioModelId,
|
||||
planModeLiteLlmModelId,
|
||||
planModeLiteLlmModelInfo,
|
||||
planModeRequestyModelId,
|
||||
planModeRequestyModelInfo,
|
||||
planModeTogetherModelId,
|
||||
planModeFireworksModelId,
|
||||
planModeSapAiCoreModelId,
|
||||
planModeGroqModelId,
|
||||
planModeGroqModelInfo,
|
||||
planModeHuggingFaceModelId,
|
||||
planModeHuggingFaceModelInfo,
|
||||
|
||||
// Act mode configuration updates
|
||||
actModeApiProvider,
|
||||
actModeApiModelId,
|
||||
actModeThinkingBudgetTokens,
|
||||
actModeReasoningEffort,
|
||||
actModeVsCodeLmModelSelector,
|
||||
actModeAwsBedrockCustomSelected,
|
||||
actModeAwsBedrockCustomModelBaseId,
|
||||
actModeOpenRouterModelId,
|
||||
actModeOpenRouterModelInfo,
|
||||
actModeOpenAiModelId,
|
||||
actModeOpenAiModelInfo,
|
||||
actModeOllamaModelId,
|
||||
actModeLmStudioModelId,
|
||||
actModeLiteLlmModelId,
|
||||
actModeLiteLlmModelInfo,
|
||||
actModeRequestyModelId,
|
||||
actModeRequestyModelInfo,
|
||||
actModeTogetherModelId,
|
||||
actModeFireworksModelId,
|
||||
actModeSapAiCoreModelId,
|
||||
actModeGroqModelId,
|
||||
actModeGroqModelInfo,
|
||||
actModeHuggingFaceModelId,
|
||||
actModeHuggingFaceModelInfo,
|
||||
|
||||
// Global state updates
|
||||
awsRegion,
|
||||
awsUseCrossRegionInference,
|
||||
awsBedrockUsePromptCache,
|
||||
awsBedrockEndpoint,
|
||||
awsProfile,
|
||||
awsUseProfile,
|
||||
awsAuthentication,
|
||||
vertexProjectId,
|
||||
vertexRegion,
|
||||
openAiBaseUrl,
|
||||
openAiHeaders,
|
||||
ollamaBaseUrl,
|
||||
ollamaApiOptionsCtxNum,
|
||||
lmStudioBaseUrl,
|
||||
anthropicBaseUrl,
|
||||
geminiBaseUrl,
|
||||
azureApiVersion,
|
||||
openRouterProviderSorting,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmUsePromptCache,
|
||||
qwenApiLine,
|
||||
moonshotApiLine,
|
||||
asksageApiUrl,
|
||||
favoritedModelIds,
|
||||
requestTimeoutMs,
|
||||
fireworksModelMaxCompletionTokens,
|
||||
fireworksModelMaxTokens,
|
||||
sapAiCoreBaseUrl,
|
||||
sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup,
|
||||
claudeCodePath,
|
||||
}
|
||||
|
||||
// Populate global state cache directly
|
||||
Object.entries(globalStateUpdates).forEach(([key, value]) => {
|
||||
this.globalStateCache.set(key as GlobalStateKey, value)
|
||||
})
|
||||
|
||||
// Directly populate secrets cache without triggering persistence
|
||||
const secretsUpdates = {
|
||||
apiKey,
|
||||
openRouterApiKey,
|
||||
clineAccountId,
|
||||
awsAccessKey,
|
||||
awsSecretKey,
|
||||
awsSessionToken,
|
||||
awsBedrockApiKey,
|
||||
openAiApiKey,
|
||||
geminiApiKey,
|
||||
openAiNativeApiKey,
|
||||
deepSeekApiKey,
|
||||
requestyApiKey,
|
||||
togetherApiKey,
|
||||
qwenApiKey,
|
||||
doubaoApiKey,
|
||||
mistralApiKey,
|
||||
liteLlmApiKey,
|
||||
fireworksApiKey,
|
||||
asksageApiKey,
|
||||
xaiApiKey,
|
||||
sambanovaApiKey,
|
||||
cerebrasApiKey,
|
||||
groqApiKey,
|
||||
moonshotApiKey,
|
||||
nebiusApiKey,
|
||||
sapAiCoreClientId,
|
||||
sapAiCoreClientSecret,
|
||||
huggingFaceApiKey,
|
||||
}
|
||||
|
||||
// Populate secrets cache directly
|
||||
Object.entries(secretsUpdates).forEach(([key, value]) => {
|
||||
this.secretsCache.set(key as SecretKey, value)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct API configuration from cached component keys
|
||||
*/
|
||||
private constructApiConfigurationFromCache(): ApiConfiguration {
|
||||
return {
|
||||
// Secrets
|
||||
apiKey: this.secretsCache.get("apiKey"),
|
||||
openRouterApiKey: this.secretsCache.get("openRouterApiKey"),
|
||||
clineAccountId: this.secretsCache.get("clineAccountId"),
|
||||
awsAccessKey: this.secretsCache.get("awsAccessKey"),
|
||||
awsSecretKey: this.secretsCache.get("awsSecretKey"),
|
||||
awsSessionToken: this.secretsCache.get("awsSessionToken"),
|
||||
awsBedrockApiKey: this.secretsCache.get("awsBedrockApiKey"),
|
||||
openAiApiKey: this.secretsCache.get("openAiApiKey"),
|
||||
geminiApiKey: this.secretsCache.get("geminiApiKey"),
|
||||
openAiNativeApiKey: this.secretsCache.get("openAiNativeApiKey"),
|
||||
deepSeekApiKey: this.secretsCache.get("deepSeekApiKey"),
|
||||
requestyApiKey: this.secretsCache.get("requestyApiKey"),
|
||||
togetherApiKey: this.secretsCache.get("togetherApiKey"),
|
||||
qwenApiKey: this.secretsCache.get("qwenApiKey"),
|
||||
doubaoApiKey: this.secretsCache.get("doubaoApiKey"),
|
||||
mistralApiKey: this.secretsCache.get("mistralApiKey"),
|
||||
liteLlmApiKey: this.secretsCache.get("liteLlmApiKey"),
|
||||
fireworksApiKey: this.secretsCache.get("fireworksApiKey"),
|
||||
asksageApiKey: this.secretsCache.get("asksageApiKey"),
|
||||
xaiApiKey: this.secretsCache.get("xaiApiKey"),
|
||||
sambanovaApiKey: this.secretsCache.get("sambanovaApiKey"),
|
||||
cerebrasApiKey: this.secretsCache.get("cerebrasApiKey"),
|
||||
groqApiKey: this.secretsCache.get("groqApiKey"),
|
||||
moonshotApiKey: this.secretsCache.get("moonshotApiKey"),
|
||||
nebiusApiKey: this.secretsCache.get("nebiusApiKey"),
|
||||
sapAiCoreClientId: this.secretsCache.get("sapAiCoreClientId"),
|
||||
sapAiCoreClientSecret: this.secretsCache.get("sapAiCoreClientSecret"),
|
||||
huggingFaceApiKey: this.secretsCache.get("huggingFaceApiKey"),
|
||||
|
||||
// Global state
|
||||
awsRegion: this.globalStateCache.get("awsRegion"),
|
||||
awsUseCrossRegionInference: this.globalStateCache.get("awsUseCrossRegionInference"),
|
||||
awsBedrockUsePromptCache: this.globalStateCache.get("awsBedrockUsePromptCache"),
|
||||
awsBedrockEndpoint: this.globalStateCache.get("awsBedrockEndpoint"),
|
||||
awsProfile: this.globalStateCache.get("awsProfile"),
|
||||
awsUseProfile: this.globalStateCache.get("awsUseProfile"),
|
||||
awsAuthentication: this.globalStateCache.get("awsAuthentication"),
|
||||
vertexProjectId: this.globalStateCache.get("vertexProjectId"),
|
||||
vertexRegion: this.globalStateCache.get("vertexRegion"),
|
||||
openAiBaseUrl: this.globalStateCache.get("openAiBaseUrl"),
|
||||
openAiHeaders: this.globalStateCache.get("openAiHeaders") || {},
|
||||
ollamaBaseUrl: this.globalStateCache.get("ollamaBaseUrl"),
|
||||
ollamaApiOptionsCtxNum: this.globalStateCache.get("ollamaApiOptionsCtxNum"),
|
||||
lmStudioBaseUrl: this.globalStateCache.get("lmStudioBaseUrl"),
|
||||
anthropicBaseUrl: this.globalStateCache.get("anthropicBaseUrl"),
|
||||
geminiBaseUrl: this.globalStateCache.get("geminiBaseUrl"),
|
||||
azureApiVersion: this.globalStateCache.get("azureApiVersion"),
|
||||
openRouterProviderSorting: this.globalStateCache.get("openRouterProviderSorting"),
|
||||
liteLlmBaseUrl: this.globalStateCache.get("liteLlmBaseUrl"),
|
||||
liteLlmUsePromptCache: this.globalStateCache.get("liteLlmUsePromptCache"),
|
||||
qwenApiLine: this.globalStateCache.get("qwenApiLine"),
|
||||
moonshotApiLine: this.globalStateCache.get("moonshotApiLine"),
|
||||
asksageApiUrl: this.globalStateCache.get("asksageApiUrl"),
|
||||
favoritedModelIds: this.globalStateCache.get("favoritedModelIds"),
|
||||
requestTimeoutMs: this.globalStateCache.get("requestTimeoutMs"),
|
||||
fireworksModelMaxCompletionTokens: this.globalStateCache.get("fireworksModelMaxCompletionTokens"),
|
||||
fireworksModelMaxTokens: this.globalStateCache.get("fireworksModelMaxTokens"),
|
||||
sapAiCoreBaseUrl: this.globalStateCache.get("sapAiCoreBaseUrl"),
|
||||
sapAiCoreTokenUrl: this.globalStateCache.get("sapAiCoreTokenUrl"),
|
||||
sapAiResourceGroup: this.globalStateCache.get("sapAiResourceGroup"),
|
||||
claudeCodePath: this.globalStateCache.get("claudeCodePath"),
|
||||
|
||||
// Plan mode configurations
|
||||
planModeApiProvider: this.globalStateCache.get("planModeApiProvider"),
|
||||
planModeApiModelId: this.globalStateCache.get("planModeApiModelId"),
|
||||
planModeThinkingBudgetTokens: this.globalStateCache.get("planModeThinkingBudgetTokens"),
|
||||
planModeReasoningEffort: this.globalStateCache.get("planModeReasoningEffort"),
|
||||
planModeVsCodeLmModelSelector: this.globalStateCache.get("planModeVsCodeLmModelSelector"),
|
||||
planModeAwsBedrockCustomSelected: this.globalStateCache.get("planModeAwsBedrockCustomSelected"),
|
||||
planModeAwsBedrockCustomModelBaseId: this.globalStateCache.get("planModeAwsBedrockCustomModelBaseId"),
|
||||
planModeOpenRouterModelId: this.globalStateCache.get("planModeOpenRouterModelId"),
|
||||
planModeOpenRouterModelInfo: this.globalStateCache.get("planModeOpenRouterModelInfo"),
|
||||
planModeOpenAiModelId: this.globalStateCache.get("planModeOpenAiModelId"),
|
||||
planModeOpenAiModelInfo: this.globalStateCache.get("planModeOpenAiModelInfo"),
|
||||
planModeOllamaModelId: this.globalStateCache.get("planModeOllamaModelId"),
|
||||
planModeLmStudioModelId: this.globalStateCache.get("planModeLmStudioModelId"),
|
||||
planModeLiteLlmModelId: this.globalStateCache.get("planModeLiteLlmModelId"),
|
||||
planModeLiteLlmModelInfo: this.globalStateCache.get("planModeLiteLlmModelInfo"),
|
||||
planModeRequestyModelId: this.globalStateCache.get("planModeRequestyModelId"),
|
||||
planModeRequestyModelInfo: this.globalStateCache.get("planModeRequestyModelInfo"),
|
||||
planModeTogetherModelId: this.globalStateCache.get("planModeTogetherModelId"),
|
||||
planModeFireworksModelId: this.globalStateCache.get("planModeFireworksModelId"),
|
||||
planModeSapAiCoreModelId: this.globalStateCache.get("planModeSapAiCoreModelId"),
|
||||
planModeGroqModelId: this.globalStateCache.get("planModeGroqModelId"),
|
||||
planModeGroqModelInfo: this.globalStateCache.get("planModeGroqModelInfo"),
|
||||
planModeHuggingFaceModelId: this.globalStateCache.get("planModeHuggingFaceModelId"),
|
||||
planModeHuggingFaceModelInfo: this.globalStateCache.get("planModeHuggingFaceModelInfo"),
|
||||
|
||||
// Act mode configurations
|
||||
actModeApiProvider: this.globalStateCache.get("actModeApiProvider"),
|
||||
actModeApiModelId: this.globalStateCache.get("actModeApiModelId"),
|
||||
actModeThinkingBudgetTokens: this.globalStateCache.get("actModeThinkingBudgetTokens"),
|
||||
actModeReasoningEffort: this.globalStateCache.get("actModeReasoningEffort"),
|
||||
actModeVsCodeLmModelSelector: this.globalStateCache.get("actModeVsCodeLmModelSelector"),
|
||||
actModeAwsBedrockCustomSelected: this.globalStateCache.get("actModeAwsBedrockCustomSelected"),
|
||||
actModeAwsBedrockCustomModelBaseId: this.globalStateCache.get("actModeAwsBedrockCustomModelBaseId"),
|
||||
actModeOpenRouterModelId: this.globalStateCache.get("actModeOpenRouterModelId"),
|
||||
actModeOpenRouterModelInfo: this.globalStateCache.get("actModeOpenRouterModelInfo"),
|
||||
actModeOpenAiModelId: this.globalStateCache.get("actModeOpenAiModelId"),
|
||||
actModeOpenAiModelInfo: this.globalStateCache.get("actModeOpenAiModelInfo"),
|
||||
actModeOllamaModelId: this.globalStateCache.get("actModeOllamaModelId"),
|
||||
actModeLmStudioModelId: this.globalStateCache.get("actModeLmStudioModelId"),
|
||||
actModeLiteLlmModelId: this.globalStateCache.get("actModeLiteLlmModelId"),
|
||||
actModeLiteLlmModelInfo: this.globalStateCache.get("actModeLiteLlmModelInfo"),
|
||||
actModeRequestyModelId: this.globalStateCache.get("actModeRequestyModelId"),
|
||||
actModeRequestyModelInfo: this.globalStateCache.get("actModeRequestyModelInfo"),
|
||||
actModeTogetherModelId: this.globalStateCache.get("actModeTogetherModelId"),
|
||||
actModeFireworksModelId: this.globalStateCache.get("actModeFireworksModelId"),
|
||||
actModeSapAiCoreModelId: this.globalStateCache.get("actModeSapAiCoreModelId"),
|
||||
actModeGroqModelId: this.globalStateCache.get("actModeGroqModelId"),
|
||||
actModeGroqModelInfo: this.globalStateCache.get("actModeGroqModelInfo"),
|
||||
actModeHuggingFaceModelId: this.globalStateCache.get("actModeHuggingFaceModelId"),
|
||||
actModeHuggingFaceModelInfo: this.globalStateCache.get("actModeHuggingFaceModelInfo"),
|
||||
} as ApiConfiguration
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const CACHE_SERVICE_NOT_INITIALIZED = "CacheService must be initialized before attempting to access state."
|
||||
@@ -28,6 +28,7 @@ export type SecretKey =
|
||||
| "sapAiCoreClientId"
|
||||
| "sapAiCoreClientSecret"
|
||||
| "groqApiKey"
|
||||
| "huaweiCloudMaasApiKey"
|
||||
|
||||
export type GlobalStateKey =
|
||||
| "awsRegion"
|
||||
@@ -82,8 +83,10 @@ export type GlobalStateKey =
|
||||
| "sapAiCoreBaseUrl"
|
||||
| "sapAiResourceGroup"
|
||||
| "claudeCodePath"
|
||||
| "strictPlanModeEnabled"
|
||||
// Settings around plan/act and ephemeral model configuration
|
||||
| "chatSettings"
|
||||
| "preferredLanguage"
|
||||
| "openaiReasoningEffort"
|
||||
| "mode"
|
||||
// Plan mode configurations
|
||||
| "planModeApiProvider"
|
||||
@@ -110,6 +113,8 @@ export type GlobalStateKey =
|
||||
| "planModeGroqModelInfo"
|
||||
| "planModeHuggingFaceModelId"
|
||||
| "planModeHuggingFaceModelInfo"
|
||||
| "planModeHuaweiCloudMaasModelId"
|
||||
| "planModeHuaweiCloudMaasModelInfo"
|
||||
// Act mode configurations
|
||||
| "actModeApiProvider"
|
||||
| "actModeApiModelId"
|
||||
@@ -135,5 +140,7 @@ export type GlobalStateKey =
|
||||
| "actModeGroqModelInfo"
|
||||
| "actModeHuggingFaceModelId"
|
||||
| "actModeHuggingFaceModelInfo"
|
||||
| "actModeHuaweiCloudMaasModelId"
|
||||
| "actModeHuaweiCloudMaasModelInfo"
|
||||
|
||||
export type LocalStateKey = "localClineRulesToggles" | "localCursorRulesToggles" | "localWindsurfRulesToggles" | "workflowToggles"
|
||||
|
||||
@@ -3,7 +3,6 @@ import { ensureRulesDirectoryExists } from "./disk"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { updateGlobalState, getAllExtensionState, getGlobalState } from "./state"
|
||||
import { GlobalStateKey } from "./state-keys"
|
||||
|
||||
export async function migrateWorkspaceToGlobalStorage(context: vscode.ExtensionContext) {
|
||||
// Keys to migrate from workspace storage back to global storage
|
||||
@@ -13,7 +12,6 @@ export async function migrateWorkspaceToGlobalStorage(context: vscode.ExtensionC
|
||||
"apiModelId",
|
||||
"thinkingBudgetTokens",
|
||||
"reasoningEffort",
|
||||
"chatSettings",
|
||||
"vsCodeLmModelSelector",
|
||||
|
||||
// Provider-specific model keys
|
||||
@@ -136,43 +134,6 @@ export async function migrateCustomInstructionsToGlobalRules(context: vscode.Ext
|
||||
}
|
||||
}
|
||||
|
||||
export async function migrateModeFromWorkspaceStorageToControllerState(context: vscode.ExtensionContext) {
|
||||
try {
|
||||
// Check legacy workspace storage (use raw methods since chatSettings is now global)
|
||||
const workspaceChatSettings = (await context.workspaceState.get("chatSettings")) as any
|
||||
|
||||
if (workspaceChatSettings && typeof workspaceChatSettings === "object" && "mode" in workspaceChatSettings) {
|
||||
console.log("Cleaning up mode from legacy workspace storage...")
|
||||
|
||||
// Remove mode property from chatSettings
|
||||
const { mode, ...cleanedChatSettings } = workspaceChatSettings
|
||||
|
||||
// Save cleaned chatSettings back to workspace storage (will be migrated later)
|
||||
await context.workspaceState.update("chatSettings", cleanedChatSettings)
|
||||
|
||||
console.log("Successfully removed mode from legacy workspace storage chatSettings")
|
||||
}
|
||||
|
||||
// Also check global storage for any mode cleanup needed
|
||||
const globalChatSettings = (await context.globalState.get("chatSettings")) as any
|
||||
|
||||
if (globalChatSettings && typeof globalChatSettings === "object" && "mode" in globalChatSettings) {
|
||||
console.log("Cleaning up mode from global storage...")
|
||||
|
||||
// Remove mode property from chatSettings
|
||||
const { mode, ...cleanedChatSettings } = globalChatSettings
|
||||
|
||||
// Save cleaned chatSettings back to global storage
|
||||
await updateGlobalState(context, "chatSettings", cleanedChatSettings)
|
||||
|
||||
console.log("Successfully removed mode from global storage chatSettings")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to cleanup mode from storage:", error)
|
||||
// Continue execution - migration failure shouldn't break extension startup
|
||||
}
|
||||
}
|
||||
|
||||
export async function migrateLegacyApiConfigurationToModeSpecific(context: vscode.ExtensionContext) {
|
||||
try {
|
||||
// Check if migration is needed - if planModeApiProvider already exists, skip migration
|
||||
|
||||
+49
-261
@@ -1,5 +1,5 @@
|
||||
import * as vscode from "vscode"
|
||||
import { DEFAULT_CHAT_SETTINGS, Mode } from "@shared/ChatSettings"
|
||||
import { Mode, OpenaiReasoningEffort } from "@shared/storage/types"
|
||||
import { DEFAULT_BROWSER_SETTINGS } from "@shared/BrowserSettings"
|
||||
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "@shared/AutoApprovalSettings"
|
||||
import { GlobalStateKey, LocalStateKey, SecretKey } from "./state-keys"
|
||||
@@ -7,12 +7,12 @@ import { ApiConfiguration, ApiProvider, BedrockModelId, ModelInfo } from "@share
|
||||
import { HistoryItem } from "@shared/HistoryItem"
|
||||
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
|
||||
import { BrowserSettings } from "@shared/BrowserSettings"
|
||||
import { StoredChatSettings } from "@shared/ChatSettings"
|
||||
import { TelemetrySetting } from "@shared/TelemetrySetting"
|
||||
import { UserInfo } from "@shared/UserInfo"
|
||||
import { ClineRulesToggles } from "@shared/cline-rules"
|
||||
import { DEFAULT_MCP_DISPLAY_MODE, McpDisplayMode } from "@shared/McpDisplayMode"
|
||||
import { migrateEnableCheckpointsSetting, migrateMcpMarketplaceEnableSetting } from "./state-migrations"
|
||||
import { Controller } from "../controller"
|
||||
/*
|
||||
Storage
|
||||
https://dev.to/kompotkot/how-to-use-secretstorage-in-your-vscode-extensions-2hco
|
||||
@@ -110,7 +110,6 @@ export async function getWorkspaceState(context: vscode.ExtensionContext, key: L
|
||||
}
|
||||
|
||||
export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
const firstBatchStart = performance.now()
|
||||
const [
|
||||
isNewUser,
|
||||
welcomeViewCompleted,
|
||||
@@ -190,6 +189,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup,
|
||||
claudeCodePath,
|
||||
huaweiCloudMaasApiKey,
|
||||
] = await Promise.all([
|
||||
getGlobalState(context, "isNewUser") as Promise<boolean | undefined>,
|
||||
getGlobalState(context, "welcomeViewCompleted") as Promise<boolean | undefined>,
|
||||
@@ -269,14 +269,21 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
getGlobalState(context, "sapAiCoreTokenUrl") as Promise<string | undefined>,
|
||||
getGlobalState(context, "sapAiResourceGroup") as Promise<string | undefined>,
|
||||
getGlobalState(context, "claudeCodePath") as Promise<string | undefined>,
|
||||
getSecret(context, "huaweiCloudMaasApiKey") as Promise<string | undefined>,
|
||||
])
|
||||
|
||||
const localClineRulesToggles = (await getWorkspaceState(context, "localClineRulesToggles")) as ClineRulesToggles
|
||||
const [localClineRulesToggles, localWindsurfRulesToggles, localCursorRulesToggles, localWorkflowToggles] = await Promise.all([
|
||||
getWorkspaceState(context, "localClineRulesToggles") as Promise<ClineRulesToggles | undefined>,
|
||||
getWorkspaceState(context, "localWindsurfRulesToggles") as Promise<ClineRulesToggles | undefined>,
|
||||
getWorkspaceState(context, "localCursorRulesToggles") as Promise<ClineRulesToggles | undefined>,
|
||||
getWorkspaceState(context, "workflowToggles") as Promise<ClineRulesToggles | undefined>,
|
||||
])
|
||||
|
||||
const secondBatchStart = performance.now()
|
||||
const [
|
||||
chatSettings,
|
||||
currentMode,
|
||||
preferredLanguage,
|
||||
openaiReasoningEffort,
|
||||
mode,
|
||||
strictPlanModeEnabled,
|
||||
// Plan mode configurations
|
||||
planModeApiProvider,
|
||||
planModeApiModelId,
|
||||
@@ -302,6 +309,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
planModeGroqModelInfo,
|
||||
planModeHuggingFaceModelId,
|
||||
planModeHuggingFaceModelInfo,
|
||||
planModeHuaweiCloudMaasModelId,
|
||||
planModeHuaweiCloudMaasModelInfo,
|
||||
// Act mode configurations
|
||||
actModeApiProvider,
|
||||
actModeApiModelId,
|
||||
@@ -327,9 +336,13 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
actModeGroqModelInfo,
|
||||
actModeHuggingFaceModelId,
|
||||
actModeHuggingFaceModelInfo,
|
||||
actModeHuaweiCloudMaasModelId,
|
||||
actModeHuaweiCloudMaasModelInfo,
|
||||
] = await Promise.all([
|
||||
getGlobalState(context, "chatSettings") as Promise<StoredChatSettings | undefined>,
|
||||
getGlobalState(context, "preferredLanguage") as Promise<string | undefined>,
|
||||
getGlobalState(context, "openaiReasoningEffort") as Promise<OpenaiReasoningEffort | undefined>,
|
||||
getGlobalState(context, "mode") as Promise<Mode | undefined>,
|
||||
getGlobalState(context, "strictPlanModeEnabled") as Promise<boolean | undefined>,
|
||||
// Plan mode configurations
|
||||
getGlobalState(context, "planModeApiProvider") as Promise<ApiProvider | undefined>,
|
||||
getGlobalState(context, "planModeApiModelId") as Promise<string | undefined>,
|
||||
@@ -355,6 +368,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
getGlobalState(context, "planModeGroqModelInfo") as Promise<ModelInfo | undefined>,
|
||||
getGlobalState(context, "planModeHuggingFaceModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "planModeHuggingFaceModelInfo") as Promise<ModelInfo | undefined>,
|
||||
getGlobalState(context, "planModeHuaweiCloudMaasModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "planModeHuaweiCloudMaasModelInfo") as Promise<ModelInfo | undefined>,
|
||||
// Act mode configurations
|
||||
getGlobalState(context, "actModeApiProvider") as Promise<ApiProvider | undefined>,
|
||||
getGlobalState(context, "actModeApiModelId") as Promise<string | undefined>,
|
||||
@@ -380,9 +395,10 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
getGlobalState(context, "actModeGroqModelInfo") as Promise<ModelInfo | undefined>,
|
||||
getGlobalState(context, "actModeHuggingFaceModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "actModeHuggingFaceModelInfo") as Promise<ModelInfo | undefined>,
|
||||
getGlobalState(context, "actModeHuaweiCloudMaasModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "actModeHuaweiCloudMaasModelInfo") as Promise<ModelInfo | undefined>,
|
||||
])
|
||||
|
||||
const processingStart = performance.now()
|
||||
let apiProvider: ApiProvider
|
||||
if (planModeApiProvider) {
|
||||
apiProvider = planModeApiProvider
|
||||
@@ -480,6 +496,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup,
|
||||
huggingFaceApiKey,
|
||||
huaweiCloudMaasApiKey,
|
||||
// Plan mode configurations
|
||||
planModeApiProvider: planModeApiProvider || apiProvider,
|
||||
planModeApiModelId,
|
||||
@@ -505,6 +522,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
planModeGroqModelInfo,
|
||||
planModeHuggingFaceModelId,
|
||||
planModeHuggingFaceModelInfo,
|
||||
planModeHuaweiCloudMaasModelId,
|
||||
planModeHuaweiCloudMaasModelInfo,
|
||||
// Act mode configurations
|
||||
actModeApiProvider: actModeApiProvider || apiProvider,
|
||||
actModeApiModelId,
|
||||
@@ -530,6 +549,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
actModeGroqModelInfo,
|
||||
actModeHuggingFaceModelId,
|
||||
actModeHuggingFaceModelInfo,
|
||||
actModeHuaweiCloudMaasModelId,
|
||||
actModeHuaweiCloudMaasModelInfo,
|
||||
},
|
||||
isNewUser: isNewUser ?? true,
|
||||
welcomeViewCompleted,
|
||||
@@ -537,13 +558,11 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
taskHistory,
|
||||
autoApprovalSettings: autoApprovalSettings || DEFAULT_AUTO_APPROVAL_SETTINGS, // default value can be 0 or empty string
|
||||
globalClineRulesToggles: globalClineRulesToggles || {},
|
||||
localClineRulesToggles: localClineRulesToggles || {},
|
||||
browserSettings: { ...DEFAULT_BROWSER_SETTINGS, ...browserSettings }, // this will ensure that older versions of browserSettings (e.g. before remoteBrowserEnabled was added) are merged with the default values (false for remoteBrowserEnabled)
|
||||
chatSettings: {
|
||||
...DEFAULT_CHAT_SETTINGS, // Apply defaults first
|
||||
...(chatSettings || {}), // Spread fetched global chatSettings, which includes preferredLanguage, and openAIReasoningEffort
|
||||
mode: currentMode || "act", // Merge mode from global state
|
||||
},
|
||||
preferredLanguage: preferredLanguage || "English",
|
||||
openaiReasoningEffort: (openaiReasoningEffort as OpenaiReasoningEffort) || "medium",
|
||||
mode: mode || "act",
|
||||
strictPlanModeEnabled: strictPlanModeEnabled ?? false,
|
||||
userInfo,
|
||||
mcpMarketplaceEnabled: mcpMarketplaceEnabled,
|
||||
mcpDisplayMode: mcpDisplayMode ?? DEFAULT_MCP_DISPLAY_MODE,
|
||||
@@ -556,256 +575,25 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
terminalOutputLineLimit: terminalOutputLineLimit ?? 500,
|
||||
defaultTerminalProfile: defaultTerminalProfile ?? "default",
|
||||
globalWorkflowToggles: globalWorkflowToggles || {},
|
||||
localClineRulesToggles: localClineRulesToggles || {},
|
||||
localWindsurfRulesToggles: localWindsurfRulesToggles || {},
|
||||
localCursorRulesToggles: localCursorRulesToggles || {},
|
||||
localWorkflowToggles: localWorkflowToggles || {},
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateApiConfiguration(context: vscode.ExtensionContext, apiConfiguration: ApiConfiguration) {
|
||||
const {
|
||||
apiKey,
|
||||
openRouterApiKey,
|
||||
awsAccessKey,
|
||||
awsSecretKey,
|
||||
awsSessionToken,
|
||||
awsRegion,
|
||||
awsUseCrossRegionInference,
|
||||
awsBedrockUsePromptCache,
|
||||
awsBedrockEndpoint,
|
||||
awsBedrockApiKey,
|
||||
awsProfile,
|
||||
awsUseProfile,
|
||||
awsAuthentication,
|
||||
vertexProjectId,
|
||||
vertexRegion,
|
||||
openAiBaseUrl,
|
||||
openAiApiKey,
|
||||
openAiHeaders,
|
||||
ollamaBaseUrl,
|
||||
ollamaApiOptionsCtxNum,
|
||||
lmStudioBaseUrl,
|
||||
anthropicBaseUrl,
|
||||
geminiApiKey,
|
||||
geminiBaseUrl,
|
||||
openAiNativeApiKey,
|
||||
deepSeekApiKey,
|
||||
requestyApiKey,
|
||||
togetherApiKey,
|
||||
qwenApiKey,
|
||||
doubaoApiKey,
|
||||
mistralApiKey,
|
||||
azureApiVersion,
|
||||
openRouterProviderSorting,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmApiKey,
|
||||
liteLlmUsePromptCache,
|
||||
qwenApiLine,
|
||||
moonshotApiLine,
|
||||
asksageApiKey,
|
||||
asksageApiUrl,
|
||||
xaiApiKey,
|
||||
clineAccountId,
|
||||
sambanovaApiKey,
|
||||
cerebrasApiKey,
|
||||
groqApiKey,
|
||||
moonshotApiKey,
|
||||
nebiusApiKey,
|
||||
favoritedModelIds,
|
||||
fireworksApiKey,
|
||||
fireworksModelMaxCompletionTokens,
|
||||
fireworksModelMaxTokens,
|
||||
sapAiCoreClientId,
|
||||
sapAiCoreClientSecret,
|
||||
sapAiCoreBaseUrl,
|
||||
sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup,
|
||||
claudeCodePath,
|
||||
huggingFaceApiKey,
|
||||
// Plan mode configurations
|
||||
planModeApiProvider,
|
||||
planModeApiModelId,
|
||||
planModeThinkingBudgetTokens,
|
||||
planModeReasoningEffort,
|
||||
planModeVsCodeLmModelSelector,
|
||||
planModeAwsBedrockCustomSelected,
|
||||
planModeAwsBedrockCustomModelBaseId,
|
||||
planModeOpenRouterModelId,
|
||||
planModeOpenRouterModelInfo,
|
||||
planModeOpenAiModelId,
|
||||
planModeOpenAiModelInfo,
|
||||
planModeOllamaModelId,
|
||||
planModeLmStudioModelId,
|
||||
planModeLiteLlmModelId,
|
||||
planModeLiteLlmModelInfo,
|
||||
planModeRequestyModelId,
|
||||
planModeRequestyModelInfo,
|
||||
planModeTogetherModelId,
|
||||
planModeFireworksModelId,
|
||||
planModeSapAiCoreModelId,
|
||||
planModeGroqModelId,
|
||||
planModeGroqModelInfo,
|
||||
planModeHuggingFaceModelId,
|
||||
planModeHuggingFaceModelInfo,
|
||||
// Act mode configurations
|
||||
actModeApiProvider,
|
||||
actModeApiModelId,
|
||||
actModeThinkingBudgetTokens,
|
||||
actModeReasoningEffort,
|
||||
actModeVsCodeLmModelSelector,
|
||||
actModeAwsBedrockCustomSelected,
|
||||
actModeAwsBedrockCustomModelBaseId,
|
||||
actModeOpenRouterModelId,
|
||||
actModeOpenRouterModelInfo,
|
||||
actModeOpenAiModelId,
|
||||
actModeOpenAiModelInfo,
|
||||
actModeOllamaModelId,
|
||||
actModeLmStudioModelId,
|
||||
actModeLiteLlmModelId,
|
||||
actModeLiteLlmModelInfo,
|
||||
actModeRequestyModelId,
|
||||
actModeRequestyModelInfo,
|
||||
actModeTogetherModelId,
|
||||
actModeFireworksModelId,
|
||||
actModeSapAiCoreModelId,
|
||||
actModeGroqModelId,
|
||||
actModeGroqModelInfo,
|
||||
actModeHuggingFaceModelId,
|
||||
actModeHuggingFaceModelInfo,
|
||||
} = apiConfiguration
|
||||
export async function resetWorkspaceState(controller: Controller) {
|
||||
const context = controller.context
|
||||
await Promise.all(context.workspaceState.keys().map((key) => controller.context.workspaceState.update(key, undefined)))
|
||||
|
||||
// OPTIMIZED: Batch all global state updates into 2 operations instead of 47
|
||||
const batchedGlobalUpdates = {
|
||||
// Plan mode configuration updates
|
||||
planModeApiProvider,
|
||||
planModeApiModelId,
|
||||
planModeThinkingBudgetTokens,
|
||||
planModeReasoningEffort,
|
||||
planModeVsCodeLmModelSelector,
|
||||
planModeAwsBedrockCustomSelected,
|
||||
planModeAwsBedrockCustomModelBaseId,
|
||||
planModeOpenRouterModelId,
|
||||
planModeOpenRouterModelInfo,
|
||||
planModeOpenAiModelId,
|
||||
planModeOpenAiModelInfo,
|
||||
planModeOllamaModelId,
|
||||
planModeLmStudioModelId,
|
||||
planModeLiteLlmModelId,
|
||||
planModeLiteLlmModelInfo,
|
||||
planModeRequestyModelId,
|
||||
planModeRequestyModelInfo,
|
||||
planModeTogetherModelId,
|
||||
planModeFireworksModelId,
|
||||
planModeSapAiCoreModelId,
|
||||
planModeGroqModelId,
|
||||
planModeGroqModelInfo,
|
||||
planModeHuggingFaceModelId,
|
||||
planModeHuggingFaceModelInfo,
|
||||
|
||||
// Act mode configuration updates
|
||||
actModeApiProvider,
|
||||
actModeApiModelId,
|
||||
actModeThinkingBudgetTokens,
|
||||
actModeReasoningEffort,
|
||||
actModeVsCodeLmModelSelector,
|
||||
actModeAwsBedrockCustomSelected,
|
||||
actModeAwsBedrockCustomModelBaseId,
|
||||
actModeOpenRouterModelId,
|
||||
actModeOpenRouterModelInfo,
|
||||
actModeOpenAiModelId,
|
||||
actModeOpenAiModelInfo,
|
||||
actModeOllamaModelId,
|
||||
actModeLmStudioModelId,
|
||||
actModeLiteLlmModelId,
|
||||
actModeLiteLlmModelInfo,
|
||||
actModeRequestyModelId,
|
||||
actModeRequestyModelInfo,
|
||||
actModeTogetherModelId,
|
||||
actModeFireworksModelId,
|
||||
actModeSapAiCoreModelId,
|
||||
actModeGroqModelId,
|
||||
actModeGroqModelInfo,
|
||||
actModeHuggingFaceModelId,
|
||||
actModeHuggingFaceModelInfo,
|
||||
|
||||
// Global state updates (27 keys)
|
||||
awsRegion,
|
||||
awsUseCrossRegionInference,
|
||||
awsBedrockUsePromptCache,
|
||||
awsBedrockEndpoint,
|
||||
awsProfile,
|
||||
awsUseProfile,
|
||||
awsAuthentication,
|
||||
vertexProjectId,
|
||||
vertexRegion,
|
||||
openAiBaseUrl,
|
||||
openAiHeaders: openAiHeaders || {},
|
||||
ollamaBaseUrl,
|
||||
ollamaApiOptionsCtxNum,
|
||||
lmStudioBaseUrl,
|
||||
anthropicBaseUrl,
|
||||
geminiBaseUrl,
|
||||
azureApiVersion,
|
||||
openRouterProviderSorting,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmUsePromptCache,
|
||||
qwenApiLine,
|
||||
moonshotApiLine,
|
||||
asksageApiUrl,
|
||||
favoritedModelIds,
|
||||
requestTimeoutMs: apiConfiguration.requestTimeoutMs,
|
||||
fireworksModelMaxCompletionTokens,
|
||||
fireworksModelMaxTokens,
|
||||
sapAiCoreBaseUrl,
|
||||
sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup,
|
||||
claudeCodePath,
|
||||
}
|
||||
|
||||
// OPTIMIZED: Batch all secret updates into 1 operation instead of 23
|
||||
const batchedSecretUpdates = {
|
||||
apiKey,
|
||||
openRouterApiKey,
|
||||
clineAccountId,
|
||||
awsAccessKey,
|
||||
awsSecretKey,
|
||||
awsSessionToken,
|
||||
awsBedrockApiKey,
|
||||
openAiApiKey,
|
||||
geminiApiKey,
|
||||
openAiNativeApiKey,
|
||||
deepSeekApiKey,
|
||||
requestyApiKey,
|
||||
togetherApiKey,
|
||||
qwenApiKey,
|
||||
doubaoApiKey,
|
||||
mistralApiKey,
|
||||
liteLlmApiKey,
|
||||
fireworksApiKey,
|
||||
asksageApiKey,
|
||||
xaiApiKey,
|
||||
sambanovaApiKey,
|
||||
cerebrasApiKey,
|
||||
groqApiKey,
|
||||
moonshotApiKey,
|
||||
nebiusApiKey,
|
||||
sapAiCoreClientId,
|
||||
sapAiCoreClientSecret,
|
||||
huggingFaceApiKey,
|
||||
}
|
||||
|
||||
// Execute batched operations in parallel for maximum performance
|
||||
await Promise.all([updateGlobalStateBatch(context, batchedGlobalUpdates), updateSecretsBatch(context, batchedSecretUpdates)])
|
||||
await controller.cacheService.reInitialize()
|
||||
}
|
||||
|
||||
export async function resetWorkspaceState(context: vscode.ExtensionContext) {
|
||||
for (const key of context.workspaceState.keys()) {
|
||||
await context.workspaceState.update(key, undefined)
|
||||
}
|
||||
}
|
||||
|
||||
export async function resetGlobalState(context: vscode.ExtensionContext) {
|
||||
export async function resetGlobalState(controller: Controller) {
|
||||
// TODO: Reset all workspace states?
|
||||
for (const key of context.globalState.keys()) {
|
||||
await context.globalState.update(key, undefined)
|
||||
}
|
||||
const context = controller.context
|
||||
|
||||
await Promise.all(context.globalState.keys().map((key) => context.globalState.update(key, undefined)))
|
||||
const secretKeys: SecretKey[] = [
|
||||
"apiKey",
|
||||
"openRouterApiKey",
|
||||
@@ -833,8 +621,8 @@ export async function resetGlobalState(context: vscode.ExtensionContext) {
|
||||
"moonshotApiKey",
|
||||
"nebiusApiKey",
|
||||
"huggingFaceApiKey",
|
||||
"huaweiCloudMaasApiKey",
|
||||
]
|
||||
for (const key of secretKeys) {
|
||||
await storeSecret(context, key, undefined)
|
||||
}
|
||||
await Promise.all(secretKeys.map((key) => storeSecret(context, key, undefined)))
|
||||
await controller.cacheService.reInitialize()
|
||||
}
|
||||
|
||||
@@ -32,9 +32,10 @@ import {
|
||||
COMPLETION_RESULT_CHANGES_FLAG,
|
||||
} from "@shared/ExtensionMessage"
|
||||
import { ClineAskResponse } from "@shared/WebviewMessage"
|
||||
import { extractFileContent, FileContentResult } from "@integrations/misc/extract-file-content"
|
||||
import { COMMAND_REQ_APP_STRING } from "@shared/combineCommandSequences"
|
||||
import { fileExistsAtPath } from "@utils/fs"
|
||||
import { isClaude4ModelFamily, isGemini2dot5ModelFamily } from "@utils/model-utils"
|
||||
import { isClaude4ModelFamily, isGemini2dot5ModelFamily, isGrok4ModelFamily, modelDoesntSupportWebp } from "@utils/model-utils"
|
||||
import { fixModelHtmlEscaping, removeInvalidChars } from "@utils/string"
|
||||
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
|
||||
import os from "os"
|
||||
@@ -49,12 +50,12 @@ import { ContextManager } from "../context/context-management/ContextManager"
|
||||
import { loadMcpDocumentation } from "../prompts/loadMcpDocumentation"
|
||||
import { formatResponse } from "../prompts/responses"
|
||||
import { ensureTaskDirectoryExists } from "../storage/disk"
|
||||
import { getGlobalState, getWorkspaceState } from "../storage/state"
|
||||
import { CacheService } from "../storage/CacheService"
|
||||
import { TaskState } from "./TaskState"
|
||||
import { MessageStateHandler } from "./message-state"
|
||||
import { AutoApprove } from "./tools/autoApprove"
|
||||
import { showNotificationForApprovalIfAutoApprovalEnabled } from "./utils"
|
||||
import { ChatSettings } from "@/shared/ChatSettings"
|
||||
import { Mode } from "@shared/storage/types"
|
||||
|
||||
export class ToolExecutor {
|
||||
private autoApprover: AutoApprove
|
||||
@@ -85,13 +86,15 @@ export class ToolExecutor {
|
||||
private clineIgnoreController: ClineIgnoreController,
|
||||
private workspaceTracker: WorkspaceTracker,
|
||||
private contextManager: ContextManager,
|
||||
private cacheService: CacheService,
|
||||
|
||||
// Configuration & Settings
|
||||
private autoApprovalSettings: AutoApprovalSettings,
|
||||
private browserSettings: BrowserSettings,
|
||||
private cwd: string,
|
||||
private taskId: string,
|
||||
private chatSettings: ChatSettings,
|
||||
private mode: Mode,
|
||||
private strictPlanModeEnabled: boolean,
|
||||
|
||||
// Callbacks to the Task (Entity)
|
||||
private say: (
|
||||
@@ -122,8 +125,25 @@ export class ToolExecutor {
|
||||
this.autoApprover.updateSettings(settings)
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the tools which should be restricted in plan mode
|
||||
*/
|
||||
private isPlanModeToolRestricted(toolName: ToolUseName): boolean {
|
||||
const planModeRestrictedTools: ToolUseName[] = ["write_to_file", "replace_in_file"]
|
||||
return planModeRestrictedTools.includes(toolName)
|
||||
}
|
||||
|
||||
public updateMode(mode: Mode): void {
|
||||
this.mode = mode
|
||||
}
|
||||
|
||||
public updateStrictPlanModeEnabled(strictPlanModeEnabled: boolean): void {
|
||||
this.strictPlanModeEnabled = strictPlanModeEnabled
|
||||
}
|
||||
|
||||
private pushToolResult = (content: ToolResponse, block: ToolUse) => {
|
||||
const isNextGenModel = isClaude4ModelFamily(this.api) || isGemini2dot5ModelFamily(this.api)
|
||||
const isNextGenModel =
|
||||
isClaude4ModelFamily(this.api) || isGemini2dot5ModelFamily(this.api) || isGrok4ModelFamily(this.api)
|
||||
|
||||
if (typeof content === "string") {
|
||||
const resultText = content || "(tool did not return anything)"
|
||||
@@ -434,6 +454,15 @@ export class ToolExecutor {
|
||||
return
|
||||
}
|
||||
|
||||
// Logic for plan-model tool call restrictions
|
||||
if (this.strictPlanModeEnabled && this.mode === "plan" && block.name && this.isPlanModeToolRestricted(block.name)) {
|
||||
const errorMessage = `Tool '${block.name}' is not available in PLAN MODE. This tool is restricted to ACT MODE for file modifications. Only use tools available for PLAN MODE when in that mode.`
|
||||
await this.say("error", errorMessage)
|
||||
this.pushToolResult(formatResponse.toolError(errorMessage), block)
|
||||
await this.saveCheckpoint()
|
||||
return
|
||||
}
|
||||
|
||||
if (block.name !== "browser_action") {
|
||||
await this.browserSession.closeBrowser()
|
||||
}
|
||||
@@ -488,7 +517,8 @@ export class ToolExecutor {
|
||||
|
||||
const currentFullJson = block.params.diff
|
||||
// Check if we should use streaming (e.g., for specific models)
|
||||
const isNextGenModel = isClaude4ModelFamily(this.api) || isGemini2dot5ModelFamily(this.api)
|
||||
const isNextGenModel =
|
||||
isClaude4ModelFamily(this.api) || isGemini2dot5ModelFamily(this.api) || isGrok4ModelFamily(this.api)
|
||||
// Going through claude family of models
|
||||
if (isNextGenModel && USE_EXPERIMENTAL_CLAUDE4_FEATURES && currentFullJson) {
|
||||
const streamingResult = await this.handleStreamingJsonReplacement(block, relPath, currentFullJson)
|
||||
@@ -839,12 +869,18 @@ export class ToolExecutor {
|
||||
telemetryService.captureToolUsage(this.taskId, block.name, this.api.getModel().id, false, true)
|
||||
}
|
||||
// now execute the tool like normal
|
||||
const content = await extractTextFromFile(absolutePath)
|
||||
const supportsImages = this.api.getModel().info.supportsImages ?? false
|
||||
const result = await extractFileContent(absolutePath, supportsImages)
|
||||
|
||||
// Track file read operation
|
||||
await this.fileContextTracker.trackFileContext(relPath, "read_tool")
|
||||
|
||||
this.pushToolResult(content, block)
|
||||
this.pushToolResult(result.text, block)
|
||||
|
||||
if (result.imageBlock) {
|
||||
this.taskState.userMessageContent.push(result.imageBlock)
|
||||
}
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
@@ -1175,7 +1211,9 @@ export class ToolExecutor {
|
||||
// Re-make browserSession to make sure latest settings apply
|
||||
if (this.context) {
|
||||
await this.browserSession.dispose()
|
||||
this.browserSession = new BrowserSession(this.context, this.browserSettings)
|
||||
|
||||
let useWebp = this.api ? !modelDoesntSupportWebp(this.api) : true
|
||||
this.browserSession = new BrowserSession(this.context, this.browserSettings, useWebp)
|
||||
} else {
|
||||
console.warn("no controller context available for browserSession")
|
||||
}
|
||||
@@ -1919,11 +1957,9 @@ export class ToolExecutor {
|
||||
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 = this.chatSettings.mode
|
||||
const apiProvider =
|
||||
currentMode === "plan"
|
||||
? await getGlobalState(this.context, "planModeApiProvider")
|
||||
: await getGlobalState(this.context, "actModeApiProvider")
|
||||
const currentMode = this.mode
|
||||
const apiConfig = this.cacheService.getApiConfiguration()
|
||||
const apiProvider = currentMode === "plan" ? apiConfig.planModeApiProvider : apiConfig.actModeApiProvider
|
||||
const providerAndModel = `${apiProvider} / ${this.api.getModel().id}`
|
||||
|
||||
// Ask user for confirmation
|
||||
|
||||
+43
-32
@@ -19,7 +19,6 @@ import { ApiConfiguration } from "@shared/api"
|
||||
import { findLast, findLastIndex } from "@shared/array"
|
||||
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
|
||||
import { BrowserSettings } from "@shared/BrowserSettings"
|
||||
import { ChatSettings } from "@shared/ChatSettings"
|
||||
import { combineApiRequests } from "@shared/combineApiRequests"
|
||||
import { combineCommandSequences } from "@shared/combineCommandSequences"
|
||||
import { ClineApiReqCancelReason, ClineApiReqInfo, ClineAsk, ClineMessage, ClineSay } from "@shared/ExtensionMessage"
|
||||
@@ -78,7 +77,7 @@ import { processFilesIntoText } from "@integrations/misc/extract-text"
|
||||
import WorkspaceTracker from "@integrations/workspace/WorkspaceTracker"
|
||||
import { McpHub } from "@services/mcp/McpHub"
|
||||
import { convertClineMessageToProto } from "@shared/proto-conversions/cline-message"
|
||||
import { isClaude4ModelFamily, isGemini2dot5ModelFamily } from "@utils/model-utils"
|
||||
import { isClaude4ModelFamily, isGemini2dot5ModelFamily, isGrok4ModelFamily } from "@utils/model-utils"
|
||||
import { isInTestMode } from "../../services/test/TestMode"
|
||||
import { ensureLocalClineDirExists } from "../context/instructions/user-instructions/rule-helpers"
|
||||
import { refreshWorkflowToggles } from "../context/instructions/user-instructions/workflows"
|
||||
@@ -86,6 +85,8 @@ import { MessageStateHandler } from "./message-state"
|
||||
import { TaskState } from "./TaskState"
|
||||
import { ToolExecutor } from "./ToolExecutor"
|
||||
import { updateApiReqMsg } from "./utils"
|
||||
import { CacheService } from "../storage/CacheService"
|
||||
import { Mode, OpenaiReasoningEffort } from "@shared/storage/types"
|
||||
import { ShowMessageType } from "@/shared/proto/index.host"
|
||||
|
||||
export const USE_EXPERIMENTAL_CLAUDE4_FEATURES = false
|
||||
@@ -130,10 +131,15 @@ export class Task {
|
||||
private reinitExistingTaskFromId: (taskId: string) => Promise<void>
|
||||
private cancelTask: () => Promise<void>
|
||||
|
||||
// Cache service
|
||||
private cacheService: CacheService
|
||||
|
||||
// User chat state
|
||||
autoApprovalSettings: AutoApprovalSettings
|
||||
browserSettings: BrowserSettings
|
||||
chatSettings: ChatSettings
|
||||
preferredLanguage: string
|
||||
openaiReasoningEffort: OpenaiReasoningEffort
|
||||
mode: Mode
|
||||
|
||||
// Message and conversation state
|
||||
messageStateHandler: MessageStateHandler
|
||||
@@ -148,13 +154,17 @@ export class Task {
|
||||
apiConfiguration: ApiConfiguration,
|
||||
autoApprovalSettings: AutoApprovalSettings,
|
||||
browserSettings: BrowserSettings,
|
||||
chatSettings: ChatSettings,
|
||||
preferredLanguage: string,
|
||||
openaiReasoningEffort: OpenaiReasoningEffort,
|
||||
mode: Mode,
|
||||
strictPlanModeEnabled: boolean,
|
||||
shellIntegrationTimeout: number,
|
||||
terminalReuseEnabled: boolean,
|
||||
terminalOutputLineLimit: number,
|
||||
defaultTerminalProfile: string,
|
||||
enableCheckpointsSetting: boolean,
|
||||
cwd: string,
|
||||
cacheService: CacheService,
|
||||
task?: string,
|
||||
images?: string[],
|
||||
files?: string[],
|
||||
@@ -193,9 +203,12 @@ export class Task {
|
||||
this.diffViewProvider = HostProvider.get().createDiffViewProvider()
|
||||
this.autoApprovalSettings = autoApprovalSettings
|
||||
this.browserSettings = browserSettings
|
||||
this.chatSettings = chatSettings
|
||||
this.preferredLanguage = preferredLanguage
|
||||
this.openaiReasoningEffort = openaiReasoningEffort
|
||||
this.mode = mode
|
||||
this.enableCheckpoints = enableCheckpointsSetting
|
||||
this.cwd = cwd
|
||||
this.cacheService = cacheService
|
||||
|
||||
// Set up MCP notification callback for real-time notifications
|
||||
this.mcpHub.setNotificationCallback(async (serverName: string, level: string, message: string) => {
|
||||
@@ -267,19 +280,18 @@ export class Task {
|
||||
},
|
||||
}
|
||||
|
||||
const currentProvider =
|
||||
chatSettings.mode === "plan" ? apiConfiguration.planModeApiProvider : apiConfiguration.actModeApiProvider
|
||||
const currentProvider = this.mode === "plan" ? apiConfiguration.planModeApiProvider : apiConfiguration.actModeApiProvider
|
||||
|
||||
if (currentProvider === "openai" || currentProvider === "openai-native") {
|
||||
if (chatSettings.mode === "plan") {
|
||||
effectiveApiConfiguration.planModeReasoningEffort = chatSettings.openAIReasoningEffort
|
||||
if (this.mode === "plan") {
|
||||
effectiveApiConfiguration.planModeReasoningEffort = this.openaiReasoningEffort
|
||||
} else {
|
||||
effectiveApiConfiguration.actModeReasoningEffort = chatSettings.openAIReasoningEffort
|
||||
effectiveApiConfiguration.actModeReasoningEffort = this.openaiReasoningEffort
|
||||
}
|
||||
}
|
||||
|
||||
// Now that taskId is initialized, we can build the API handler
|
||||
this.api = buildApiHandler(effectiveApiConfiguration, chatSettings.mode)
|
||||
this.api = buildApiHandler(effectiveApiConfiguration, this.mode)
|
||||
|
||||
// Set taskId on browserSession for telemetry tracking
|
||||
this.browserSession.setTaskId(this.taskId)
|
||||
@@ -313,11 +325,13 @@ export class Task {
|
||||
this.clineIgnoreController,
|
||||
this.workspaceTracker,
|
||||
this.contextManager,
|
||||
this.cacheService,
|
||||
this.autoApprovalSettings,
|
||||
this.browserSettings,
|
||||
cwd,
|
||||
this.taskId,
|
||||
this.chatSettings,
|
||||
this.mode,
|
||||
strictPlanModeEnabled,
|
||||
this.say.bind(this),
|
||||
this.ask.bind(this),
|
||||
this.saveCheckpoint.bind(this),
|
||||
@@ -328,6 +342,15 @@ export class Task {
|
||||
)
|
||||
}
|
||||
|
||||
public updateMode(mode: Mode): void {
|
||||
this.mode = mode
|
||||
this.toolExecutor.updateMode(mode)
|
||||
}
|
||||
|
||||
public updateStrictPlanMode(strictPlanModeEnabled: boolean): void {
|
||||
this.toolExecutor.updateStrictPlanModeEnabled(strictPlanModeEnabled)
|
||||
}
|
||||
|
||||
// While a task is ref'd by a controller, it will always have access to the extension context
|
||||
// This error is thrown if the controller derefs the task after e.g., aborting the task
|
||||
private getContext(): vscode.ExtensionContext {
|
||||
@@ -1178,7 +1201,7 @@ export class Task {
|
||||
const hasPendingFileContextWarnings = pendingContextWarning && pendingContextWarning.length > 0
|
||||
|
||||
const [taskResumptionMessage, userResponseMessage] = formatResponse.taskResumption(
|
||||
this.chatSettings?.mode === "plan" ? "plan" : "act",
|
||||
this.mode === "plan" ? "plan" : "act",
|
||||
agoText,
|
||||
this.cwd,
|
||||
wasRecent,
|
||||
@@ -1654,22 +1677,10 @@ export class Task {
|
||||
}
|
||||
}
|
||||
|
||||
private async migratePreferredLanguageToolSetting(): Promise<void> {
|
||||
const config = vscode.workspace.getConfiguration("cline")
|
||||
const preferredLanguage = config.get<LanguageDisplay>("preferredLanguage")
|
||||
if (preferredLanguage !== undefined) {
|
||||
this.chatSettings.preferredLanguage = preferredLanguage
|
||||
// Remove from VSCode configuration
|
||||
await config.update("preferredLanguage", undefined, true)
|
||||
}
|
||||
}
|
||||
|
||||
private async getCurrentProviderInfo(): Promise<{ modelId: string; providerId: string }> {
|
||||
const modelId = this.api.getModel()?.id
|
||||
const providerId =
|
||||
this.chatSettings.mode === "plan"
|
||||
? ((await getGlobalState(this.getContext(), "planModeApiProvider")) as string)
|
||||
: ((await getGlobalState(this.getContext(), "actModeApiProvider")) as string)
|
||||
const apiConfig = this.cacheService.getApiConfiguration()
|
||||
const providerId = (this.mode === "plan" ? apiConfig.planModeApiProvider : apiConfig.actModeApiProvider) as string
|
||||
return { modelId, providerId }
|
||||
}
|
||||
|
||||
@@ -1687,11 +1698,11 @@ export class Task {
|
||||
|
||||
const supportsBrowserUse = modelSupportsBrowserUse && !disableBrowserTool // only enable browser use if the model supports it and the user hasn't disabled it
|
||||
|
||||
const isNextGenModel = isClaude4ModelFamily(this.api) || isGemini2dot5ModelFamily(this.api)
|
||||
const isNextGenModel =
|
||||
isClaude4ModelFamily(this.api) || isGemini2dot5ModelFamily(this.api) || isGrok4ModelFamily(this.api)
|
||||
let systemPrompt = await SYSTEM_PROMPT(this.cwd, supportsBrowserUse, this.mcpHub, this.browserSettings, isNextGenModel)
|
||||
|
||||
await this.migratePreferredLanguageToolSetting()
|
||||
const preferredLanguage = getLanguageKey(this.chatSettings.preferredLanguage as LanguageDisplay)
|
||||
const preferredLanguage = getLanguageKey(this.preferredLanguage as LanguageDisplay)
|
||||
const preferredLanguageInstructions =
|
||||
preferredLanguage && preferredLanguage !== DEFAULT_LANGUAGE_SETTINGS
|
||||
? `# Preferred Language\n\nSpeak in ${preferredLanguage}.`
|
||||
@@ -2000,7 +2011,7 @@ export class Task {
|
||||
const { modelId, providerId } = await this.getCurrentProviderInfo()
|
||||
if (providerId && modelId) {
|
||||
try {
|
||||
await this.modelContextTracker.recordModelUsage(providerId, modelId, this.chatSettings.mode)
|
||||
await this.modelContextTracker.recordModelUsage(providerId, modelId, this.mode)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
@@ -2771,7 +2782,7 @@ export class Task {
|
||||
details += `\n${lastApiReqTotalTokens.toLocaleString()} / ${(contextWindow / 1000).toLocaleString()}K tokens used (${usagePercentage}%)`
|
||||
|
||||
details += "\n\n# Current Mode"
|
||||
if (this.chatSettings.mode === "plan") {
|
||||
if (this.mode === "plan") {
|
||||
details += "\nPLAN MODE\n" + formatResponse.planModeInstructions()
|
||||
} else {
|
||||
details += "\nACT MODE"
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getNonce } from "./getNonce"
|
||||
|
||||
import { WebviewProviderType } from "@/shared/webview/types"
|
||||
import { Controller } from "@core/controller/index"
|
||||
import { CacheService } from "@core/storage/CacheService"
|
||||
import { findLast } from "@shared/array"
|
||||
import { readFile } from "fs/promises"
|
||||
import path from "node:path"
|
||||
@@ -11,7 +12,7 @@ import { v4 as uuidv4 } from "uuid"
|
||||
import { Uri } from "vscode"
|
||||
import { ExtensionMessage } from "@/shared/ExtensionMessage"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { ShowMessageType } from "@/shared/proto/host/window"
|
||||
|
||||
export abstract class WebviewProvider {
|
||||
public static readonly sideBarId = "claude-dev.SidebarProvider" // used in package.json as the view's id. This value cannot be changed due to how vscode caches views based on their id, and updating the id would break existing instances of the extension.
|
||||
@@ -24,13 +25,15 @@ export abstract class WebviewProvider {
|
||||
|
||||
constructor(
|
||||
readonly context: vscode.ExtensionContext,
|
||||
protected readonly outputChannel: vscode.OutputChannel,
|
||||
|
||||
private readonly providerType: WebviewProviderType,
|
||||
) {
|
||||
WebviewProvider.activeInstances.add(this)
|
||||
this.clientId = uuidv4()
|
||||
WebviewProvider.clientIdMap.set(this, this.clientId)
|
||||
this.controller = new Controller(context, outputChannel, (message) => this.postMessageToWebview(message), this.clientId)
|
||||
|
||||
// Create controller with cache service
|
||||
this.controller = new Controller(context, (message) => this.postMessageToWebview(message), this.clientId)
|
||||
}
|
||||
|
||||
// Add a method to get the client ID
|
||||
@@ -262,8 +265,8 @@ export abstract class WebviewProvider {
|
||||
try {
|
||||
await axios.get(`http://${localServerUrl}`)
|
||||
} catch (error) {
|
||||
// Only show the error message if not in development mode.
|
||||
if (!process.env.IS_DEV) {
|
||||
// Only show the error message when in development mode.
|
||||
if (process.env.IS_DEV) {
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message:
|
||||
@@ -304,7 +307,7 @@ export abstract class WebviewProvider {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script src="http://localhost:8097"></script>
|
||||
${process.env.IS_DEV ? '<script src="http://localhost:8097"></script>' : ""}
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
|
||||
<meta http-equiv="Content-Security-Policy" content="${csp.join("; ")}">
|
||||
|
||||
+10
-12
@@ -1,50 +1,48 @@
|
||||
import * as vscode from "vscode"
|
||||
import { Controller } from "@core/controller"
|
||||
import { ClineAPI } from "./cline"
|
||||
import { getGlobalState } from "@core/storage/state"
|
||||
import { sendChatButtonClickedEvent } from "@core/controller/ui/subscribeToChatButtonClicked"
|
||||
import { WebviewProviderType as WebviewProviderTypeEnum } from "@shared/proto/cline/ui"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
|
||||
export function createClineAPI(outputChannel: vscode.OutputChannel, sidebarController: Controller): ClineAPI {
|
||||
export function createClineAPI(sidebarController: Controller): ClineAPI {
|
||||
const api: ClineAPI = {
|
||||
startNewTask: async (task?: string, images?: string[]) => {
|
||||
outputChannel.appendLine("Starting new task")
|
||||
HostProvider.get().logToChannel("Starting new task")
|
||||
await sidebarController.clearTask()
|
||||
await sidebarController.postStateToWebview()
|
||||
|
||||
await sendChatButtonClickedEvent(sidebarController.id)
|
||||
await sidebarController.initTask(task, images)
|
||||
outputChannel.appendLine(
|
||||
HostProvider.get().logToChannel(
|
||||
`Task started with message: ${task ? `"${task}"` : "undefined"} and ${images?.length || 0} image(s)`,
|
||||
)
|
||||
},
|
||||
|
||||
sendMessage: async (message?: string, images?: string[]) => {
|
||||
outputChannel.appendLine(
|
||||
HostProvider.get().logToChannel(
|
||||
`Sending message: ${message ? `"${message}"` : "undefined"} with ${images?.length || 0} image(s)`,
|
||||
)
|
||||
if (sidebarController.task) {
|
||||
await sidebarController.task.handleWebviewAskResponse("messageResponse", message || "", images || [])
|
||||
} else {
|
||||
outputChannel.appendLine("No active task to send message to")
|
||||
HostProvider.get().logToChannel("No active task to send message to")
|
||||
}
|
||||
},
|
||||
|
||||
pressPrimaryButton: async () => {
|
||||
outputChannel.appendLine("Pressing primary button")
|
||||
HostProvider.get().logToChannel("Pressing primary button")
|
||||
if (sidebarController.task) {
|
||||
await sidebarController.task.handleWebviewAskResponse("yesButtonClicked", "", [])
|
||||
} else {
|
||||
outputChannel.appendLine("No active task to press button for")
|
||||
HostProvider.get().logToChannel("No active task to press button for")
|
||||
}
|
||||
},
|
||||
|
||||
pressSecondaryButton: async () => {
|
||||
outputChannel.appendLine("Pressing secondary button")
|
||||
HostProvider.get().logToChannel("Pressing secondary button")
|
||||
if (sidebarController.task) {
|
||||
await sidebarController.task.handleWebviewAskResponse("noButtonClicked", "", [])
|
||||
} else {
|
||||
outputChannel.appendLine("No active task to press button for")
|
||||
HostProvider.get().logToChannel("No active task to press button for")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
+49
-85
@@ -1,45 +1,45 @@
|
||||
// The module 'vscode' contains the VS Code extensibility API
|
||||
// Import the module and reference it with the alias vscode in your code below
|
||||
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
|
||||
import * as vscode from "vscode"
|
||||
import pWaitFor from "p-wait-for"
|
||||
import { Logger } from "./services/logging/Logger"
|
||||
import { createClineAPI } from "./exports"
|
||||
import "./utils/path" // necessary to have access to String.prototype.toPosix
|
||||
import { DIFF_VIEW_URI_SCHEME } from "@hosts/vscode/VscodeDiffViewProvider"
|
||||
import assert from "node:assert"
|
||||
import { posthogClientProvider } from "./services/posthog/PostHogClientProvider"
|
||||
import { WebviewProvider } from "./core/webview"
|
||||
import { sendMcpButtonClickedEvent } from "./core/controller/ui/subscribeToMcpButtonClicked"
|
||||
import { sendChatButtonClickedEvent } from "./core/controller/ui/subscribeToChatButtonClicked"
|
||||
import { ErrorService } from "./services/error/ErrorService"
|
||||
import { initializeTestMode, cleanupTestMode } from "./services/test/TestMode"
|
||||
import { telemetryService } from "./services/posthog/telemetry/TelemetryService"
|
||||
import { sendSettingsButtonClickedEvent } from "./core/controller/ui/subscribeToSettingsButtonClicked"
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
import { WebviewProviderType as WebviewProviderTypeEnum } from "@shared/proto/cline/ui"
|
||||
import { WebviewProviderType } from "./shared/webview/types"
|
||||
import { sendHistoryButtonClickedEvent } from "./core/controller/ui/subscribeToHistoryButtonClicked"
|
||||
import assert from "node:assert"
|
||||
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
|
||||
import pWaitFor from "p-wait-for"
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
import * as vscode from "vscode"
|
||||
import { sendAccountButtonClickedEvent } from "./core/controller/ui/subscribeToAccountButtonClicked"
|
||||
import { sendChatButtonClickedEvent } from "./core/controller/ui/subscribeToChatButtonClicked"
|
||||
import { sendHistoryButtonClickedEvent } from "./core/controller/ui/subscribeToHistoryButtonClicked"
|
||||
import { sendMcpButtonClickedEvent } from "./core/controller/ui/subscribeToMcpButtonClicked"
|
||||
import { sendSettingsButtonClickedEvent } from "./core/controller/ui/subscribeToSettingsButtonClicked"
|
||||
import {
|
||||
migrateWorkspaceToGlobalStorage,
|
||||
migrateCustomInstructionsToGlobalRules,
|
||||
migrateModeFromWorkspaceStorageToControllerState,
|
||||
migrateWelcomeViewCompleted,
|
||||
migrateLegacyApiConfigurationToModeSpecific,
|
||||
migrateWorkspaceToGlobalStorage,
|
||||
} from "./core/storage/state-migrations"
|
||||
import { WebviewProvider } from "./core/webview"
|
||||
import { createClineAPI } from "./exports"
|
||||
import { ErrorService } from "./services/error/ErrorService"
|
||||
import { Logger } from "./services/logging/Logger"
|
||||
import { posthogClientProvider } from "./services/posthog/PostHogClientProvider"
|
||||
import { telemetryService } from "./services/posthog/telemetry/TelemetryService"
|
||||
import { cleanupTestMode, initializeTestMode } from "./services/test/TestMode"
|
||||
import { WebviewProviderType } from "./shared/webview/types"
|
||||
import "./utils/path" // necessary to have access to String.prototype.toPosix
|
||||
|
||||
import { sendFocusChatInputEvent } from "./core/controller/ui/subscribeToFocusChatInput"
|
||||
import { FileContextTracker } from "./core/context/context-tracking/FileContextTracker"
|
||||
import { vscodeHostBridgeClient } from "@/hosts/vscode/hostbridge/client/host-grpc-client"
|
||||
import { VscodeWebviewProvider } from "./hosts/vscode/VscodeWebviewProvider"
|
||||
import { ExtensionContext } from "vscode"
|
||||
import { AuthService } from "./services/auth/AuthService"
|
||||
import { writeTextToClipboard, readTextFromClipboard } from "@/utils/env"
|
||||
import { VscodeDiffViewProvider } from "./hosts/vscode/VscodeDiffViewProvider"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { ShowMessageType } from "./shared/proto/host/window"
|
||||
import { vscodeHostBridgeClient } from "@/hosts/vscode/hostbridge/client/host-grpc-client"
|
||||
import { readTextFromClipboard, writeTextToClipboard } from "@/utils/env"
|
||||
import { ExtensionContext } from "vscode"
|
||||
import { FileContextTracker } from "./core/context/context-tracking/FileContextTracker"
|
||||
import { sendFocusChatInputEvent } from "./core/controller/ui/subscribeToFocusChatInput"
|
||||
import { VscodeDiffViewProvider } from "./hosts/vscode/VscodeDiffViewProvider"
|
||||
import { VscodeWebviewProvider } from "./hosts/vscode/VscodeWebviewProvider"
|
||||
import { GitCommitGenerator } from "./integrations/git/commit-message-generator"
|
||||
import { AuthService } from "./services/auth/AuthService"
|
||||
import { ShowMessageType } from "./shared/proto/host/window"
|
||||
import { SharedUriHandler } from "./services/uri/SharedUriHandler"
|
||||
import { getLatestAnnouncementId } from "./utils/announcements"
|
||||
/*
|
||||
Built using https://github.com/microsoft/vscode-webview-ui-toolkit
|
||||
|
||||
@@ -49,35 +49,23 @@ https://github.com/microsoft/vscode-webview-ui-toolkit-samples/tree/main/framewo
|
||||
|
||||
*/
|
||||
|
||||
let outputChannel: vscode.OutputChannel
|
||||
|
||||
// This method is called when your extension is activated
|
||||
// Your extension is activated the very first time the command is executed
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
outputChannel = vscode.window.createOutputChannel("Cline")
|
||||
context.subscriptions.push(outputChannel)
|
||||
maybeSetupHostProviders(context)
|
||||
|
||||
ErrorService.initialize()
|
||||
Logger.initialize(outputChannel)
|
||||
Logger.log("Cline extension activated")
|
||||
|
||||
maybeSetupHostProviders(context)
|
||||
|
||||
// Migrate custom instructions to global Cline rules (one-time cleanup)
|
||||
await migrateCustomInstructionsToGlobalRules(context)
|
||||
|
||||
// Migrate mode from workspace storage to controller state (one-time cleanup)
|
||||
await migrateModeFromWorkspaceStorageToControllerState(context)
|
||||
|
||||
// Migrate welcomeViewCompleted setting based on existing API keys (one-time cleanup)
|
||||
await migrateWelcomeViewCompleted(context)
|
||||
|
||||
// Migrate workspace storage values back to global storage (reverting previous migration)
|
||||
await migrateWorkspaceToGlobalStorage(context)
|
||||
|
||||
// Migrate legacy API configuration to mode-specific keys (one-time migration)
|
||||
await migrateLegacyApiConfigurationToModeSpecific(context)
|
||||
|
||||
// Clean up orphaned file context warnings (startup cleanup)
|
||||
await FileContextTracker.cleanupOrphanedWarnings(context)
|
||||
|
||||
@@ -105,7 +93,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
|
||||
// Use the same condition as announcements: focus when there's a new announcement to show
|
||||
const lastShownAnnouncementId = context.globalState.get<string>("lastShownAnnouncementId")
|
||||
const latestAnnouncementId = context.extension?.packageJSON?.version?.split(".").slice(0, 2).join(".") ?? ""
|
||||
const latestAnnouncementId = getLatestAnnouncementId(context)
|
||||
|
||||
if (lastShownAnnouncementId !== latestAnnouncementId) {
|
||||
// Focus Cline when there's a new announcement to show (major/minor updates or fresh installs)
|
||||
@@ -279,44 +267,10 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
})()
|
||||
context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider(DIFF_VIEW_URI_SCHEME, diffContentProvider))
|
||||
|
||||
// URI Handler
|
||||
const handleUri = async (uri: vscode.Uri) => {
|
||||
console.log("URI Handler called with:", {
|
||||
path: uri.path,
|
||||
query: uri.query,
|
||||
scheme: uri.scheme,
|
||||
})
|
||||
|
||||
const path = uri.path
|
||||
const query = new URLSearchParams(uri.query.replace(/\+/g, "%2B"))
|
||||
const visibleWebview = WebviewProvider.getVisibleInstance()
|
||||
if (!visibleWebview) {
|
||||
return
|
||||
}
|
||||
switch (path) {
|
||||
case "/openrouter": {
|
||||
const code = query.get("code")
|
||||
if (code) {
|
||||
await visibleWebview?.controller.handleOpenRouterCallback(code)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "/auth": {
|
||||
console.log("Auth callback received:", uri.toString())
|
||||
|
||||
const token = query.get("idToken")
|
||||
const provider = query.get("provider")
|
||||
|
||||
console.log("Auth callback received:", { provider })
|
||||
|
||||
if (token) {
|
||||
await visibleWebview?.controller.handleAuthCallback(token, provider)
|
||||
// await authService.handleAuthCallback(token)
|
||||
}
|
||||
break
|
||||
}
|
||||
default:
|
||||
break
|
||||
const success = await SharedUriHandler.handleUri(uri)
|
||||
if (!success) {
|
||||
console.warn("Extension URI handler: Failed to process URI:", uri.toString())
|
||||
}
|
||||
}
|
||||
context.subscriptions.push(vscode.window.registerUriHandler({ handleUri }))
|
||||
@@ -678,7 +632,10 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
if (event.key === "clineAccountId") {
|
||||
// Check if the secret was removed (logout) or added/updated (login)
|
||||
const secretValue = await context.secrets.get("clineAccountId")
|
||||
const authService = AuthService.getInstance(context)
|
||||
const activeWebviewProvider = WebviewProvider.getVisibleInstance()
|
||||
const controller = activeWebviewProvider?.controller
|
||||
|
||||
const authService = AuthService.getInstance(controller)
|
||||
if (secretValue) {
|
||||
// Secret was added or updated - restore auth info (login from another window)
|
||||
authService?.restoreRefreshTokenAndRetrieveAuthInfo()
|
||||
@@ -690,19 +647,26 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
}),
|
||||
)
|
||||
|
||||
return createClineAPI(outputChannel, sidebarWebview.controller)
|
||||
return createClineAPI(sidebarWebview.controller)
|
||||
}
|
||||
|
||||
function maybeSetupHostProviders(context: ExtensionContext) {
|
||||
if (!HostProvider.isInitialized()) {
|
||||
console.log("Setting up vscode host providers...")
|
||||
|
||||
const createWebview = function (type: WebviewProviderType) {
|
||||
return new VscodeWebviewProvider(context, outputChannel, type)
|
||||
return new VscodeWebviewProvider(context, type)
|
||||
}
|
||||
const createDiffView = function () {
|
||||
return new VscodeDiffViewProvider()
|
||||
}
|
||||
HostProvider.initialize(createWebview, createDiffView, vscodeHostBridgeClient)
|
||||
const outputChannel = vscode.window.createOutputChannel("Cline")
|
||||
context.subscriptions.push(outputChannel)
|
||||
|
||||
const getCallbackUri = async function () {
|
||||
return `${vscode.env.uriScheme || "vscode"}://saoudrizwan.claude-dev`
|
||||
}
|
||||
HostProvider.initialize(createWebview, createDiffView, vscodeHostBridgeClient, outputChannel.appendLine, getCallbackUri)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Vendored
+270
@@ -0,0 +1,270 @@
|
||||
import type { IncomingMessage, Server, ServerResponse } from "node:http"
|
||||
import http from "node:http"
|
||||
import type { AddressInfo } from "node:net"
|
||||
import { openExternal } from "@/utils/env"
|
||||
import { SharedUriHandler } from "@/services/uri/SharedUriHandler"
|
||||
|
||||
const SERVER_TIMEOUT = 10 * 60 * 1000 // 10 minutes
|
||||
|
||||
/**
|
||||
* Handles OAuth authentication flow by creating a local server to receive tokens.
|
||||
*/
|
||||
export class AuthHandler {
|
||||
private static instance: AuthHandler | null = null
|
||||
|
||||
private port = 0
|
||||
private server: Server | null = null
|
||||
private serverCreationPromise: Promise<void> | null = null
|
||||
private timeoutId: NodeJS.Timeout | null = null
|
||||
private enabled: boolean = false
|
||||
|
||||
private constructor() {}
|
||||
|
||||
/**
|
||||
* Gets the singleton instance of AuthHandler
|
||||
* @returns The singleton AuthHandler instance
|
||||
*/
|
||||
public static getInstance(): AuthHandler {
|
||||
if (!AuthHandler.instance) {
|
||||
AuthHandler.instance = new AuthHandler()
|
||||
}
|
||||
return AuthHandler.instance
|
||||
}
|
||||
|
||||
public setEnabled(enabled: boolean): void {
|
||||
this.enabled = enabled
|
||||
}
|
||||
|
||||
public async getCallbackUri(): Promise<string> {
|
||||
if (!this.enabled) {
|
||||
throw Error("AuthHandler was not enabled")
|
||||
}
|
||||
|
||||
if (!this.server) {
|
||||
// If server creation is already in progress, wait for it
|
||||
if (this.serverCreationPromise) {
|
||||
await this.serverCreationPromise
|
||||
} else {
|
||||
// Start server creation and track the promise
|
||||
this.serverCreationPromise = this.createServer()
|
||||
await this.serverCreationPromise
|
||||
}
|
||||
} else {
|
||||
this.updateTimeout()
|
||||
}
|
||||
|
||||
return `http://127.0.0.1:${this.port}`
|
||||
}
|
||||
|
||||
private async createServer(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const server = http.createServer(this.handleRequest.bind(this))
|
||||
|
||||
// Use callback to ensure server is ready before getting address
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const address = server.address()
|
||||
if (!address) {
|
||||
console.error("AuthHandler: Failed to get server address")
|
||||
this.server = null
|
||||
this.port = 0
|
||||
this.serverCreationPromise = null
|
||||
reject(new Error("Failed to get server address"))
|
||||
return
|
||||
}
|
||||
|
||||
// Get the assigned port and set up the server
|
||||
this.port = (address as AddressInfo).port
|
||||
this.server = server
|
||||
console.log("AuthHandler: Server started on port", this.port)
|
||||
this.updateTimeout()
|
||||
this.serverCreationPromise = null
|
||||
resolve()
|
||||
})
|
||||
|
||||
server.on("error", (error) => {
|
||||
console.error("AuthHandler: Server error", error)
|
||||
this.server = null
|
||||
this.port = 0
|
||||
this.serverCreationPromise = null
|
||||
reject(error)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("AuthHandler: Failed to create server", error)
|
||||
this.server = null
|
||||
this.port = 0
|
||||
this.serverCreationPromise = null
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private updateTimeout(): void {
|
||||
if (this.timeoutId) {
|
||||
clearTimeout(this.timeoutId)
|
||||
}
|
||||
|
||||
this.timeoutId = setTimeout(() => this.stop(), SERVER_TIMEOUT)
|
||||
}
|
||||
|
||||
private async handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
||||
console.log("AuthHandler: Received request", req.url)
|
||||
|
||||
if (!req.url) {
|
||||
this.sendResponse(res, 404, "text/plain", "Not found")
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Convert HTTP URL to vscode.Uri and use shared handler directly
|
||||
const fullUrl = `http://127.0.0.1:${this.port}${req.url}`
|
||||
const uri = SharedUriHandler.convertHttpUrlToUri(fullUrl)
|
||||
|
||||
// Use SharedUriHandler directly - it handles all validation and processing
|
||||
const success = await SharedUriHandler.handleUri(uri)
|
||||
|
||||
if (success) {
|
||||
this.sendResponse(res, 200, "text/html", TOKEN_REQUEST_VIEW)
|
||||
} else {
|
||||
this.sendResponse(res, 400, "text/plain", "Bad request")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("AuthHandler: Error processing request", error)
|
||||
this.sendResponse(res, 400, "text/plain", "Bad request")
|
||||
} finally {
|
||||
// Stop the server after handling any request (success or failure)
|
||||
this.stop()
|
||||
}
|
||||
}
|
||||
|
||||
private sendResponse(res: ServerResponse, status: number, type: string, content: string): void {
|
||||
res.writeHead(status, { "Content-Type": type })
|
||||
res.end(content)
|
||||
}
|
||||
|
||||
private async openBrowser(callbackUrl: URL): Promise<void> {
|
||||
await openExternal(callbackUrl.toString())
|
||||
}
|
||||
|
||||
public stop(): void {
|
||||
if (this.timeoutId) {
|
||||
clearTimeout(this.timeoutId)
|
||||
this.timeoutId = null
|
||||
}
|
||||
|
||||
if (this.server) {
|
||||
this.server.close()
|
||||
this.server = null
|
||||
}
|
||||
|
||||
this.serverCreationPromise = null
|
||||
this.port = 0
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.stop()
|
||||
}
|
||||
}
|
||||
|
||||
const TOKEN_REQUEST_VIEW = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Cline - Authentication Success</title>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Azeret+Mono:wght@300;400;700&display=swap');
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Azeret Mono', monospace;
|
||||
background-color: #ffffff;
|
||||
color: #333333;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.container {
|
||||
text-align: center;
|
||||
padding: 32px;
|
||||
background-color: #f8f8f8;
|
||||
border: 1px solid #e1e1e1;
|
||||
border-radius: 6px;
|
||||
max-width: 480px;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background-color: #73c991;
|
||||
margin: 0 auto 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.checkmark::after {
|
||||
content: '✓';
|
||||
font-size: 24px;
|
||||
color: #ffffff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 16px;
|
||||
font-weight: 400;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 24px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.countdown {
|
||||
font-size: 0.8125rem;
|
||||
color: #666666;
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #d1d1d1;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.container {
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="checkmark"></div>
|
||||
<h1>Authentication Successful</h1>
|
||||
<p>Your authentication token has been securely sent back to your IDE. You can now return to your development environment to continue working.</p>
|
||||
<div class="countdown">Feel free to close this window and continue in your IDE</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
+2
-2
@@ -8,8 +8,8 @@ export class ExternalWebviewProvider extends WebviewProvider {
|
||||
// This hostname cannot be changed without updating the external webview handler.
|
||||
private RESOURCE_HOSTNAME: string = "internal.resources"
|
||||
|
||||
constructor(context: vscode.ExtensionContext, outputChannel: vscode.OutputChannel, providerType: WebviewProviderType) {
|
||||
super(context, outputChannel, providerType)
|
||||
constructor(context: vscode.ExtensionContext, providerType: WebviewProviderType) {
|
||||
super(context, providerType)
|
||||
}
|
||||
|
||||
override getWebviewUri(uri: URI) {
|
||||
|
||||
@@ -23,26 +23,44 @@ export class HostProvider {
|
||||
createDiffViewProvider: DiffViewProviderCreator
|
||||
hostBridge: HostBridgeClientProvider
|
||||
|
||||
// Logs to a user-visible output channel.
|
||||
logToChannel: LogToChannel
|
||||
|
||||
// Returns a callback URI that will redirect to Cline.
|
||||
getCallbackUri: () => Promise<string>
|
||||
|
||||
// Private constructor to enforce singleton pattern
|
||||
private constructor(
|
||||
createWebviewProvider: WebviewProviderCreator,
|
||||
createDiffViewProvider: DiffViewProviderCreator,
|
||||
hostBridge: HostBridgeClientProvider,
|
||||
logToChannel: LogToChannel,
|
||||
getCallbackUri: () => Promise<string>,
|
||||
) {
|
||||
this.createWebviewProvider = createWebviewProvider
|
||||
this.createDiffViewProvider = createDiffViewProvider
|
||||
this.hostBridge = hostBridge
|
||||
this.logToChannel = logToChannel
|
||||
this.getCallbackUri = getCallbackUri
|
||||
}
|
||||
|
||||
public static initialize(
|
||||
webviewProviderCreator: WebviewProviderCreator,
|
||||
diffViewProviderCreator: DiffViewProviderCreator,
|
||||
hostBridgeProvider: HostBridgeClientProvider,
|
||||
logToChannel: LogToChannel,
|
||||
getCallbackUri: () => Promise<string>,
|
||||
): HostProvider {
|
||||
if (HostProvider.instance) {
|
||||
throw new Error("Host providers have already been initialized.")
|
||||
}
|
||||
HostProvider.instance = new HostProvider(webviewProviderCreator, diffViewProviderCreator, hostBridgeProvider)
|
||||
HostProvider.instance = new HostProvider(
|
||||
webviewProviderCreator,
|
||||
diffViewProviderCreator,
|
||||
hostBridgeProvider,
|
||||
logToChannel,
|
||||
getCallbackUri,
|
||||
)
|
||||
return HostProvider.instance
|
||||
}
|
||||
|
||||
@@ -51,7 +69,7 @@ export class HostProvider {
|
||||
*/
|
||||
public static get(): HostProvider {
|
||||
if (!HostProvider.instance) {
|
||||
throw new Error("HostProvider not initialized. Call HostProvider.initialize() first.")
|
||||
throw new Error("HostProvider not setup. Call HostProvider.initialize() first.")
|
||||
}
|
||||
return HostProvider.instance
|
||||
}
|
||||
@@ -99,3 +117,5 @@ export type WebviewProviderCreator = (providerType: WebviewProviderType) => Webv
|
||||
* A function that creates DiffViewProvider instances
|
||||
*/
|
||||
export type DiffViewProviderCreator = () => DiffViewProvider
|
||||
|
||||
export type LogToChannel = (message: string) => void
|
||||
|
||||
@@ -30,7 +30,11 @@ export class VscodeDiffViewProvider extends DiffViewProvider {
|
||||
.filter((tab) => tab.input instanceof vscode.TabInputText && arePathsEqual(tab.input.uri.fsPath, this.absolutePath))
|
||||
for (const tab of tabs) {
|
||||
if (!tab.isDirty) {
|
||||
await vscode.window.tabGroups.close(tab)
|
||||
try {
|
||||
await vscode.window.tabGroups.close(tab)
|
||||
} catch (error) {
|
||||
console.warn("Tab close retry failed:", error.message)
|
||||
}
|
||||
}
|
||||
this.documentWasOpen = true
|
||||
}
|
||||
@@ -64,7 +68,9 @@ export class VscodeDiffViewProvider extends DiffViewProvider {
|
||||
})
|
||||
vscode.commands.executeCommand(
|
||||
"vscode.diff",
|
||||
vscode.Uri.parse(`${DIFF_VIEW_URI_SCHEME}:${fileName}`).with({
|
||||
vscode.Uri.from({
|
||||
scheme: DIFF_VIEW_URI_SCHEME,
|
||||
path: fileName,
|
||||
query: Buffer.from(this.originalContent ?? "").toString("base64"),
|
||||
}),
|
||||
uri,
|
||||
@@ -187,7 +193,11 @@ export class VscodeDiffViewProvider extends DiffViewProvider {
|
||||
for (const tab of tabs) {
|
||||
// trying to close dirty views results in save popup
|
||||
if (!tab.isDirty) {
|
||||
await vscode.window.tabGroups.close(tab)
|
||||
try {
|
||||
await vscode.window.tabGroups.close(tab)
|
||||
} catch (error) {
|
||||
console.warn("Tab close retry failed:", error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { Uri } from "vscode"
|
||||
import * as vscode from "vscode"
|
||||
import type { ExtensionMessage } from "@/shared/ExtensionMessage"
|
||||
import type { WebviewProviderType } from "@/shared/webview/types"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
|
||||
/*
|
||||
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
||||
@@ -15,8 +16,8 @@ https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/c
|
||||
export class VscodeWebviewProvider extends WebviewProvider implements vscode.WebviewViewProvider {
|
||||
public webview?: vscode.WebviewView | vscode.WebviewPanel
|
||||
|
||||
constructor(context: vscode.ExtensionContext, outputChannel: vscode.OutputChannel, providerType: WebviewProviderType) {
|
||||
super(context, outputChannel, providerType)
|
||||
constructor(context: vscode.ExtensionContext, providerType: WebviewProviderType) {
|
||||
super(context, providerType)
|
||||
}
|
||||
|
||||
override getWebviewUri(uri: Uri) {
|
||||
@@ -122,7 +123,7 @@ export class VscodeWebviewProvider extends WebviewProvider implements vscode.Web
|
||||
// if the extension is starting a new session, clear previous task state
|
||||
this.controller.clearTask()
|
||||
|
||||
this.outputChannel.appendLine("Webview view resolved")
|
||||
HostProvider.get().logToChannel("Webview view resolved")
|
||||
|
||||
// Title setting logic removed to allow VSCode to use the container title primarily.
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { describe, it, beforeEach, afterEach } from "mocha"
|
||||
import { strict as assert } from "assert"
|
||||
import * as vscode from "vscode"
|
||||
import pWaitFor from "p-wait-for"
|
||||
import { getOpenTabs } from "@/hosts/vscode/hostbridge/window/getOpenTabs"
|
||||
import { GetOpenTabsRequest } from "@/shared/proto/host/window"
|
||||
|
||||
@@ -54,8 +55,18 @@ describe("Hostbridge - Window - getOpenTabs", () => {
|
||||
await createAndOpenTestDocument(1, vscode.ViewColumn.One)
|
||||
await createAndOpenTestDocument(2, vscode.ViewColumn.Two)
|
||||
|
||||
// Wait a bit for tabs to be fully created
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
// Wait for tabs to be fully created
|
||||
await pWaitFor(
|
||||
async () => {
|
||||
const request = GetOpenTabsRequest.create({})
|
||||
const response = await getOpenTabs(request)
|
||||
return response.paths.length === 2
|
||||
},
|
||||
{
|
||||
timeout: 2000,
|
||||
interval: 50,
|
||||
},
|
||||
)
|
||||
|
||||
const request = GetOpenTabsRequest.create({})
|
||||
const response = await getOpenTabs(request)
|
||||
@@ -74,8 +85,18 @@ describe("Hostbridge - Window - getOpenTabs", () => {
|
||||
await createAndOpenTestDocument(2, vscode.ViewColumn.One)
|
||||
await createAndOpenTestDocument(3, vscode.ViewColumn.One)
|
||||
|
||||
// Wait a bit for tabs to be fully created
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
// Wait for tabs to be fully created
|
||||
await pWaitFor(
|
||||
async () => {
|
||||
const request = GetOpenTabsRequest.create({})
|
||||
const response = await getOpenTabs(request)
|
||||
return response.paths.length === 3
|
||||
},
|
||||
{
|
||||
timeout: 2000,
|
||||
interval: 50,
|
||||
},
|
||||
)
|
||||
|
||||
const request = GetOpenTabsRequest.create({})
|
||||
const response = await getOpenTabs(request)
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
import { describe, it, before, after, beforeEach } from "mocha"
|
||||
import { expect } from "chai"
|
||||
import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
import * as fs from "fs/promises"
|
||||
import * as os from "os"
|
||||
import { saveOpenDocumentIfDirty } from "@/hosts/vscode/hostbridge/workspace/saveOpenDocumentIfDirty"
|
||||
import { SaveOpenDocumentIfDirtyRequest } from "@/shared/proto/index.host"
|
||||
|
||||
describe("saveOpenDocumentIfDirty Integration Test", () => {
|
||||
let testWorkspaceRoot: string
|
||||
let testFilePath: string
|
||||
let testFileUri: vscode.Uri
|
||||
|
||||
before(async () => {
|
||||
// Use a temporary directory for tests
|
||||
testWorkspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cline-test-"))
|
||||
|
||||
// Create a test file path
|
||||
testFilePath = path.join(testWorkspaceRoot, "test-save-document.txt")
|
||||
testFileUri = vscode.Uri.file(testFilePath)
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
// Clean up: close all editors and delete test directory
|
||||
await vscode.commands.executeCommand("workbench.action.closeAllEditors")
|
||||
try {
|
||||
await fs.rm(testWorkspaceRoot, { recursive: true, force: true })
|
||||
} catch (error) {
|
||||
// Directory might not exist, ignore
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
// Close all editors before each test
|
||||
await vscode.commands.executeCommand("workbench.action.closeAllEditors")
|
||||
})
|
||||
|
||||
it("should save a dirty document and return wasSaved: true", async () => {
|
||||
// Create a test file with initial content
|
||||
await fs.writeFile(testFilePath, "Initial content")
|
||||
|
||||
// Open the document in VSCode
|
||||
const document = await vscode.workspace.openTextDocument(testFileUri)
|
||||
const editor = await vscode.window.showTextDocument(document)
|
||||
|
||||
// Make the document dirty by editing it
|
||||
await editor.edit((editBuilder) => {
|
||||
editBuilder.insert(new vscode.Position(0, 0), "Modified ")
|
||||
})
|
||||
|
||||
// Verify the document is dirty
|
||||
expect(document.isDirty).to.be.true
|
||||
|
||||
// Call saveOpenDocumentIfDirty
|
||||
const request = SaveOpenDocumentIfDirtyRequest.create({
|
||||
filePath: testFilePath,
|
||||
})
|
||||
const response = await saveOpenDocumentIfDirty(request)
|
||||
|
||||
// Verify the response
|
||||
expect(response.wasSaved).to.be.true
|
||||
|
||||
// Verify the document is no longer dirty
|
||||
expect(document.isDirty).to.be.false
|
||||
|
||||
// Verify the file content was saved
|
||||
const savedContent = await fs.readFile(testFilePath, "utf-8")
|
||||
expect(savedContent).to.equal("Modified Initial content")
|
||||
})
|
||||
|
||||
it("should not save a clean document and return empty response", async () => {
|
||||
// Create a test file
|
||||
await fs.writeFile(testFilePath, "Clean content")
|
||||
|
||||
// Open the document in VSCode
|
||||
const document = await vscode.workspace.openTextDocument(testFileUri)
|
||||
await vscode.window.showTextDocument(document)
|
||||
|
||||
// Verify the document is not dirty
|
||||
expect(document.isDirty).to.be.false
|
||||
|
||||
// Call saveOpenDocumentIfDirty
|
||||
const request = SaveOpenDocumentIfDirtyRequest.create({
|
||||
filePath: testFilePath,
|
||||
})
|
||||
const response = await saveOpenDocumentIfDirty(request)
|
||||
|
||||
// Verify the response
|
||||
expect(response.wasSaved).to.be.undefined
|
||||
|
||||
// Verify the document is still not dirty
|
||||
expect(document.isDirty).to.be.false
|
||||
})
|
||||
|
||||
it("should return empty response when document is not open", async () => {
|
||||
// Ensure no documents are open
|
||||
await vscode.commands.executeCommand("workbench.action.closeAllEditors")
|
||||
|
||||
// Call saveOpenDocumentIfDirty with a non-existent file
|
||||
const request = SaveOpenDocumentIfDirtyRequest.create({
|
||||
filePath: path.join(testWorkspaceRoot, "non-existent-file.txt"),
|
||||
})
|
||||
const response = await saveOpenDocumentIfDirty(request)
|
||||
|
||||
// Verify the response
|
||||
expect(response.wasSaved).to.be.undefined
|
||||
})
|
||||
|
||||
it("should handle multiple open documents and save only the specified one", async () => {
|
||||
// Create multiple test files
|
||||
const testFile1 = path.join(testWorkspaceRoot, "test-file-1.txt")
|
||||
const testFile2 = path.join(testWorkspaceRoot, "test-file-2.txt")
|
||||
const testFile3 = path.join(testWorkspaceRoot, "test-file-3.txt")
|
||||
|
||||
await fs.writeFile(testFile1, "File 1 content")
|
||||
await fs.writeFile(testFile2, "File 2 content")
|
||||
await fs.writeFile(testFile3, "File 3 content")
|
||||
|
||||
try {
|
||||
// Open all documents
|
||||
const doc1 = await vscode.workspace.openTextDocument(vscode.Uri.file(testFile1))
|
||||
const doc2 = await vscode.workspace.openTextDocument(vscode.Uri.file(testFile2))
|
||||
const doc3 = await vscode.workspace.openTextDocument(vscode.Uri.file(testFile3))
|
||||
|
||||
// Edit all documents to make them dirty
|
||||
const editor1 = await vscode.window.showTextDocument(doc1)
|
||||
await editor1.edit((editBuilder) => {
|
||||
editBuilder.insert(new vscode.Position(0, 0), "Modified ")
|
||||
})
|
||||
|
||||
const editor2 = await vscode.window.showTextDocument(doc2)
|
||||
await editor2.edit((editBuilder) => {
|
||||
editBuilder.insert(new vscode.Position(0, 0), "Modified ")
|
||||
})
|
||||
|
||||
const editor3 = await vscode.window.showTextDocument(doc3)
|
||||
await editor3.edit((editBuilder) => {
|
||||
editBuilder.insert(new vscode.Position(0, 0), "Modified ")
|
||||
})
|
||||
|
||||
// Verify all documents are dirty
|
||||
expect(doc1.isDirty).to.be.true
|
||||
expect(doc2.isDirty).to.be.true
|
||||
expect(doc3.isDirty).to.be.true
|
||||
|
||||
// Save only the second document
|
||||
const request = SaveOpenDocumentIfDirtyRequest.create({
|
||||
filePath: testFile2,
|
||||
})
|
||||
const response = await saveOpenDocumentIfDirty(request)
|
||||
|
||||
// Verify the response
|
||||
expect(response.wasSaved).to.be.true
|
||||
|
||||
// Verify only doc2 was saved
|
||||
expect(doc1.isDirty).to.be.true
|
||||
expect(doc2.isDirty).to.be.false
|
||||
expect(doc3.isDirty).to.be.true
|
||||
|
||||
// Verify the file content
|
||||
const savedContent = await fs.readFile(testFile2, "utf-8")
|
||||
expect(savedContent).to.equal("Modified File 2 content")
|
||||
} finally {
|
||||
// Clean up
|
||||
await fs.unlink(testFile1).catch(() => {})
|
||||
await fs.unlink(testFile2).catch(() => {})
|
||||
await fs.unlink(testFile3).catch(() => {})
|
||||
}
|
||||
})
|
||||
|
||||
it("should handle empty file path gracefully", async () => {
|
||||
const request = SaveOpenDocumentIfDirtyRequest.create({
|
||||
filePath: "",
|
||||
})
|
||||
const response = await saveOpenDocumentIfDirty(request)
|
||||
|
||||
expect(response.wasSaved).to.be.undefined
|
||||
})
|
||||
|
||||
it("should handle undefined file path gracefully", async () => {
|
||||
const request = SaveOpenDocumentIfDirtyRequest.create({})
|
||||
const response = await saveOpenDocumentIfDirty(request)
|
||||
|
||||
expect(response.wasSaved).to.be.undefined
|
||||
})
|
||||
})
|
||||
@@ -1,14 +1,12 @@
|
||||
import { SaveOpenDocumentIfDirtyRequest } from "@/shared/proto/index.host"
|
||||
import { Empty } from "@shared/proto/cline/common"
|
||||
import { SaveOpenDocumentIfDirtyRequest, SaveOpenDocumentIfDirtyResponse } from "@/shared/proto/index.host"
|
||||
import * as vscode from "vscode"
|
||||
import { arePathsEqual } from "@utils/path"
|
||||
|
||||
export async function saveOpenDocumentIfDirty(request: SaveOpenDocumentIfDirtyRequest): Promise<Empty> {
|
||||
export async function saveOpenDocumentIfDirty(request: SaveOpenDocumentIfDirtyRequest): Promise<SaveOpenDocumentIfDirtyResponse> {
|
||||
const existingDocument = vscode.workspace.textDocuments.find((doc) => arePathsEqual(doc.uri.fsPath, request.filePath))
|
||||
|
||||
if (existingDocument && existingDocument.isDirty) {
|
||||
await existingDocument.save()
|
||||
return { wasSaved: true }
|
||||
}
|
||||
|
||||
return Empty.create({})
|
||||
return {}
|
||||
}
|
||||
|
||||
@@ -96,7 +96,6 @@ function getBuildArtifactPatterns(): string[] {
|
||||
"node_modules/",
|
||||
"obj/",
|
||||
"out/",
|
||||
"pkg/",
|
||||
"pycache/",
|
||||
"target/dependency/",
|
||||
"temp/",
|
||||
|
||||
@@ -2,17 +2,17 @@ import fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { fileExistsAtPath } from "@utils/fs"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
|
||||
/**
|
||||
* Cleans up legacy checkpoints from task folders.
|
||||
* This is a one-time operation that runs when the extension is updated to use the new checkpoint system.
|
||||
*
|
||||
* @param globalStoragePath - Path to the extension's global storage
|
||||
* @param outputChannel - VSCode output channel for logging
|
||||
*/
|
||||
export async function cleanupLegacyCheckpoints(globalStoragePath: string, outputChannel: vscode.OutputChannel): Promise<void> {
|
||||
export async function cleanupLegacyCheckpoints(globalStoragePath: string): Promise<void> {
|
||||
try {
|
||||
outputChannel.appendLine("Checking for legacy checkpoints...")
|
||||
HostProvider.get().logToChannel("Checking for legacy checkpoints...")
|
||||
|
||||
const tasksDir = path.join(globalStoragePath, "tasks")
|
||||
|
||||
@@ -45,27 +45,29 @@ export async function cleanupLegacyCheckpoints(globalStoragePath: string, output
|
||||
const checkpointsDir = path.join(mostRecentFolder.path, "checkpoints")
|
||||
|
||||
if (await fileExistsAtPath(checkpointsDir)) {
|
||||
outputChannel.appendLine("Found legacy checkpoints directory, cleaning up...")
|
||||
HostProvider.get().logToChannel("Found legacy checkpoints directory, cleaning up...")
|
||||
|
||||
// Legacy checkpoints found, delete checkpoints directories in all task folders
|
||||
for (const folder of folderStats) {
|
||||
const folderCheckpointsDir = path.join(folder.path, "checkpoints")
|
||||
if (await fileExistsAtPath(folderCheckpointsDir)) {
|
||||
outputChannel.appendLine(`Deleting legacy checkpoints in ${folder.folder}`)
|
||||
HostProvider.get().logToChannel(`Deleting legacy checkpoints in ${folder.folder}`)
|
||||
try {
|
||||
await fs.rm(folderCheckpointsDir, { recursive: true, force: true })
|
||||
} catch (error) {
|
||||
// Ignore error if directory removal fails
|
||||
outputChannel.appendLine(`Warning: Failed to delete checkpoints in ${folder.folder}, continuing...`)
|
||||
HostProvider.get().logToChannel(
|
||||
`Warning: Failed to delete checkpoints in ${folder.folder}, continuing...`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
outputChannel.appendLine("Legacy checkpoints cleanup completed")
|
||||
HostProvider.get().logToChannel("Legacy checkpoints cleanup completed")
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
outputChannel.appendLine(`Error cleaning up legacy checkpoints: ${error}`)
|
||||
HostProvider.get().logToChannel(`Error cleaning up legacy checkpoints: ${error}`)
|
||||
console.error("Error cleaning up legacy checkpoints:", error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as vscode from "vscode"
|
||||
import { openExternal } from "@utils/env"
|
||||
import { HostProvider } from "@hosts/host-provider"
|
||||
import { ShowMessageType } from "@shared/proto/host/window"
|
||||
|
||||
/**
|
||||
* Detects potential AI-generated code omissions in the given file content.
|
||||
@@ -41,13 +42,16 @@ function detectCodeOmission(originalFileContent: string, newFileContent: string)
|
||||
*/
|
||||
export function showOmissionWarning(originalFileContent: string, newFileContent: string): void {
|
||||
if (detectCodeOmission(originalFileContent, newFileContent)) {
|
||||
vscode.window
|
||||
.showWarningMessage(
|
||||
"Potential code truncation detected. This happens when the AI reaches its max output limit.",
|
||||
"Follow this guide to fix the issue",
|
||||
)
|
||||
.then((selection) => {
|
||||
if (selection === "Follow this guide to fix the issue") {
|
||||
HostProvider.window
|
||||
.showMessage({
|
||||
type: ShowMessageType.WARNING,
|
||||
message: "Potential code truncation detected. This happens when the AI reaches its max output limit.",
|
||||
options: {
|
||||
items: ["Follow this guide to fix the issue"],
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.selectedOption === "Follow this guide to fix the issue") {
|
||||
openExternal(
|
||||
"https://github.com/cline/cline/wiki/Troubleshooting-%E2%80%90-Cline-Deleting-Code-with-%22Rest-of-Code-Here%22-Comments",
|
||||
)
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import * as path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { callTextExtractionFunctions } from "./extract-text"
|
||||
import { extractImageContent } from "./extract-images"
|
||||
|
||||
export type FileContentResult = {
|
||||
text: string
|
||||
imageBlock?: Anthropic.ImageBlockParam
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract content from a file, handling both text and images
|
||||
* Extra logic for handling images based on whether the model supports images
|
||||
*/
|
||||
export async function extractFileContent(absolutePath: string, modelSupportsImages: boolean): Promise<FileContentResult> {
|
||||
// Check if file exists first
|
||||
try {
|
||||
await fs.access(absolutePath)
|
||||
} catch (error) {
|
||||
throw new Error(`File not found: ${absolutePath}`)
|
||||
}
|
||||
|
||||
const fileExtension = path.extname(absolutePath).toLowerCase()
|
||||
const imageExtensions = [".png", ".jpg", ".jpeg", ".webp"]
|
||||
const isImage = imageExtensions.includes(fileExtension)
|
||||
|
||||
if (isImage && modelSupportsImages) {
|
||||
const imageResult = await extractImageContent(absolutePath)
|
||||
|
||||
if (imageResult.success) {
|
||||
return {
|
||||
text: "Successfully read image",
|
||||
imageBlock: imageResult.imageBlock,
|
||||
}
|
||||
} else {
|
||||
throw new Error(imageResult.error)
|
||||
}
|
||||
} else if (isImage && !modelSupportsImages) {
|
||||
throw new Error(`Current model does not support image input`)
|
||||
} else {
|
||||
// Handle text files using existing extraction functions
|
||||
try {
|
||||
const textContent = await callTextExtractionFunctions(absolutePath)
|
||||
return {
|
||||
text: textContent,
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error"
|
||||
throw new Error(`Error reading file: ${errorMessage}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import sizeOf from "image-size"
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { getMimeType } from "./process-files"
|
||||
|
||||
/**
|
||||
* Extract image content without VSCode dependencies
|
||||
* Returns success/error result to avoid throwing exceptions
|
||||
*/
|
||||
export async function extractImageContent(
|
||||
filePath: string,
|
||||
): Promise<{ success: true; imageBlock: Anthropic.ImageBlockParam } | { success: false; error: string }> {
|
||||
try {
|
||||
// Read the file into a buffer
|
||||
const buffer = await fs.readFile(filePath)
|
||||
|
||||
// Convert Node.js Buffer to Uint8Array for image-size
|
||||
const uint8Array = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength)
|
||||
|
||||
// Get dimensions from Uint8Array
|
||||
const dimensions = sizeOf(uint8Array)
|
||||
|
||||
if (!dimensions.width || !dimensions.height) {
|
||||
return { success: false, error: "Could not determine image dimensions, so image could not be read" }
|
||||
}
|
||||
|
||||
if (dimensions.width > 7500 || dimensions.height > 7500) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Image dimensions exceed 7500px by 7500px, so image could not be read",
|
||||
}
|
||||
}
|
||||
|
||||
// Convert buffer to base64
|
||||
const base64 = buffer.toString("base64")
|
||||
const mimeType = getMimeType(filePath) as "image/jpeg" | "image/png" | "image/webp"
|
||||
|
||||
// Create the image block in Anthropic format
|
||||
const imageBlock: Anthropic.ImageBlockParam = {
|
||||
type: "image",
|
||||
source: {
|
||||
type: "base64",
|
||||
media_type: mimeType,
|
||||
data: base64,
|
||||
},
|
||||
}
|
||||
|
||||
return { success: true, imageBlock }
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error"
|
||||
return { success: false, error: `Error reading image: ${errorMessage}` }
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,16 @@ export async function extractTextFromFile(filePath: string): Promise<string> {
|
||||
} catch (error) {
|
||||
throw new Error(`File not found: ${filePath}`)
|
||||
}
|
||||
|
||||
return callTextExtractionFunctions(filePath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Expects the fs.access call to have already been performed prior to calling
|
||||
*/
|
||||
export async function callTextExtractionFunctions(filePath: string): Promise<string> {
|
||||
const fileExtension = path.extname(filePath).toLowerCase()
|
||||
|
||||
switch (fileExtension) {
|
||||
case ".pdf":
|
||||
return extractTextFromPDF(filePath)
|
||||
|
||||
@@ -44,7 +44,7 @@ export async function selectFiles(imagesAllowed: boolean): Promise<{ images: str
|
||||
// Convert Node.js Buffer to Uint8Array
|
||||
const uint8Array = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength)
|
||||
const dimensions = sizeOf(uint8Array) // Get dimensions from Uint8Array
|
||||
if (dimensions.width! > 7500 || dimensions.height! > 7500) {
|
||||
if (dimensions.width! > 7680 || dimensions.height! > 7680) {
|
||||
console.warn(`Image dimensions exceed 7500px, skipping: ${filePath}`)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
@@ -107,7 +107,7 @@ export async function selectFiles(imagesAllowed: boolean): Promise<{ images: str
|
||||
return { images, files }
|
||||
}
|
||||
|
||||
function getMimeType(filePath: string): string {
|
||||
export function getMimeType(filePath: string): string {
|
||||
const ext = path.extname(filePath).toLowerCase()
|
||||
switch (ext) {
|
||||
case ".png":
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { describe, it, beforeEach, afterEach } from "mocha"
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import "should"
|
||||
import * as sinon from "sinon"
|
||||
import { TerminalProcess } from "./TerminalProcess"
|
||||
import * as vscode from "vscode"
|
||||
import { type DiffViewProviderCreator, HostProvider, type WebviewProviderCreator } from "@/hosts/host-provider"
|
||||
import { vscodeHostBridgeClient } from "@/hosts/vscode/hostbridge/client/host-grpc-client"
|
||||
import { TerminalProcess } from "./TerminalProcess"
|
||||
import { TerminalRegistry } from "./TerminalRegistry"
|
||||
|
||||
declare module "vscode" {
|
||||
@@ -36,6 +38,14 @@ describe("TerminalProcess (Integration Tests)", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox({ useFakeTimers: true })
|
||||
HostProvider.reset()
|
||||
HostProvider.initialize(
|
||||
((_) => {}) as WebviewProviderCreator,
|
||||
(() => {}) as DiffViewProviderCreator,
|
||||
vscodeHostBridgeClient,
|
||||
(s: string) => console.log(s),
|
||||
async () => "http://localhost:3000/callback",
|
||||
)
|
||||
process = new TerminalProcess()
|
||||
})
|
||||
|
||||
@@ -225,7 +235,7 @@ describe("TerminalProcess (Integration Tests)", () => {
|
||||
// The following tests require shell integration and controlled terminal output
|
||||
describe("Shell integration tests", () => {
|
||||
// We'll mock the terminal run process and TerminalProcess for these tests
|
||||
it("should emit completed and continue events when command finishes", async function () {
|
||||
it("should emit completed and continue events when command finishes", async () => {
|
||||
// Create a terminal to ensure proper interface, but we'll use mocking under the hood
|
||||
const terminal = TerminalRegistry.createTerminal().terminal
|
||||
createdTerminals.push(terminal)
|
||||
@@ -260,7 +270,7 @@ describe("TerminalProcess (Integration Tests)", () => {
|
||||
|
||||
// Tests with controlled output
|
||||
describe("Controlled output tests", () => {
|
||||
it("should emit line events for each line of output", async function () {
|
||||
it("should emit line events for each line of output", async () => {
|
||||
// Create a terminal
|
||||
const terminal = TerminalRegistry.createTerminal().terminal
|
||||
createdTerminals.push(terminal)
|
||||
@@ -285,7 +295,7 @@ describe("TerminalProcess (Integration Tests)", () => {
|
||||
;(emitSpy as sinon.SinonSpy).calledWith("line", "line3").should.be.true()
|
||||
})
|
||||
|
||||
it("should properly handle process hot state (e.g. compiling)", async function () {
|
||||
it("should properly handle process hot state (e.g. compiling)", async () => {
|
||||
// Create a terminal
|
||||
const terminal = TerminalRegistry.createTerminal().terminal
|
||||
createdTerminals.push(terminal)
|
||||
@@ -313,7 +323,7 @@ describe("TerminalProcess (Integration Tests)", () => {
|
||||
foundCompilingTimeout.length.should.be.greaterThan(0)
|
||||
})
|
||||
|
||||
it("should handle standard commands with normal hot timeout", async function () {
|
||||
it("should handle standard commands with normal hot timeout", async () => {
|
||||
// Create a terminal
|
||||
const terminal = TerminalRegistry.createTerminal().terminal
|
||||
createdTerminals.push(terminal)
|
||||
@@ -343,7 +353,7 @@ describe("TerminalProcess (Integration Tests)", () => {
|
||||
;(emitSpy as sinon.SinonSpy).calledWith("completed").should.be.true()
|
||||
})
|
||||
|
||||
it("should correctly filter command echoes based on current implementation", async function () {
|
||||
it("should correctly filter command echoes based on current implementation", async () => {
|
||||
// Create a terminal
|
||||
const terminal = TerminalRegistry.createTerminal().terminal
|
||||
createdTerminals.push(terminal)
|
||||
@@ -374,7 +384,7 @@ describe("TerminalProcess (Integration Tests)", () => {
|
||||
;(emitSpy as sinon.SinonSpy).calledWith("line", "test-command").should.be.false()
|
||||
})
|
||||
|
||||
it("should handle npm run commands", async function () {
|
||||
it("should handle npm run commands", async () => {
|
||||
// Create a terminal
|
||||
const terminal = TerminalRegistry.createTerminal().terminal
|
||||
createdTerminals.push(terminal)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { EventEmitter } from "events"
|
||||
import { stripAnsi } from "./ansiUtils"
|
||||
import * as vscode from "vscode"
|
||||
import { Logger } from "@services/logging/Logger"
|
||||
import { getLatestTerminalOutput } from "./get-latest-output"
|
||||
|
||||
export interface TerminalProcessEvents {
|
||||
@@ -28,9 +27,6 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
|
||||
private gracePeriodTimer: NodeJS.Timeout | null = null
|
||||
private hasEmittedCompleted: boolean = false
|
||||
|
||||
// constructor() {
|
||||
// super()
|
||||
|
||||
private async emitCurrentTerminalContents(): Promise<void> {
|
||||
try {
|
||||
const terminalSnapshot = await getLatestTerminalOutput()
|
||||
|
||||
@@ -3,15 +3,18 @@ import { clineEnvConfig } from "@/config"
|
||||
import { Controller } from "@/core/controller"
|
||||
import { getRequestRegistry, type StreamingResponseHandler } from "@/core/controller/grpc-handler"
|
||||
import { storeSecret } from "@/core/storage/state"
|
||||
import { telemetryService } from "@services/posthog/telemetry/TelemetryService"
|
||||
import { AuthState, UserInfo } from "@shared/proto/cline/account"
|
||||
import { type EmptyRequest, String } from "@shared/proto/cline/common"
|
||||
import { AuthHandler } from "@/hosts/external/AuthHandler"
|
||||
import { FirebaseAuthProvider } from "./providers/FirebaseAuthProvider"
|
||||
import { openExternal } from "@/utils/env"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
|
||||
const DefaultClineAccountURI = `${clineEnvConfig.appBaseUrl}/auth`
|
||||
let authProviders: any[] = []
|
||||
|
||||
type ServiceConfig = {
|
||||
export type ServiceConfig = {
|
||||
URI?: string
|
||||
[key: string]: any
|
||||
}
|
||||
@@ -49,13 +52,13 @@ export interface ClineAccountOrganization {
|
||||
// TODO: Add logic to handle multiple webviews getting auth updates.
|
||||
|
||||
export class AuthService {
|
||||
private static instance: AuthService | null = null
|
||||
private _config: ServiceConfig
|
||||
private _authenticated: boolean = false
|
||||
private _clineAuthInfo: ClineAuthInfo | null = null
|
||||
private _provider: { provider: FirebaseAuthProvider } | null = null
|
||||
private _activeAuthStatusUpdateSubscriptions = new Set<[Controller, StreamingResponseHandler<AuthState>]>()
|
||||
private _context: vscode.ExtensionContext
|
||||
protected static instance: AuthService | null = null
|
||||
protected _config: ServiceConfig
|
||||
protected _authenticated: boolean = false
|
||||
protected _clineAuthInfo: ClineAuthInfo | null = null
|
||||
protected _provider: { provider: FirebaseAuthProvider } | null = null
|
||||
protected _activeAuthStatusUpdateSubscriptions = new Set<[Controller, StreamingResponseHandler<AuthState>]>()
|
||||
protected _controller: Controller
|
||||
|
||||
/**
|
||||
* Creates an instance of AuthService.
|
||||
@@ -63,7 +66,7 @@ export class AuthService {
|
||||
* @param authProvider - Optional authentication provider to use.
|
||||
* @param controller - Optional reference to the Controller instance.
|
||||
*/
|
||||
private constructor(context: vscode.ExtensionContext, config: ServiceConfig, authProvider?: any) {
|
||||
protected constructor(controller: Controller, config: ServiceConfig, authProvider?: any) {
|
||||
const providerName = authProvider || "firebase"
|
||||
this._config = Object.assign({ URI: DefaultClineAccountURI }, config)
|
||||
|
||||
@@ -94,7 +97,7 @@ export class AuthService {
|
||||
|
||||
this._setProvider(authProviders.find((authProvider) => authProvider.name === providerName).name)
|
||||
|
||||
this._context = context
|
||||
this._controller = controller
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,22 +107,29 @@ export class AuthService {
|
||||
* @param controller - Optional reference to the Controller instance.
|
||||
* @returns The singleton instance of AuthService.
|
||||
*/
|
||||
public static getInstance(context?: vscode.ExtensionContext, config?: ServiceConfig, authProvider?: any): AuthService {
|
||||
public static getInstance(controller?: Controller, config?: ServiceConfig, authProvider?: any): AuthService {
|
||||
if (!AuthService.instance) {
|
||||
if (!context) {
|
||||
if (!controller) {
|
||||
console.warn("Extension context was not provided to AuthService.getInstance, using default context")
|
||||
context = {} as vscode.ExtensionContext
|
||||
controller = {} as Controller
|
||||
}
|
||||
if (process.env.E2E_TEST) {
|
||||
// Use require instead of import to avoid circular dependency issues
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { AuthServiceMock } = require("./AuthServiceMock")
|
||||
AuthService.instance = AuthServiceMock.getInstance(controller, config || {}, authProvider)
|
||||
} else {
|
||||
AuthService.instance = new AuthService(controller, config || {}, authProvider)
|
||||
}
|
||||
AuthService.instance = new AuthService(context, config || {}, authProvider)
|
||||
}
|
||||
if (context !== undefined) {
|
||||
AuthService.instance.context = context
|
||||
if (controller !== undefined && AuthService.instance) {
|
||||
AuthService.instance.controller = controller
|
||||
}
|
||||
return AuthService.instance
|
||||
return AuthService.instance!
|
||||
}
|
||||
|
||||
set context(context: vscode.ExtensionContext) {
|
||||
this._context = context
|
||||
set controller(controller: Controller) {
|
||||
this._controller = controller
|
||||
}
|
||||
|
||||
get authProvider(): any {
|
||||
@@ -146,7 +156,7 @@ export class AuthService {
|
||||
return this._clineAuthInfo.idToken
|
||||
}
|
||||
|
||||
private _setProvider(providerName: string): void {
|
||||
protected _setProvider(providerName: string): void {
|
||||
const providerConfig = authProviders.find((provider) => provider.name === providerName)
|
||||
if (!providerConfig) {
|
||||
throw new Error(`Auth provider "${providerName}" not found`)
|
||||
@@ -187,7 +197,8 @@ export class AuthService {
|
||||
throw new Error("Authentication URI is not configured")
|
||||
}
|
||||
|
||||
const callbackUrl = `${vscode.env.uriScheme || "vscode"}://saoudrizwan.claude-dev/auth`
|
||||
const callbackHost = await HostProvider.get().getCallbackUri()
|
||||
const callbackUrl = `${callbackHost}/auth`
|
||||
|
||||
// Use URL object for more graceful query construction
|
||||
const authUrl = new URL(this._config.URI)
|
||||
@@ -220,9 +231,13 @@ export class AuthService {
|
||||
}
|
||||
|
||||
try {
|
||||
this._clineAuthInfo = await this._provider.provider.signIn(this._context, token, provider)
|
||||
this._clineAuthInfo = await this._provider.provider.signIn(this._controller, token, provider)
|
||||
this._authenticated = true
|
||||
|
||||
if (this._clineAuthInfo) {
|
||||
telemetryService.identifyAccount(this._clineAuthInfo.userInfo)
|
||||
}
|
||||
|
||||
await this.sendAuthStatusUpdate()
|
||||
// return this._clineAuthInfo
|
||||
} catch (error) {
|
||||
@@ -236,7 +251,7 @@ export class AuthService {
|
||||
* This is typically called when the user logs out.
|
||||
*/
|
||||
async clearAuthToken(): Promise<void> {
|
||||
await storeSecret(this._context, "clineAccountId", undefined)
|
||||
this._controller.cacheService.setSecret("clineAccountId", undefined)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -249,9 +264,10 @@ export class AuthService {
|
||||
}
|
||||
|
||||
try {
|
||||
this._clineAuthInfo = await this._provider.provider.retrieveClineAuthInfo(this._context)
|
||||
this._clineAuthInfo = await this._provider.provider.retrieveClineAuthInfo(this._controller)
|
||||
if (this._clineAuthInfo) {
|
||||
this._authenticated = true
|
||||
telemetryService.identifyAccount(this._clineAuthInfo.userInfo)
|
||||
await this.sendAuthStatusUpdate()
|
||||
} else {
|
||||
console.warn("No user found after restoring auth token")
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import { String } from "@shared/proto/cline/common"
|
||||
import type vscode from "vscode"
|
||||
import { clineEnvConfig } from "@/config"
|
||||
import { WebviewProvider } from "@/core/webview"
|
||||
import type { UserResponse } from "@/shared/ClineAccount"
|
||||
import { AuthService, type ServiceConfig } from "./AuthService"
|
||||
import { Controller } from "@/core/controller"
|
||||
|
||||
export class AuthServiceMock extends AuthService {
|
||||
protected constructor(controller: Controller, config: ServiceConfig, authProvider?: any) {
|
||||
super(controller, config, authProvider)
|
||||
|
||||
if (process?.env?.CLINE_ENVIRONMENT !== "local") {
|
||||
throw new Error("AuthServiceMock should only be used in local environment for testing purposes.")
|
||||
}
|
||||
|
||||
this._config = Object.assign({ URI: clineEnvConfig.apiBaseUrl }, config)
|
||||
|
||||
const providerName = "firebase"
|
||||
this._setProvider(providerName)
|
||||
|
||||
this._controller = controller
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the singleton instance of AuthServiceMock.
|
||||
*/
|
||||
public static override getInstance(controller?: Controller, config?: ServiceConfig, authProvider?: any): AuthServiceMock {
|
||||
if (!AuthServiceMock.instance) {
|
||||
if (!controller) {
|
||||
console.error("Extension controller was not provided to AuthServiceMock.getInstance")
|
||||
throw new Error("Extension controller was not provided to AuthServiceMock.getInstance")
|
||||
}
|
||||
AuthServiceMock.instance = new AuthServiceMock(controller, config || {}, authProvider)
|
||||
}
|
||||
if (controller !== undefined) {
|
||||
AuthServiceMock.instance.controller = controller
|
||||
}
|
||||
return AuthServiceMock.instance
|
||||
}
|
||||
|
||||
override async getAuthToken(): Promise<string | null> {
|
||||
if (!this._clineAuthInfo) {
|
||||
return null
|
||||
}
|
||||
return this._clineAuthInfo.idToken
|
||||
}
|
||||
|
||||
override async createAuthRequest(): Promise<String> {
|
||||
// Use URL object for more graceful query construction
|
||||
const authUrl = new URL(clineEnvConfig.apiBaseUrl)
|
||||
const authUrlString = authUrl.toString()
|
||||
// Call the parent implementation
|
||||
if (this._authenticated && this._clineAuthInfo) {
|
||||
console.log("Already authenticated with mock server")
|
||||
return String.create({ value: authUrlString })
|
||||
}
|
||||
|
||||
try {
|
||||
// Fetch user data from mock server
|
||||
const meUri = new URL("/api/v1/users/me", clineEnvConfig.apiBaseUrl)
|
||||
const tokenType = "personal"
|
||||
const testToken = `test-${tokenType}-token`
|
||||
const response = await fetch(meUri, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${testToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Mock server authentication failed: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const responseData = await response.json()
|
||||
|
||||
if (!responseData.success || !responseData.data) {
|
||||
throw new Error("Invalid response from mock server")
|
||||
}
|
||||
|
||||
const userData = responseData.data as UserResponse
|
||||
|
||||
// Convert UserResponse to ClineAuthInfo format
|
||||
this._clineAuthInfo = {
|
||||
idToken: testToken,
|
||||
userInfo: {
|
||||
id: userData.id,
|
||||
email: userData.email,
|
||||
displayName: userData.displayName,
|
||||
createdAt: userData.createdAt,
|
||||
organizations: userData.organizations.map((org) => ({
|
||||
active: org.active,
|
||||
memberId: org.memberId,
|
||||
name: org.name,
|
||||
organizationId: org.organizationId,
|
||||
roles: org.roles,
|
||||
})),
|
||||
},
|
||||
}
|
||||
|
||||
console.log(`Successfully authenticated with mock server as ${userData.displayName} (${userData.email})`)
|
||||
|
||||
const visibleWebview = WebviewProvider.getVisibleInstance()
|
||||
await visibleWebview?.controller.handleAuthCallback(testToken, "mock")
|
||||
} catch (error) {
|
||||
console.error("Error signing in with mock server:", error)
|
||||
this._authenticated = false
|
||||
this._clineAuthInfo = null
|
||||
throw error
|
||||
}
|
||||
|
||||
return String.create({ value: authUrlString })
|
||||
}
|
||||
|
||||
override async handleAuthCallback(_token: string, _provider: string): Promise<void> {
|
||||
try {
|
||||
this._authenticated = true
|
||||
await this.sendAuthStatusUpdate()
|
||||
} catch (error) {
|
||||
console.error("Error signing in with custom token:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
override async restoreRefreshTokenAndRetrieveAuthInfo(): Promise<void> {
|
||||
try {
|
||||
if (this._clineAuthInfo) {
|
||||
this._authenticated = true
|
||||
await this.sendAuthStatusUpdate()
|
||||
} else {
|
||||
console.warn("No user found after restoring auth token")
|
||||
this._authenticated = false
|
||||
this._clineAuthInfo = null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error restoring auth token:", error)
|
||||
this._authenticated = false
|
||||
this._clineAuthInfo = null
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { ExtensionContext } from "vscode"
|
||||
import { ClineAccountUserInfo, ClineAuthInfo } from "../AuthService"
|
||||
import { jwtDecode } from "jwt-decode"
|
||||
import { clineEnvConfig } from "@/config"
|
||||
import { Controller } from "@/core/controller"
|
||||
|
||||
export class FirebaseAuthProvider {
|
||||
private _config: any
|
||||
@@ -41,8 +42,8 @@ export class FirebaseAuthProvider {
|
||||
* @returns {Promise<User>} A promise that resolves with the authenticated user.
|
||||
* @throws {Error} Throws an error if the restoration fails.
|
||||
*/
|
||||
async retrieveClineAuthInfo(context: ExtensionContext): Promise<ClineAuthInfo | null> {
|
||||
const userRefreshToken = await getSecret(context, "clineAccountId")
|
||||
async retrieveClineAuthInfo(controller: Controller): Promise<ClineAuthInfo | null> {
|
||||
const userRefreshToken = controller.cacheService.getSecretKey("clineAccountId")
|
||||
if (!userRefreshToken) {
|
||||
console.error("No stored authentication credential found.")
|
||||
return null
|
||||
@@ -100,7 +101,7 @@ export class FirebaseAuthProvider {
|
||||
* @returns {Promise<User>} A promise that resolves with the authenticated user.
|
||||
* @throws {Error} Throws an error if the sign-in fails.
|
||||
*/
|
||||
async signIn(context: ExtensionContext, token: string, provider: string): Promise<ClineAuthInfo | null> {
|
||||
async signIn(controller: Controller, token: string, provider: string): Promise<ClineAuthInfo | null> {
|
||||
try {
|
||||
let credential
|
||||
switch (provider) {
|
||||
@@ -123,7 +124,7 @@ export class FirebaseAuthProvider {
|
||||
|
||||
// store the long-lived refresh token in secret storage
|
||||
try {
|
||||
await storeSecret(context, "clineAccountId", userCredential.refreshToken)
|
||||
controller.cacheService.setSecret("clineAccountId", userCredential.refreshToken)
|
||||
} catch (error) {
|
||||
ErrorService.logMessage("Firebase store token error", "error")
|
||||
ErrorService.logException(error)
|
||||
@@ -131,7 +132,7 @@ export class FirebaseAuthProvider {
|
||||
}
|
||||
|
||||
// userCredential = await this._signInWithCredential(context, credential)
|
||||
return await this.retrieveClineAuthInfo(context)
|
||||
return await this.retrieveClineAuthInfo(controller)
|
||||
} catch (error) {
|
||||
ErrorService.logMessage("Firebase sign-in error", "error")
|
||||
ErrorService.logException(error)
|
||||
|
||||
@@ -41,15 +41,17 @@ export class BrowserSession {
|
||||
private lastConnectionAttempt: number = 0
|
||||
browserSettings: BrowserSettings
|
||||
private isConnectedToRemote: boolean = false
|
||||
private useWebp: boolean
|
||||
|
||||
// Telemetry tracking properties
|
||||
private sessionStartTime: number = 0
|
||||
private browserActions: string[] = []
|
||||
private taskId?: string
|
||||
|
||||
constructor(context: vscode.ExtensionContext, browserSettings: BrowserSettings) {
|
||||
constructor(context: vscode.ExtensionContext, browserSettings: BrowserSettings, useWebp: boolean = true) {
|
||||
this.context = context
|
||||
this.browserSettings = browserSettings
|
||||
this.useWebp = useWebp
|
||||
}
|
||||
|
||||
// Tests remote browser connection
|
||||
@@ -487,14 +489,16 @@ export class BrowserSession {
|
||||
// },
|
||||
}
|
||||
|
||||
const screenshotType = this.useWebp ? "webp" : "png"
|
||||
let screenshotBase64 = await this.page.screenshot({
|
||||
...options,
|
||||
type: "webp",
|
||||
type: screenshotType,
|
||||
})
|
||||
let screenshot = `data:image/webp;base64,${screenshotBase64}`
|
||||
let screenshot = `data:image/${screenshotType};base64,${screenshotBase64}`
|
||||
|
||||
if (!screenshotBase64) {
|
||||
console.info("webp screenshot failed, trying png")
|
||||
// choosing to try screenshot again, regardless of the initial type
|
||||
console.info(`${screenshotType} screenshot failed, trying png`)
|
||||
screenshotBase64 = await this.page.screenshot({
|
||||
...options,
|
||||
type: "png",
|
||||
|
||||
@@ -18,7 +18,6 @@ const DEFAULT_IGNORE_DIRECTORIES = [
|
||||
"tmp",
|
||||
"temp",
|
||||
"deps",
|
||||
"pkg",
|
||||
"Pods",
|
||||
]
|
||||
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
import type { OutputChannel } from "vscode"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { ErrorService } from "../error/ErrorService"
|
||||
|
||||
/**
|
||||
* Simple logging utility for the extension's backend code.
|
||||
* Uses VS Code's OutputChannel which must be initialized from extension.ts
|
||||
* to ensure proper registration with the extension context.
|
||||
*/
|
||||
export class Logger {
|
||||
private static outputChannel: OutputChannel
|
||||
|
||||
static initialize(outputChannel: OutputChannel) {
|
||||
Logger.outputChannel = outputChannel
|
||||
}
|
||||
|
||||
static error(message: string, error?: Error) {
|
||||
Logger.#output("ERROR", message, error)
|
||||
ErrorService.logMessage(message, "error")
|
||||
@@ -34,24 +26,12 @@ export class Logger {
|
||||
static trace(message: string) {
|
||||
Logger.#output("TRACE", message)
|
||||
}
|
||||
static #timestamp() {
|
||||
const now = new Date()
|
||||
const year = now.getFullYear()
|
||||
const month = String(now.getMonth() + 1).padStart(2, "0")
|
||||
const day = String(now.getDate()).padStart(2, "0")
|
||||
const hour = String(now.getHours()).padStart(2, "0")
|
||||
const minute = String(now.getMinutes()).padStart(2, "0")
|
||||
const second = String(now.getSeconds()).padStart(2, "0")
|
||||
const timestamp = `${year}-${month}-${day}T${hour}:${minute}:${second}`
|
||||
return timestamp
|
||||
}
|
||||
static #output(level: string, message: string, error?: Error) {
|
||||
let fullMessage = message
|
||||
if (error?.message) {
|
||||
fullMessage += ` ${error.message}`
|
||||
}
|
||||
Logger.outputChannel.appendLine(`${level} ${fullMessage}`)
|
||||
console.log(`[${Logger.#timestamp()}] ${level} ${fullMessage}`)
|
||||
HostProvider.get().logToChannel(`${level} ${fullMessage}`)
|
||||
if (error?.stack) {
|
||||
console.log(`Stack trace:\n${error.stack}`)
|
||||
}
|
||||
|
||||
@@ -349,8 +349,8 @@ export class McpHub {
|
||||
|
||||
// Register notification handler for real-time messages
|
||||
console.log(`[MCP Debug] Setting up notification handlers for server: ${name}`)
|
||||
console.log(`[MCP Debug] Client instance:`, connection.client)
|
||||
console.log(`[MCP Debug] Transport type:`, config.type)
|
||||
//console.log(`[MCP Debug] Client instance:`, connection.client)
|
||||
//console.log(`[MCP Debug] Transport type:`, config.type)
|
||||
|
||||
// Try to set notification handler using the client's method
|
||||
try {
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { PostHog } from "posthog-node"
|
||||
import * as vscode from "vscode"
|
||||
import { version as extensionVersion } from "../../../../package.json"
|
||||
import { HostProvider } from "@hosts/host-provider"
|
||||
import { ShowMessageType } from "@shared/proto/host/window"
|
||||
|
||||
import type { TaskFeedbackType } from "@shared/WebviewMessage"
|
||||
import type { BrowserSettings } from "@shared/BrowserSettings"
|
||||
import { posthogClientProvider } from "../PostHogClientProvider"
|
||||
import { Mode } from "@/shared/ChatSettings"
|
||||
import { Mode } from "@/shared/storage/types"
|
||||
import { ClineAccountUserInfo } from "@/services/auth/AuthService"
|
||||
|
||||
/**
|
||||
* TelemetryService handles telemetry event tracking for the Cline extension
|
||||
@@ -134,13 +137,17 @@ class TelemetryService {
|
||||
} else {
|
||||
// Only show warning if user has opted in to Cline telemetry but VS Code telemetry is disabled
|
||||
if (didUserOptIn) {
|
||||
void vscode.window
|
||||
.showWarningMessage(
|
||||
"Anonymous Cline error and usage reporting is enabled, but VSCode telemetry is disabled. To enable error and usage reporting for this extension, enable VSCode telemetry in settings.",
|
||||
"Open Settings",
|
||||
)
|
||||
.then((selection) => {
|
||||
if (selection === "Open Settings") {
|
||||
void HostProvider.window
|
||||
.showMessage({
|
||||
type: ShowMessageType.WARNING,
|
||||
message:
|
||||
"Anonymous Cline error and usage reporting is enabled, but VSCode telemetry is disabled. To enable error and usage reporting for this extension, enable VSCode telemetry in settings.",
|
||||
options: {
|
||||
items: ["Open Settings"],
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.selectedOption === "Open Settings") {
|
||||
void vscode.commands.executeCommand("workbench.action.openSettings", "telemetry.telemetryLevel")
|
||||
}
|
||||
})
|
||||
@@ -211,6 +218,31 @@ class TelemetryService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the accounts user
|
||||
* @param userInfo The user's information
|
||||
*/
|
||||
public identifyAccount(userInfo: ClineAccountUserInfo) {
|
||||
if (!this.telemetryEnabled) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.client) {
|
||||
console.warn("Telemetry client is not initialized. Skipping identifyAccount.")
|
||||
return
|
||||
}
|
||||
|
||||
this.client.identify({
|
||||
distinctId: userInfo.id,
|
||||
properties: {
|
||||
uuid: userInfo.id,
|
||||
email: userInfo.email,
|
||||
name: userInfo.displayName,
|
||||
...this.addProperties({}),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Task events
|
||||
/**
|
||||
* Records when a new task/conversation is started
|
||||
|
||||
@@ -5,21 +5,15 @@ import { execa } from "execa"
|
||||
import { Logger } from "@services/logging/Logger"
|
||||
import { WebviewProvider } from "@core/webview"
|
||||
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
|
||||
import { TaskServiceClient } from "webview-ui/src/services/grpc-client"
|
||||
import { validateWorkspacePath, initializeGitRepository, getFileChanges, calculateToolSuccessRate } from "./GitHelper"
|
||||
import {
|
||||
updateGlobalState,
|
||||
getAllExtensionState,
|
||||
updateApiConfiguration,
|
||||
storeSecret,
|
||||
updateWorkspaceState,
|
||||
} from "@core/storage/state"
|
||||
import { updateGlobalState, getAllExtensionState, storeSecret } from "@core/storage/state"
|
||||
import { ClineAsk, ExtensionMessage } from "@shared/ExtensionMessage"
|
||||
import { ApiProvider } from "@shared/api"
|
||||
import { HistoryItem } from "@shared/HistoryItem"
|
||||
import { getSavedClineMessages, getSavedApiConversationHistory } from "@core/storage/disk"
|
||||
import { AskResponseRequest } from "@shared/proto/cline/task"
|
||||
import { getCwd } from "@/utils/path"
|
||||
import { askResponse } from "@core/controller/task/askResponse"
|
||||
|
||||
/**
|
||||
* Creates a tracker to monitor tool calls and failures during task execution
|
||||
@@ -268,24 +262,27 @@ export function createTestServer(webviewProvider?: WebviewProvider): http.Server
|
||||
}
|
||||
|
||||
// Store the API key securely
|
||||
await storeSecret(visibleWebview.controller.context, "clineAccountId", apiKey)
|
||||
visibleWebview.controller.cacheService.setSecret("clineAccountId", apiKey)
|
||||
|
||||
// Update the API configuration
|
||||
await updateApiConfiguration(visibleWebview.controller.context, updatedConfig)
|
||||
visibleWebview.controller.cacheService.setApiConfiguration(updatedConfig)
|
||||
|
||||
// Update global state to use cline provider
|
||||
await updateGlobalState(visibleWebview.controller.context, "planModeApiProvider", "cline")
|
||||
await updateGlobalState(visibleWebview.controller.context, "actModeApiProvider", "cline")
|
||||
// Update cache service to use cline provider
|
||||
const currentConfig = visibleWebview.controller.cacheService.getApiConfiguration()
|
||||
visibleWebview.controller.cacheService.setApiConfiguration({
|
||||
...currentConfig,
|
||||
planModeApiProvider: "cline",
|
||||
actModeApiProvider: "cline",
|
||||
})
|
||||
|
||||
// Post state to webview to reflect changes
|
||||
await visibleWebview.controller.postStateToWebview()
|
||||
}
|
||||
|
||||
// Ensure we're in Act mode before initiating the task
|
||||
const { chatSettings } = await visibleWebview.controller.getStateToPostToWebview()
|
||||
if (chatSettings.mode === "plan") {
|
||||
const { mode } = await visibleWebview.controller.getStateToPostToWebview()
|
||||
if (mode === "plan") {
|
||||
// Switch to Act mode if currently in Plan mode
|
||||
await visibleWebview.controller.togglePlanActModeWithChatSettings({ mode: "act" })
|
||||
await visibleWebview.controller.togglePlanActMode("act")
|
||||
}
|
||||
|
||||
// Initialize tool call tracker
|
||||
@@ -612,7 +609,7 @@ async function autoRespondToAsk(webviewProvider: WebviewProvider, askType: Cline
|
||||
try {
|
||||
if (webviewProvider.controller) {
|
||||
Logger.log("Auto-toggling to Act mode from Plan mode")
|
||||
await webviewProvider.controller.togglePlanActModeWithChatSettings({ mode: "act" })
|
||||
await webviewProvider.controller.togglePlanActMode("act")
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.log(`Error toggling to Act mode: ${error}`)
|
||||
@@ -624,9 +621,10 @@ async function autoRespondToAsk(webviewProvider: WebviewProvider, askType: Cline
|
||||
// we use the default "yesButtonClicked" to approve the action
|
||||
}
|
||||
|
||||
// Send the response message
|
||||
// Send the response message using the backend controller method
|
||||
try {
|
||||
await TaskServiceClient.askResponse(
|
||||
await askResponse(
|
||||
webviewProvider.controller,
|
||||
AskResponseRequest.create({
|
||||
responseType,
|
||||
text: responseText,
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import * as vscode from "vscode"
|
||||
import { WebviewProvider } from "@/core/webview"
|
||||
|
||||
/**
|
||||
* Shared URI handler that processes both VSCode URI events and HTTP server callbacks
|
||||
*/
|
||||
export class SharedUriHandler {
|
||||
/**
|
||||
* Processes a URI and routes it to the appropriate handler
|
||||
* @param uri The URI to process (can be from VSCode or converted from HTTP)
|
||||
* @returns Promise<boolean> indicating success (true) or failure (false)
|
||||
*/
|
||||
public static async handleUri(uri: vscode.Uri): Promise<boolean> {
|
||||
console.log("SharedUriHandler: Processing URI:", {
|
||||
path: uri.path,
|
||||
query: uri.query,
|
||||
scheme: uri.scheme,
|
||||
})
|
||||
|
||||
const path = uri.path
|
||||
const query = new URLSearchParams(uri.query.replace(/\+/g, "%2B"))
|
||||
const visibleWebview = WebviewProvider.getVisibleInstance()
|
||||
|
||||
if (!visibleWebview) {
|
||||
console.warn("SharedUriHandler: No visible webview found")
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
switch (path) {
|
||||
case "/openrouter": {
|
||||
const code = query.get("code")
|
||||
if (code) {
|
||||
await visibleWebview.controller.handleOpenRouterCallback(code)
|
||||
return true
|
||||
}
|
||||
console.warn("SharedUriHandler: Missing code parameter for OpenRouter callback")
|
||||
return false
|
||||
}
|
||||
case "/auth": {
|
||||
console.log("SharedUriHandler: Auth callback received:", { path: uri.path, provider: query.get("provider") })
|
||||
|
||||
const token = query.get("idToken")
|
||||
const provider = query.get("provider")
|
||||
|
||||
if (token) {
|
||||
await visibleWebview.controller.handleAuthCallback(token, provider)
|
||||
return true
|
||||
}
|
||||
console.warn("SharedUriHandler: Missing idToken parameter for auth callback")
|
||||
return false
|
||||
}
|
||||
default:
|
||||
console.warn(`SharedUriHandler: Unknown path: ${path}`)
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("SharedUriHandler: Error processing URI:", error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an HTTP URL to a vscode.Uri for unified processing
|
||||
* @param httpUrl The HTTP URL to convert
|
||||
* @returns vscode.Uri representation of the URL
|
||||
*/
|
||||
public static convertHttpUrlToUri(httpUrl: string): vscode.Uri {
|
||||
return vscode.Uri.parse(httpUrl)
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
export type OpenAIReasoningEffort = "low" | "medium" | "high"
|
||||
|
||||
export type Mode = "plan" | "act"
|
||||
|
||||
export interface ChatSettings {
|
||||
mode: Mode
|
||||
preferredLanguage?: string
|
||||
openAIReasoningEffort?: OpenAIReasoningEffort
|
||||
}
|
||||
|
||||
export type PartialChatSettings = Partial<ChatSettings>
|
||||
|
||||
// Type for chat settings stored in workspace (excludes in-memory mode)
|
||||
export type StoredChatSettings = Omit<ChatSettings, "mode">
|
||||
|
||||
export const DEFAULT_CHAT_SETTINGS: ChatSettings = {
|
||||
mode: "act",
|
||||
preferredLanguage: "English",
|
||||
openAIReasoningEffort: "medium",
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
import { ApiConfiguration } from "./api"
|
||||
import { AutoApprovalSettings } from "./AutoApprovalSettings"
|
||||
import { BrowserSettings } from "./BrowserSettings"
|
||||
import { ChatSettings } from "./ChatSettings"
|
||||
import { Mode, OpenaiReasoningEffort } from "./storage/types"
|
||||
import { HistoryItem } from "./HistoryItem"
|
||||
import { TelemetrySetting } from "./TelemetrySetting"
|
||||
import { ClineRulesToggles } from "./cline-rules"
|
||||
@@ -12,14 +12,15 @@ import { McpDisplayMode } from "./McpDisplayMode"
|
||||
// webview will hold state
|
||||
export interface ExtensionMessage {
|
||||
type: "grpc_response" // New type for gRPC responses
|
||||
grpc_response?: GrpcResponse
|
||||
}
|
||||
|
||||
grpc_response?: {
|
||||
message?: any // JSON serialized protobuf message
|
||||
request_id: string // Same ID as the request
|
||||
error?: string // Optional error message
|
||||
is_streaming?: boolean // Whether this is part of a streaming response
|
||||
sequence_number?: number // For ordering chunks in streaming responses
|
||||
}
|
||||
export type GrpcResponse = {
|
||||
message?: any // JSON serialized protobuf message
|
||||
request_id: string // Same ID as the request
|
||||
error?: string // Optional error message
|
||||
is_streaming?: boolean // Whether this is part of a streaming response
|
||||
sequence_number?: number // For ordering chunks in streaming responses
|
||||
}
|
||||
|
||||
export type Platform = "aix" | "darwin" | "freebsd" | "linux" | "openbsd" | "sunos" | "win32" | "unknown"
|
||||
@@ -33,7 +34,9 @@ export interface ExtensionState {
|
||||
autoApprovalSettings: AutoApprovalSettings
|
||||
browserSettings: BrowserSettings
|
||||
remoteBrowserHost?: string
|
||||
chatSettings: ChatSettings
|
||||
preferredLanguage?: string
|
||||
openaiReasoningEffort?: OpenaiReasoningEffort
|
||||
mode: Mode
|
||||
checkpointTrackerErrorMessage?: string
|
||||
clineMessages: ClineMessage[]
|
||||
currentTaskItem?: HistoryItem
|
||||
@@ -60,6 +63,7 @@ export interface ExtensionState {
|
||||
localCursorRulesToggles: ClineRulesToggles
|
||||
localWindsurfRulesToggles: ClineRulesToggles
|
||||
mcpResponsesCollapsed?: boolean
|
||||
strictPlanModeEnabled?: boolean
|
||||
}
|
||||
|
||||
export interface ClineMessage {
|
||||
|
||||
@@ -1,73 +1,19 @@
|
||||
import { ApiConfiguration } from "./api"
|
||||
import { BrowserSettings } from "./BrowserSettings"
|
||||
import { ChatSettings } from "./ChatSettings"
|
||||
import { UserInfo } from "./UserInfo"
|
||||
import { ChatContent } from "./ChatContent"
|
||||
import { TelemetrySetting } from "./TelemetrySetting"
|
||||
import { McpViewTab } from "./mcp"
|
||||
|
||||
export interface WebviewMessage {
|
||||
type:
|
||||
| "requestVsCodeLmModels"
|
||||
| "fetchMcpMarketplace"
|
||||
| "searchCommits"
|
||||
| "telemetrySetting"
|
||||
| "grpc_request"
|
||||
| "grpc_request_cancel"
|
||||
type: "grpc_request" | "grpc_request_cancel"
|
||||
grpc_request?: GrpcRequest
|
||||
grpc_request_cancel?: GrpcCancel
|
||||
}
|
||||
|
||||
text?: string
|
||||
disabled?: boolean
|
||||
apiConfiguration?: ApiConfiguration
|
||||
images?: string[]
|
||||
files?: string[]
|
||||
bool?: boolean
|
||||
number?: number
|
||||
browserSettings?: BrowserSettings
|
||||
chatSettings?: ChatSettings
|
||||
chatContent?: ChatContent
|
||||
mcpId?: string
|
||||
timeout?: number
|
||||
tab?: McpViewTab
|
||||
// For toggleToolAutoApprove
|
||||
serverName?: string
|
||||
serverUrl?: string
|
||||
toolNames?: string[]
|
||||
autoApprove?: boolean
|
||||
export type GrpcRequest = {
|
||||
service: string
|
||||
method: string
|
||||
message: any // JSON serialized protobuf message
|
||||
request_id: string // For correlating requests and responses
|
||||
is_streaming: boolean // Whether this is a streaming request
|
||||
}
|
||||
|
||||
// For auth
|
||||
user?: UserInfo | null
|
||||
customToken?: string
|
||||
planActSeparateModelsSetting?: boolean
|
||||
enableCheckpointsSetting?: boolean
|
||||
mcpMarketplaceEnabled?: boolean
|
||||
mcpResponsesCollapsed?: boolean
|
||||
telemetrySetting?: TelemetrySetting
|
||||
mcpRichDisplayEnabled?: boolean
|
||||
mentionsRequestId?: string
|
||||
query?: string
|
||||
// For toggleFavoriteModel
|
||||
modelId?: string
|
||||
grpc_request?: {
|
||||
service: string
|
||||
method: string
|
||||
message: any // JSON serialized protobuf message
|
||||
request_id: string // For correlating requests and responses
|
||||
is_streaming?: boolean // Whether this is a streaming request
|
||||
}
|
||||
grpc_request_cancel?: {
|
||||
request_id: string // ID of the request to cancel
|
||||
}
|
||||
// For cline rules and workflows
|
||||
isGlobal?: boolean
|
||||
rulePath?: string
|
||||
workflowPath?: string
|
||||
enabled?: boolean
|
||||
filename?: string
|
||||
|
||||
offset?: number
|
||||
shellIntegrationTimeout?: number
|
||||
terminalReuseEnabled?: boolean
|
||||
defaultTerminalProfile?: string
|
||||
export type GrpcCancel = {
|
||||
request_id: string // ID of the request to cancel
|
||||
}
|
||||
|
||||
export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse"
|
||||
|
||||
@@ -47,6 +47,11 @@ describe("Mention Regex", () => {
|
||||
assertMatch(result)
|
||||
})
|
||||
})
|
||||
it("handles unquoted paths with spaces correctly", () => {
|
||||
// Should stop at the space
|
||||
const match = mentionRegex.exec("@/path with spaces/file.txt")
|
||||
expect(match?.[0]).to.equal("@/path")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Existing Functionality", () => {
|
||||
@@ -107,6 +112,7 @@ describe("Mention Regex", () => {
|
||||
["C:\\folder\\file.txt", null],
|
||||
["@", null],
|
||||
["@ C:\\file.txt", null],
|
||||
["@'/path/file.tar.gz'", null],
|
||||
]
|
||||
|
||||
cases.forEach(([input, expected]) => {
|
||||
@@ -188,4 +194,228 @@ describe("Mention Regex", () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
describe("Git Hash Edge Cases", () => {
|
||||
it("matches git hashes of various valid lengths", () => {
|
||||
const cases: Array<[string, string | null]> = [
|
||||
// Valid lengths (7-40 characters)
|
||||
["@abcdef1", "@abcdef1"], // 7 chars (minimum)
|
||||
["@abcdef12", "@abcdef12"], // 8 chars
|
||||
["@abcdef1234567890", "@abcdef1234567890"], // 16 chars
|
||||
["@abcdef1234567890abcdef1234567890abcdef12", "@abcdef1234567890abcdef1234567890abcdef12"], // 40 chars (maximum)
|
||||
|
||||
// Invalid lengths
|
||||
["@abcdef", null], // 6 chars (too short)
|
||||
["@abcdef1234567890abcdef1234567890abcdef123", null], // 41 chars (too long, but would match first 40)
|
||||
|
||||
// Invalid characters
|
||||
["@ghijklm", null], // Contains non-hex characters
|
||||
["@ABCDEF1", null], // Uppercase not allowed
|
||||
]
|
||||
|
||||
cases.forEach(([input, expected]) => {
|
||||
const match = mentionRegex.exec(input)
|
||||
const actual = match ? match[0] : null
|
||||
if (expected && expected.includes("41 chars")) {
|
||||
// Special case: should match first 40 chars
|
||||
expect(actual).to.equal("@abcdef1234567890abcdef1234567890abcdef12")
|
||||
} else {
|
||||
expect(actual).to.equal(expected)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("Punctuation at Boundaries", () => {
|
||||
it("excludes all types of trailing punctuation", () => {
|
||||
const cases: Array<[string, string]> = [
|
||||
["@/path/file.txt.", "@/path/file.txt"],
|
||||
["@problems:", "@problems"],
|
||||
["@terminal;", "@terminal"],
|
||||
["@/path/file.txt!", "@/path/file.txt"],
|
||||
["@/path/file.txt?", "@/path/file.txt"],
|
||||
["@git-changes,", "@git-changes"],
|
||||
]
|
||||
|
||||
cases.forEach(([input, expected]) => {
|
||||
const match = mentionRegex.exec(input)
|
||||
expect(match?.[0]).to.equal(expected)
|
||||
})
|
||||
})
|
||||
|
||||
it("handles multiple punctuation marks", () => {
|
||||
const cases: Array<[string, string]> = [
|
||||
["@/path/file.txt!?", "@/path/file.txt"],
|
||||
["@problems...", "@problems"],
|
||||
["@terminal!!", "@terminal"],
|
||||
]
|
||||
|
||||
cases.forEach(([input, expected]) => {
|
||||
const match = mentionRegex.exec(input)
|
||||
expect(match?.[0]).to.equal(expected)
|
||||
})
|
||||
})
|
||||
|
||||
it("doesn't match trailing punctuation in context", () => {
|
||||
const cases: Array<[string, string[]]> = [
|
||||
["Check the file at @/C:\\folder\\file.txt! for details.", ["@/C:\\folder\\file.txt"]],
|
||||
["Review @problems, and @git-changes.", ["@problems", "@git-changes"]],
|
||||
["Multiple: @/file1.txt, and @/C:\\file2.txt; and @terminal?", ["@/file1.txt", "@/C:\\file2.txt", "@terminal"]],
|
||||
]
|
||||
|
||||
cases.forEach(([input, expected]) => {
|
||||
const matches = input.match(mentionRegexGlobal)
|
||||
expect(matches).deep.eq(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("URL Protocol Variations", () => {
|
||||
it("matches various URL protocols", () => {
|
||||
const cases: Array<[string, string]> = [
|
||||
["@file://localhost/path/to/file", "@file://localhost/path/to/file"],
|
||||
["@custom://app/resource", "@custom://app/resource"],
|
||||
["@app://settings", "@app://settings"],
|
||||
["@ssh://git@github.com/repo", "@ssh://git@github.com/repo"],
|
||||
]
|
||||
|
||||
cases.forEach(([input, expected]) => {
|
||||
const match = mentionRegex.exec(input)
|
||||
expect(match?.[0]).to.equal(expected)
|
||||
})
|
||||
})
|
||||
|
||||
it("matches URLs with complex structures", () => {
|
||||
const cases: Array<[string, string]> = [
|
||||
["@https://example.com?q=test&p=1", "@https://example.com?q=test&p=1"],
|
||||
["@https://example.com#section", "@https://example.com#section"],
|
||||
["@http://localhost:3000", "@http://localhost:3000"],
|
||||
["@https://user:pass@example.com", "@https://user:pass@example.com"],
|
||||
["@https://example.com/", "@https://example.com/"],
|
||||
]
|
||||
|
||||
cases.forEach(([input, expected]) => {
|
||||
const match = mentionRegex.exec(input)
|
||||
expect(match?.[0]).to.equal(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("End of String Handling", () => {
|
||||
it("matches mentions at end of string", () => {
|
||||
const cases: Array<[string, string]> = [
|
||||
["Check @/path/file.txt", "@/path/file.txt"],
|
||||
["Review @problems", "@problems"],
|
||||
["Open @terminal", "@terminal"],
|
||||
["See @git-changes", "@git-changes"],
|
||||
]
|
||||
|
||||
cases.forEach(([input, expected]) => {
|
||||
const match = mentionRegex.exec(input)
|
||||
expect(match?.[0]).to.equal(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("Complex Real-World Scenarios", () => {
|
||||
it("handles mentions in markdown-like text", () => {
|
||||
const text = "See @/docs/README.md, check @problems, and visit @https://example.com."
|
||||
const matches = text.match(mentionRegexGlobal)
|
||||
expect(matches).to.deep.equal(["@/docs/README.md", "@problems", "@https://example.com"])
|
||||
})
|
||||
|
||||
it("handles mentions in code comments", () => {
|
||||
const text = "// TODO: Fix @problems in @/src/index.js (see @git-changes)"
|
||||
const matches = text.match(mentionRegexGlobal)
|
||||
expect(matches).to.deep.equal(["@problems", "@/src/index.js", "@git-changes"])
|
||||
})
|
||||
})
|
||||
describe("Quoted file paths", () => {
|
||||
it("handles quoted paths correctly", () => {
|
||||
const cases: Array<[string, string]> = [
|
||||
['@"/path with space.txt"', '@"/path with space.txt"'],
|
||||
['@"/path/ends/with-space "', '@"/path/ends/with-space "'],
|
||||
['@"/ path-starts-with-space.txt"', '@"/ path-starts-with-space.txt"'],
|
||||
['@"/path with space.txt!"', '@"/path with space.txt!"'],
|
||||
['@"/path with space.txt!"!', '@"/path with space.txt!"'],
|
||||
]
|
||||
|
||||
cases.forEach(([input, expected]) => {
|
||||
const match = mentionRegex.exec(input)
|
||||
expect(match?.[0]).to.equal(expected)
|
||||
})
|
||||
})
|
||||
it("handles quotes inside file paths correctly", () => {
|
||||
const cases: Array<[string, string]> = [
|
||||
['@/"path/file.txt', '@/"path/file.txt'],
|
||||
['@/path"/file".tar.gz', '@/path"/file".tar.gz'],
|
||||
]
|
||||
|
||||
cases.forEach(([input, expected]) => {
|
||||
const match = mentionRegex.exec(input)
|
||||
expect(match?.[0]).to.equal(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
describe("Path Edge Cases", () => {
|
||||
it("matches various path structures", () => {
|
||||
const cases: Array<[string, string]> = [
|
||||
["@/", "@/"], // root directory
|
||||
['@"/"', '@"/"'], // quoted root directory
|
||||
["@/path/to/.hidden/file", "@/path/to/.hidden/file"],
|
||||
["@/path/file...txt", "@/path/file...txt"],
|
||||
["@/path/file.tar.gz", "@/path/file.tar.gz"],
|
||||
]
|
||||
|
||||
cases.forEach(([input, expected]) => {
|
||||
const match = mentionRegex.exec(input)
|
||||
expect(match?.[0]).to.equal(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
describe("Whitespace Handling", () => {
|
||||
it("stops at various whitespace characters", () => {
|
||||
const cases: Array<[string, string]> = [
|
||||
["@/path/file.txt\trest", "@/path/file.txt"],
|
||||
["@/path/file.txt\nrest", "@/path/file.txt"],
|
||||
["@/path/file.txt\rrest", "@/path/file.txt"],
|
||||
["@/path/file.txt rest", "@/path/file.txt"],
|
||||
]
|
||||
|
||||
cases.forEach(([input, expected]) => {
|
||||
const match = mentionRegex.exec(input)
|
||||
expect(match?.[0]).to.equal(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("Keyword Boundaries", () => {
|
||||
it("only matches exact keywords", () => {
|
||||
const cases: Array<[string, string | null]> = [
|
||||
["@problemsolver", null], // Should not match
|
||||
["@terminals", null], // Should not match
|
||||
["@git-changeset", null], // Should not match
|
||||
["@problem", null], // Should not match
|
||||
["@git-change", null], // Should not match
|
||||
]
|
||||
|
||||
cases.forEach(([input, expected]) => {
|
||||
const match = mentionRegex.exec(input)
|
||||
const actual = match ? match[0] : null
|
||||
expect(actual).to.equal(expected)
|
||||
})
|
||||
})
|
||||
|
||||
it("matches keywords with trailing punctuation", () => {
|
||||
const cases: Array<[string, string]> = [
|
||||
["@problems!", "@problems"],
|
||||
["@terminal.", "@terminal"],
|
||||
["@git-changes,", "@git-changes"],
|
||||
]
|
||||
|
||||
cases.forEach(([input, expected]) => {
|
||||
const match = mentionRegex.exec(input)
|
||||
expect(match?.[0]).to.equal(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
+111
-4
@@ -30,6 +30,7 @@ export type ApiProvider =
|
||||
| "sapaicore"
|
||||
| "groq"
|
||||
| "huggingface"
|
||||
| "huawei-cloud-maas"
|
||||
|
||||
export interface ApiHandlerOptions {
|
||||
// Global configuration (not mode-specific)
|
||||
@@ -92,6 +93,7 @@ export interface ApiHandlerOptions {
|
||||
sapAiResourceGroup?: string
|
||||
sapAiCoreTokenUrl?: string
|
||||
sapAiCoreBaseUrl?: string
|
||||
huaweiCloudMaasApiKey?: string
|
||||
onRetryAttempt?: (attempt: number, maxRetries: number, delay: number, error: any) => void
|
||||
// Plan mode configurations
|
||||
planModeApiModelId?: string
|
||||
@@ -117,6 +119,8 @@ export interface ApiHandlerOptions {
|
||||
planModeGroqModelInfo?: ModelInfo
|
||||
planModeHuggingFaceModelId?: string
|
||||
planModeHuggingFaceModelInfo?: ModelInfo
|
||||
planModeHuaweiCloudMaasModelId?: string
|
||||
planModeHuaweiCloudMaasModelInfo?: ModelInfo
|
||||
// Act mode configurations
|
||||
|
||||
actModeApiModelId?: string
|
||||
@@ -142,6 +146,8 @@ export interface ApiHandlerOptions {
|
||||
actModeGroqModelInfo?: ModelInfo
|
||||
actModeHuggingFaceModelId?: string
|
||||
actModeHuggingFaceModelInfo?: ModelInfo
|
||||
actModeHuaweiCloudMaasModelId?: string
|
||||
actModeHuaweiCloudMaasModelInfo?: ModelInfo
|
||||
}
|
||||
|
||||
export type ApiConfiguration = ApiHandlerOptions & {
|
||||
@@ -2474,8 +2480,37 @@ export const sambanovaModels = {
|
||||
// Cerebras
|
||||
// https://inference-docs.cerebras.ai/api-reference/models
|
||||
export type CerebrasModelId = keyof typeof cerebrasModels
|
||||
export const cerebrasDefaultModelId: CerebrasModelId = "qwen-3-32b"
|
||||
export const cerebrasDefaultModelId: CerebrasModelId = "qwen-3-coder-480b-free"
|
||||
export const cerebrasModels = {
|
||||
"qwen-3-coder-480b-free": {
|
||||
maxTokens: 40000,
|
||||
contextWindow: 64000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description:
|
||||
"SOTA coding model with ~2000 tokens/s ($0 free tier)\n\n• Use this if you don't have a Cerebras subscription\n• 64K context window\n• Rate limits: 150K TPM, 1M TPH/TPD, 10 RPM, 100 RPH/RPD\n\nUpgrade for higher limits: [https://cloud.cerebras.ai/?utm=cline](https://cloud.cerebras.ai/?utm=cline)",
|
||||
},
|
||||
"qwen-3-coder-480b": {
|
||||
maxTokens: 40000,
|
||||
contextWindow: 128000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description:
|
||||
"SOTA coding model with ~2000 tokens/s ($50/$250 paid tiers)\n\n• Use this if you have a Cerebras subscription\n• 131K context window with higher rate limits",
|
||||
},
|
||||
"qwen-3-235b-a22b-instruct-2507": {
|
||||
maxTokens: 64000,
|
||||
contextWindow: 64000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "Intelligent model with ~1400 tokens/s",
|
||||
},
|
||||
"llama-3.3-70b": {
|
||||
maxTokens: 64000,
|
||||
contextWindow: 64000,
|
||||
@@ -2494,9 +2529,9 @@ export const cerebrasModels = {
|
||||
outputPrice: 0,
|
||||
description: "SOTA coding performance with ~2500 tokens/s",
|
||||
},
|
||||
"qwen-3-235b-a22b": {
|
||||
maxTokens: 40000,
|
||||
contextWindow: 40000,
|
||||
"qwen-3-235b-a22b-thinking-2507": {
|
||||
maxTokens: 32000,
|
||||
contextWindow: 65000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
@@ -2777,3 +2812,75 @@ export const moonshotModels = {
|
||||
} as const satisfies Record<string, ModelInfo>
|
||||
export type MoonshotModelId = keyof typeof moonshotModels
|
||||
export const moonshotDefaultModelId = "kimi-k2-0711-preview" satisfies MoonshotModelId
|
||||
|
||||
// Huawei Cloud MaaS
|
||||
export type HuaweiCloudMaasModelId = keyof typeof huaweiCloudMaasModels
|
||||
export const huaweiCloudMaasDefaultModelId: HuaweiCloudMaasModelId = "DeepSeek-V3"
|
||||
export const huaweiCloudMaasModels = {
|
||||
"DeepSeek-V3": {
|
||||
maxTokens: 16_384,
|
||||
contextWindow: 64_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.27,
|
||||
outputPrice: 1.1,
|
||||
cacheWritesPrice: 0,
|
||||
cacheReadsPrice: 0,
|
||||
},
|
||||
"DeepSeek-R1": {
|
||||
maxTokens: 16_384,
|
||||
contextWindow: 64_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.55,
|
||||
outputPrice: 2.2,
|
||||
cacheWritesPrice: 0,
|
||||
cacheReadsPrice: 0,
|
||||
thinkingConfig: {
|
||||
maxBudget: 8192,
|
||||
outputPrice: 2.2,
|
||||
},
|
||||
},
|
||||
"deepseek-r1-250528": {
|
||||
maxTokens: 16_384,
|
||||
contextWindow: 64_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.55,
|
||||
outputPrice: 2.2,
|
||||
cacheWritesPrice: 0,
|
||||
cacheReadsPrice: 0,
|
||||
thinkingConfig: {
|
||||
maxBudget: 8192,
|
||||
outputPrice: 2.2,
|
||||
},
|
||||
},
|
||||
"qwen3-235b-a22b": {
|
||||
maxTokens: 8_192,
|
||||
contextWindow: 32_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.27,
|
||||
outputPrice: 1.1,
|
||||
cacheWritesPrice: 0,
|
||||
cacheReadsPrice: 0,
|
||||
thinkingConfig: {
|
||||
maxBudget: 4096,
|
||||
outputPrice: 1.1,
|
||||
},
|
||||
},
|
||||
"qwen3-32b": {
|
||||
maxTokens: 8_192,
|
||||
contextWindow: 32_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.27,
|
||||
outputPrice: 1.1,
|
||||
cacheWritesPrice: 0,
|
||||
cacheReadsPrice: 0,
|
||||
thinkingConfig: {
|
||||
maxBudget: 4096,
|
||||
outputPrice: 1.1,
|
||||
},
|
||||
},
|
||||
} as const satisfies Record<string, ModelInfo>
|
||||
|
||||
@@ -29,10 +29,10 @@ Mention regex:
|
||||
- **Exact Word ('terminal')**: Matches the exact word 'terminal'.
|
||||
- **Word Boundary (`\b`)**: Ensures that 'terminal' is matched as a whole word and not as part of another word (e.g., 'terminals').
|
||||
|
||||
- `(?=[.,;:!?]?(?=[\s\r\n]|$))`:
|
||||
- `(?=[.,;:!?()]*(?=[\s\r\n]|$))`:
|
||||
- **Positive Lookahead (`(?=...)`)**: Ensures that the match is followed by specific patterns without including them in the match.
|
||||
- `[.,;:!?]?`:
|
||||
- **Optional Punctuation (`[.,;:!?]?`)**: Matches zero or one of the specified punctuation marks.
|
||||
- `[.,;:!?()]*`:
|
||||
- **Optional Punctuation (`[.,;:!?()]*`)**: Matches zero or more of the specified punctuation marks (including parentheses).
|
||||
- `(?=[\s\r\n]|$)`:
|
||||
- **Nested Positive Lookahead (`(?=[\s\r\n]|$)`)**: Ensures that the punctuation (if present) is followed by a whitespace character, a line break, or the end of the string.
|
||||
|
||||
@@ -49,6 +49,16 @@ Mention regex:
|
||||
- `mentionRegexGlobal`: Creates a global version of the `mentionRegex` to find all matches within a given string.
|
||||
|
||||
*/
|
||||
export const mentionRegex =
|
||||
/@((?:\/|\w+:\/\/)[^\s]+?|[a-f0-9]{7,40}\b|problems\b|terminal\b|git-changes\b)(?=[.,;:!?]?(?=[\s\r\n]|$))/
|
||||
export const mentionRegex = new RegExp(
|
||||
`@(` +
|
||||
`/[^\\s]*?` + // Simple file paths (can't contain)
|
||||
`|"\\/[^"]*?"` + // Quoted file paths which can contain spaces
|
||||
`|(?:\\w+:\\/\\/)[^\\s]+?` + // URLs
|
||||
`|[a-f0-9]{7,40}\\b` + // Git commit hashes
|
||||
`|problems\\b` + // Exact word 'problems'
|
||||
`|terminal\\b` + // Exact word 'terminal'
|
||||
`|git-changes\\b` + // Exact word 'git-changes'
|
||||
`)` +
|
||||
`(?=[.,;:!?()]*(?=[\\s\\r\\n]|$))`, // Lookahead for trailing punctuation (multiple allowed)
|
||||
)
|
||||
export const mentionRegexGlobal = new RegExp(mentionRegex.source, "g")
|
||||
|
||||
@@ -244,6 +244,8 @@ function convertApiProviderToProto(provider: string | undefined): ProtoApiProvid
|
||||
return ProtoApiProvider.SAPAICORE
|
||||
case "claude-code":
|
||||
return ProtoApiProvider.CLAUDE_CODE
|
||||
case "huawei-cloud-maas":
|
||||
return ProtoApiProvider.HUAWEI_CLOUD_MAAS
|
||||
default:
|
||||
return ProtoApiProvider.ANTHROPIC
|
||||
}
|
||||
@@ -310,6 +312,8 @@ function convertProtoToApiProvider(provider: ProtoApiProvider): ApiProvider {
|
||||
return "sapaicore"
|
||||
case ProtoApiProvider.CLAUDE_CODE:
|
||||
return "claude-code"
|
||||
case ProtoApiProvider.HUAWEI_CLOUD_MAAS:
|
||||
return "huawei-cloud-maas"
|
||||
default:
|
||||
return "anthropic"
|
||||
}
|
||||
@@ -378,6 +382,7 @@ export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoA
|
||||
sapAiResourceGroup: config.sapAiResourceGroup,
|
||||
sapAiCoreTokenUrl: config.sapAiCoreTokenUrl,
|
||||
sapAiCoreBaseUrl: config.sapAiCoreBaseUrl,
|
||||
huaweiCloudMaasApiKey: config.huaweiCloudMaasApiKey,
|
||||
|
||||
// Plan mode configurations
|
||||
planModeApiProvider: config.planModeApiProvider ? convertApiProviderToProto(config.planModeApiProvider) : undefined,
|
||||
@@ -404,6 +409,8 @@ export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoA
|
||||
planModeHuggingFaceModelId: config.planModeHuggingFaceModelId,
|
||||
planModeHuggingFaceModelInfo: convertModelInfoToProtoOpenRouter(config.planModeHuggingFaceModelInfo),
|
||||
planModeSapAiCoreModelId: config.planModeSapAiCoreModelId,
|
||||
planModeHuaweiCloudMaasModelId: config.planModeHuaweiCloudMaasModelId,
|
||||
planModeHuaweiCloudMaasModelInfo: convertModelInfoToProtoOpenRouter(config.planModeHuaweiCloudMaasModelInfo),
|
||||
|
||||
// Act mode configurations
|
||||
actModeApiProvider: config.actModeApiProvider ? convertApiProviderToProto(config.actModeApiProvider) : undefined,
|
||||
@@ -430,6 +437,8 @@ export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoA
|
||||
actModeHuggingFaceModelId: config.actModeHuggingFaceModelId,
|
||||
actModeHuggingFaceModelInfo: convertModelInfoToProtoOpenRouter(config.actModeHuggingFaceModelInfo),
|
||||
actModeSapAiCoreModelId: config.actModeSapAiCoreModelId,
|
||||
actModeHuaweiCloudMaasModelId: config.actModeHuaweiCloudMaasModelId,
|
||||
actModeHuaweiCloudMaasModelInfo: convertModelInfoToProtoOpenRouter(config.actModeHuaweiCloudMaasModelInfo),
|
||||
|
||||
// Favorited model IDs
|
||||
favoritedModelIds: config.favoritedModelIds || [],
|
||||
@@ -499,6 +508,7 @@ export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguratio
|
||||
sapAiResourceGroup: protoConfig.sapAiResourceGroup,
|
||||
sapAiCoreTokenUrl: protoConfig.sapAiCoreTokenUrl,
|
||||
sapAiCoreBaseUrl: protoConfig.sapAiCoreBaseUrl,
|
||||
huaweiCloudMaasApiKey: protoConfig.huaweiCloudMaasApiKey,
|
||||
|
||||
// Plan mode configurations
|
||||
planModeApiProvider:
|
||||
@@ -528,6 +538,8 @@ export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguratio
|
||||
planModeHuggingFaceModelId: protoConfig.planModeHuggingFaceModelId,
|
||||
planModeHuggingFaceModelInfo: convertProtoToModelInfo(protoConfig.planModeHuggingFaceModelInfo),
|
||||
planModeSapAiCoreModelId: protoConfig.planModeSapAiCoreModelId,
|
||||
planModeHuaweiCloudMaasModelId: protoConfig.planModeHuaweiCloudMaasModelId,
|
||||
planModeHuaweiCloudMaasModelInfo: convertProtoToModelInfo(protoConfig.planModeHuaweiCloudMaasModelInfo),
|
||||
|
||||
// Act mode configurations
|
||||
actModeApiProvider:
|
||||
@@ -555,6 +567,8 @@ export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguratio
|
||||
actModeHuggingFaceModelId: protoConfig.actModeHuggingFaceModelId,
|
||||
actModeHuggingFaceModelInfo: convertProtoToModelInfo(protoConfig.actModeHuggingFaceModelInfo),
|
||||
actModeSapAiCoreModelId: protoConfig.actModeSapAiCoreModelId,
|
||||
actModeHuaweiCloudMaasModelId: protoConfig.actModeHuaweiCloudMaasModelId,
|
||||
actModeHuaweiCloudMaasModelInfo: convertProtoToModelInfo(protoConfig.actModeHuaweiCloudMaasModelInfo),
|
||||
|
||||
// Favorited model IDs
|
||||
favoritedModelIds:
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { ChatContent } from "@shared/ChatContent"
|
||||
import { ChatSettings } from "@shared/ChatSettings"
|
||||
import { ChatContent as ProtoChatContent, ChatSettings as ProtoChatSettings, PlanActMode } from "@shared/proto/cline/state"
|
||||
|
||||
/**
|
||||
* Converts domain ChatSettings objects to proto ChatSettings objects
|
||||
*/
|
||||
export function convertChatSettingsToProtoChatSettings(chatSettings: ChatSettings): ProtoChatSettings {
|
||||
return ProtoChatSettings.create({
|
||||
mode: chatSettings.mode === "plan" ? PlanActMode.PLAN : PlanActMode.ACT,
|
||||
preferredLanguage: chatSettings.preferredLanguage,
|
||||
openAiReasoningEffort: chatSettings.openAIReasoningEffort,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts proto ChatSettings objects to domain ChatSettings objects
|
||||
*/
|
||||
export function convertProtoChatSettingsToChatSettings(protoChatSettings: ProtoChatSettings): ChatSettings {
|
||||
return {
|
||||
mode: protoChatSettings.mode === PlanActMode.PLAN ? "plan" : "act",
|
||||
preferredLanguage: protoChatSettings.preferredLanguage,
|
||||
openAIReasoningEffort: protoChatSettings.openAiReasoningEffort as "low" | "medium" | "high" | undefined,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts domain ChatContent objects to proto ChatContent objects
|
||||
*/
|
||||
export function convertChatContentToProtoChatContent(chatContent?: ChatContent): ProtoChatContent | undefined {
|
||||
if (!chatContent) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return ProtoChatContent.create({
|
||||
message: chatContent.message,
|
||||
images: chatContent.images || [],
|
||||
files: chatContent.files || [],
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts proto ChatContent objects to domain ChatContent objects
|
||||
*/
|
||||
export function convertProtoChatContentToChatContent(protoChatContent?: ProtoChatContent): ChatContent | undefined {
|
||||
if (!protoChatContent) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
message: protoChatContent.message,
|
||||
images: protoChatContent.images || [],
|
||||
files: protoChatContent.files || [],
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,5 @@
|
||||
import { ApiConfiguration, ApiProvider, BedrockModelId } from "@shared/api"
|
||||
import { ChatSettings } from "@shared/ChatSettings"
|
||||
import {
|
||||
ApiConfiguration as ProtoApiConfiguration,
|
||||
ChatSettings as ProtoChatSettings,
|
||||
PlanActMode,
|
||||
} from "@shared/proto/cline/state"
|
||||
import { ApiConfiguration as ProtoApiConfiguration } from "@shared/proto/cline/state"
|
||||
|
||||
/**
|
||||
* Converts domain ApiConfiguration objects to proto ApiConfiguration objects
|
||||
@@ -279,25 +274,3 @@ export function convertProtoApiConfigurationToApiConfiguration(protoConfig: Prot
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts domain ChatSettings objects to proto ChatSettings objects
|
||||
*/
|
||||
export function convertChatSettingsToProtoChatSettings(chatSettings: ChatSettings): ProtoChatSettings {
|
||||
return ProtoChatSettings.create({
|
||||
mode: chatSettings.mode === "plan" ? PlanActMode.PLAN : PlanActMode.ACT,
|
||||
preferredLanguage: chatSettings.preferredLanguage,
|
||||
openAiReasoningEffort: chatSettings.openAIReasoningEffort,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts proto ChatSettings objects to domain ChatSettings objects
|
||||
*/
|
||||
export function convertProtoChatSettingsToChatSettings(protoChatSettings: ProtoChatSettings): ChatSettings {
|
||||
return {
|
||||
mode: protoChatSettings.mode === PlanActMode.PLAN ? "plan" : "act",
|
||||
preferredLanguage: protoChatSettings.preferredLanguage,
|
||||
openAIReasoningEffort: protoChatSettings.openAiReasoningEffort as "low" | "medium" | "high" | undefined,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export type OpenaiReasoningEffort = "low" | "medium" | "high"
|
||||
|
||||
export type Mode = "plan" | "act"
|
||||
@@ -1,5 +1,6 @@
|
||||
import { activate } from "@/extension"
|
||||
import { Controller } from "@core/controller"
|
||||
import { CacheService } from "@core/storage/CacheService"
|
||||
import { ExternalDiffViewProvider } from "@hosts/external/ExternalDiffviewProvider"
|
||||
import { ExternalWebviewProvider } from "@hosts/external/ExternalWebviewProvider"
|
||||
import { ExternalHostBridgeClientManager } from "@hosts/external/host-bridge-client-manager"
|
||||
@@ -7,26 +8,42 @@ import { HostProvider } from "@/hosts/host-provider"
|
||||
import { WebviewProviderType } from "@shared/webview/types"
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
import { log } from "./utils"
|
||||
import { extensionContext, outputChannel, postMessage } from "./vscode-context"
|
||||
import { extensionContext, postMessage } from "./vscode-context"
|
||||
import { startProtobusService } from "./protobus-service"
|
||||
import { AuthHandler } from "@/hosts/external/AuthHandler"
|
||||
import { WebviewProvider } from "@/core/webview"
|
||||
import { DiffViewProvider } from "@/integrations/editor/DiffViewProvider"
|
||||
|
||||
async function main() {
|
||||
log("\n\n\nStarting cline-core service...\n\n\n")
|
||||
|
||||
AuthHandler.getInstance().setEnabled(true)
|
||||
|
||||
setupHostProvider()
|
||||
|
||||
// Set up global error handlers to prevent process crashes
|
||||
setupGlobalErrorHandlers()
|
||||
|
||||
HostProvider.initialize(createWebview, createDiffView, new ExternalHostBridgeClientManager())
|
||||
activate(extensionContext)
|
||||
const controller = new Controller(extensionContext, outputChannel, postMessage, uuidv4())
|
||||
// Create and initialize cache service
|
||||
|
||||
// Create controller with cache service
|
||||
const controller = new Controller(extensionContext, postMessage, uuidv4())
|
||||
startProtobusService(controller)
|
||||
}
|
||||
|
||||
function createWebview() {
|
||||
return new ExternalWebviewProvider(extensionContext, outputChannel, WebviewProviderType.SIDEBAR)
|
||||
}
|
||||
function createDiffView() {
|
||||
return new ExternalDiffViewProvider()
|
||||
function setupHostProvider() {
|
||||
const createWebview = (_: WebviewProviderType): WebviewProvider => {
|
||||
return new ExternalWebviewProvider(extensionContext, WebviewProviderType.SIDEBAR)
|
||||
}
|
||||
const createDiffView = (): DiffViewProvider => {
|
||||
return new ExternalDiffViewProvider()
|
||||
}
|
||||
const getCallbackUri = (): Promise<string> => {
|
||||
return AuthHandler.getInstance().getCallbackUri()
|
||||
}
|
||||
|
||||
HostProvider.initialize(createWebview, createDiffView, new ExternalHostBridgeClientManager(), log, getCallbackUri)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,20 +3,9 @@ import * as vscode from "vscode"
|
||||
|
||||
import { log } from "./utils"
|
||||
|
||||
const outputChannel: vscode.OutputChannel = {
|
||||
append: (text) => process.stdout.write(text),
|
||||
appendLine: (line) => console.log(`OUTPUT_CHANNEL: ${line}`),
|
||||
clear: () => {},
|
||||
show: () => {},
|
||||
hide: () => {},
|
||||
dispose: () => {},
|
||||
name: "",
|
||||
replace: function (value: string): void {},
|
||||
}
|
||||
|
||||
function postMessage(message: ExtensionMessage): Promise<boolean> {
|
||||
log("postMessage stub called:", JSON.stringify(message).slice(0, 200))
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
|
||||
export { outputChannel, postMessage }
|
||||
export { postMessage }
|
||||
|
||||
@@ -5,7 +5,7 @@ import path, { join } from "path"
|
||||
import type { Extension, ExtensionContext } from "vscode"
|
||||
import { ExtensionKind, ExtensionMode } from "vscode"
|
||||
import { log } from "./utils"
|
||||
import { outputChannel, postMessage } from "./vscode-context-stubs"
|
||||
import { postMessage } from "./vscode-context-stubs"
|
||||
import { EnvironmentVariableCollection, MementoStore, readJson, SecretStore } from "./vscode-context-utils"
|
||||
|
||||
const VERSION = getPackageVersion()
|
||||
@@ -66,4 +66,4 @@ function getPackageVersion(): string {
|
||||
|
||||
console.log("Finished loading vscode context...")
|
||||
|
||||
export { extensionContext, outputChannel, postMessage }
|
||||
export { extensionContext, postMessage }
|
||||
|
||||
@@ -1,33 +1,32 @@
|
||||
import { describe, it, beforeEach, afterEach } from "mocha"
|
||||
import * as sinon from "sinon"
|
||||
import * as should from "should"
|
||||
import * as vscode from "vscode"
|
||||
import * as sinon from "sinon"
|
||||
import type { ClineAPI } from "../exports/cline"
|
||||
import { DiffViewProviderCreator, HostProvider, WebviewProviderCreator } from "@/hosts/host-provider"
|
||||
import { vscodeHostBridgeClient } from "@/hosts/vscode/hostbridge/client/host-grpc-client"
|
||||
import * as stateModule from "@core/storage/state"
|
||||
import { createClineAPI } from "../index"
|
||||
import type { ClineAPI } from "../cline"
|
||||
import { createClineAPI } from "@/exports"
|
||||
|
||||
describe("ClineAPI Core Functionality", () => {
|
||||
let api: ClineAPI
|
||||
let mockController: any
|
||||
let mockOutputChannel: sinon.SinonStubbedInstance<vscode.OutputChannel>
|
||||
let mockLogToChannel: sinon.SinonStub<[string], void>
|
||||
let sandbox: sinon.SinonSandbox
|
||||
let getGlobalStateStub: sinon.SinonStub
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
// Create mock output channel
|
||||
mockOutputChannel = {
|
||||
appendLine: sandbox.stub(),
|
||||
append: sandbox.stub(),
|
||||
clear: sandbox.stub(),
|
||||
show: sandbox.stub(),
|
||||
hide: sandbox.stub(),
|
||||
dispose: sandbox.stub(),
|
||||
replace: sandbox.stub(),
|
||||
name: "Cline Test",
|
||||
} as any
|
||||
|
||||
// Create mock log function
|
||||
mockLogToChannel = sandbox.stub<[string], void>()
|
||||
HostProvider.reset()
|
||||
HostProvider.initialize(
|
||||
((_) => {}) as WebviewProviderCreator,
|
||||
(() => {}) as DiffViewProviderCreator,
|
||||
vscodeHostBridgeClient,
|
||||
mockLogToChannel,
|
||||
async () => "",
|
||||
)
|
||||
// Stub the getGlobalState function from the state module
|
||||
// This is needed because the real createClineAPI uses it for getCustomInstructions
|
||||
getGlobalStateStub = sandbox.stub(stateModule, "getGlobalState")
|
||||
@@ -35,6 +34,7 @@ describe("ClineAPI Core Functionality", () => {
|
||||
// Create a mock controller that matches what the real createClineAPI expects
|
||||
// We don't import the real Controller to avoid the webview dependencies
|
||||
mockController = {
|
||||
id: "test-controller-id",
|
||||
context: {
|
||||
globalState: {
|
||||
get: sandbox.stub(),
|
||||
@@ -58,7 +58,7 @@ describe("ClineAPI Core Functionality", () => {
|
||||
}
|
||||
|
||||
// Create API instance
|
||||
api = createClineAPI(mockOutputChannel as any, mockController)
|
||||
api = createClineAPI(mockController)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -75,19 +75,12 @@ describe("ClineAPI Core Functionality", () => {
|
||||
// Verify task clearing sequence
|
||||
sinon.assert.called(mockController.clearTask)
|
||||
sinon.assert.called(mockController.postStateToWebview)
|
||||
sinon.assert.calledWith(mockController.postMessageToWebview, {
|
||||
type: "action",
|
||||
action: "chatButtonClicked",
|
||||
})
|
||||
sinon.assert.calledWith(mockController.initTask, taskDescription, images)
|
||||
|
||||
// Verify logging - first it logs "Starting new task"
|
||||
sinon.assert.calledWith(mockOutputChannel.appendLine, "Starting new task")
|
||||
sinon.assert.calledWith(mockLogToChannel, "Starting new task")
|
||||
// Then it logs the task details
|
||||
sinon.assert.calledWith(
|
||||
mockOutputChannel.appendLine,
|
||||
`Task started with message: "Create a test function" and 2 image(s)`,
|
||||
)
|
||||
sinon.assert.calledWith(mockLogToChannel, `Task started with message: "Create a test function" and 2 image(s)`)
|
||||
})
|
||||
|
||||
it("should handle undefined task description", async () => {
|
||||
@@ -96,7 +89,7 @@ describe("ClineAPI Core Functionality", () => {
|
||||
sinon.assert.called(mockController.clearTask)
|
||||
sinon.assert.calledWith(mockController.initTask, undefined, [])
|
||||
|
||||
sinon.assert.calledWith(mockOutputChannel.appendLine, "Task started with message: undefined and 0 image(s)")
|
||||
sinon.assert.calledWith(mockLogToChannel, "Task started with message: undefined and 0 image(s)")
|
||||
})
|
||||
|
||||
it("should handle task with no images", async () => {
|
||||
@@ -104,10 +97,7 @@ describe("ClineAPI Core Functionality", () => {
|
||||
|
||||
sinon.assert.calledWith(mockController.initTask, "Task without images", undefined)
|
||||
|
||||
sinon.assert.calledWith(
|
||||
mockOutputChannel.appendLine,
|
||||
`Task started with message: "Task without images" and 0 image(s)`,
|
||||
)
|
||||
sinon.assert.calledWith(mockLogToChannel, `Task started with message: "Task without images" and 0 image(s)`)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -122,7 +112,7 @@ describe("ClineAPI Core Functionality", () => {
|
||||
|
||||
sinon.assert.calledWith(mockTask.handleWebviewAskResponse, "messageResponse", "Test message", ["image.png"])
|
||||
|
||||
sinon.assert.calledWith(mockOutputChannel.appendLine, `Sending message: "Test message" with 1 image(s)`)
|
||||
sinon.assert.calledWith(mockLogToChannel, `Sending message: "Test message" with 1 image(s)`)
|
||||
})
|
||||
|
||||
it("should handle no active task gracefully", async () => {
|
||||
@@ -130,7 +120,7 @@ describe("ClineAPI Core Functionality", () => {
|
||||
|
||||
await api.sendMessage("Message to nowhere", [])
|
||||
|
||||
sinon.assert.calledWith(mockOutputChannel.appendLine, "No active task to send message to")
|
||||
sinon.assert.calledWith(mockLogToChannel, "No active task to send message to")
|
||||
})
|
||||
|
||||
it("should handle empty message", async () => {
|
||||
@@ -154,7 +144,7 @@ describe("ClineAPI Core Functionality", () => {
|
||||
|
||||
sinon.assert.calledWith(mockTask.handleWebviewAskResponse, "messageResponse", "", [])
|
||||
|
||||
sinon.assert.calledWith(mockOutputChannel.appendLine, `Sending message: undefined with 0 image(s)`)
|
||||
sinon.assert.calledWith(mockLogToChannel, `Sending message: undefined with 0 image(s)`)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -170,7 +160,7 @@ describe("ClineAPI Core Functionality", () => {
|
||||
|
||||
sinon.assert.calledWith(mockTask.handleWebviewAskResponse, "yesButtonClicked", "", [])
|
||||
|
||||
sinon.assert.calledWith(mockOutputChannel.appendLine, "Pressing primary button")
|
||||
sinon.assert.calledWith(mockLogToChannel, "Pressing primary button")
|
||||
})
|
||||
|
||||
it("should handle primary button press with no active task", async () => {
|
||||
@@ -178,7 +168,7 @@ describe("ClineAPI Core Functionality", () => {
|
||||
|
||||
await api.pressPrimaryButton()
|
||||
|
||||
sinon.assert.calledWith(mockOutputChannel.appendLine, "No active task to press button for")
|
||||
sinon.assert.calledWith(mockLogToChannel, "No active task to press button for")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -193,7 +183,7 @@ describe("ClineAPI Core Functionality", () => {
|
||||
|
||||
sinon.assert.calledWith(mockTask.handleWebviewAskResponse, "noButtonClicked", "", [])
|
||||
|
||||
sinon.assert.calledWith(mockOutputChannel.appendLine, "Pressing secondary button")
|
||||
sinon.assert.calledWith(mockLogToChannel, "Pressing secondary button")
|
||||
})
|
||||
|
||||
it("should handle secondary button press with no active task", async () => {
|
||||
@@ -201,7 +191,7 @@ describe("ClineAPI Core Functionality", () => {
|
||||
|
||||
await api.pressSecondaryButton()
|
||||
|
||||
sinon.assert.calledWith(mockOutputChannel.appendLine, "No active task to press button for")
|
||||
sinon.assert.calledWith(mockLogToChannel, "No active task to press button for")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,9 @@
|
||||
import { expect } from "@playwright/test"
|
||||
import { e2e } from "./utils/helpers"
|
||||
|
||||
// Test for setting up API keys
|
||||
e2e("Auth - can set up API keys", async ({ page, sidebar }) => {
|
||||
// Use the page object to interact with editor outside the sidebar
|
||||
// Verify initial state
|
||||
await expect(sidebar.getByRole("button", { name: "Get Started for Free" })).toBeVisible()
|
||||
await expect(sidebar.getByRole("button", { name: "Use your own API key" })).toBeVisible()
|
||||
@@ -13,7 +15,6 @@ e2e("Auth - can set up API keys", async ({ page, sidebar }) => {
|
||||
|
||||
// Verify provider selector is visible and set to OpenRouter
|
||||
await expect(sidebar.locator("slot").filter({ hasText: /^OpenRouter$/ })).toBeVisible()
|
||||
|
||||
// Test Cline provider option
|
||||
await providerSelector.click({ delay: 100 })
|
||||
await expect(sidebar.getByRole("option", { name: "Cline" })).toBeVisible()
|
||||
@@ -24,7 +25,9 @@ e2e("Auth - can set up API keys", async ({ page, sidebar }) => {
|
||||
await providerSelector.click({ delay: 100 })
|
||||
await sidebar.getByRole("option", { name: "OpenRouter" }).click({ delay: 100 })
|
||||
|
||||
const apiKeyInput = sidebar.getByRole("textbox", { name: "OpenRouter API Key" })
|
||||
const apiKeyInput = sidebar.getByRole("textbox", {
|
||||
name: "OpenRouter API Key",
|
||||
})
|
||||
await apiKeyInput.fill("test-api-key")
|
||||
await expect(apiKeyInput).toHaveValue("test-api-key")
|
||||
await apiKeyInput.click({ delay: 100 })
|
||||
@@ -50,8 +53,16 @@ e2e("Auth - can set up API keys", async ({ page, sidebar }) => {
|
||||
await expect(helpBanner).not.toBeVisible()
|
||||
|
||||
// Verify the release banner is visible for new installs and can be closed.
|
||||
const releaseBanner = sidebar.getByRole("heading", { name: /^🎉 New in v\d/ })
|
||||
const releaseBanner = sidebar.getByRole("heading", {
|
||||
name: /^🎉 New in v\d/,
|
||||
})
|
||||
await expect(releaseBanner).toBeVisible()
|
||||
await sidebar.getByTestId("close-button").locator("span").first().click()
|
||||
await expect(releaseBanner).not.toBeVisible()
|
||||
|
||||
// Sidebar menu should now be visible
|
||||
// await expect(sidebar.getByRole("button", { name: "Account", exact: true })).toBeVisible()
|
||||
|
||||
// await sidebar.getByRole("button", { name: "Settings" }).click()
|
||||
// await expect(sidebar.getByRole("button", { name: "Done" })).toBeVisible()
|
||||
})
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { expect } from "@playwright/test"
|
||||
import { e2e } from "./utils/helpers"
|
||||
|
||||
e2e("Diff editor", async ({ page, sidebar }) => {
|
||||
await sidebar.getByRole("button", { name: "Get Started for Free" }).click({ delay: 100 })
|
||||
|
||||
await expect(sidebar.getByText(/cline:anthropic\/claude/, { exact: true })).toBeVisible()
|
||||
|
||||
// Verify the help improve banner is visible and can be closed.
|
||||
const helpBanner = sidebar.getByText("Help Improve Cline")
|
||||
await expect(helpBanner).toBeVisible()
|
||||
await sidebar.getByRole("button", { name: "Close banner and enable" }).click()
|
||||
await expect(helpBanner).not.toBeVisible()
|
||||
|
||||
// Verify the release banner is visible for new installs and can be closed.
|
||||
const releaseBanner = sidebar.getByRole("heading", {
|
||||
name: /^🎉 New in v\d/,
|
||||
})
|
||||
await expect(releaseBanner).toBeVisible()
|
||||
await sidebar.getByTestId("close-button").locator("span").first().click()
|
||||
await expect(releaseBanner).not.toBeVisible()
|
||||
|
||||
// Submit a message
|
||||
const inputbox = sidebar.getByTestId("chat-input")
|
||||
await expect(inputbox).toBeVisible()
|
||||
|
||||
await inputbox.fill("Hello, Cline!")
|
||||
await expect(inputbox).toHaveValue("Hello, Cline!")
|
||||
await sidebar.getByTestId("send-button").click({ delay: 100 })
|
||||
await expect(inputbox).toHaveValue("")
|
||||
|
||||
// Loading State initially
|
||||
await expect(sidebar.getByText("API Request...")).toBeVisible()
|
||||
|
||||
// Back to home page with history
|
||||
await sidebar.getByRole("button", { name: "Start New Task" }).click()
|
||||
await expect(sidebar.getByText("Recent Tasks")).toBeVisible()
|
||||
await expect(sidebar.getByText("Hello, Cline!")).toBeVisible() // History with the previous sent message
|
||||
await expect(sidebar.getByText("Tokens:")).toBeVisible() // History with token usage
|
||||
|
||||
// Submit a file edit request
|
||||
await sidebar.getByTestId("chat-input").click()
|
||||
await sidebar.getByTestId("chat-input").fill("edit_request")
|
||||
await sidebar.getByTestId("send-button").click({ delay: 100 })
|
||||
|
||||
// Wait for the sidebar to load the file edit request
|
||||
await sidebar.waitForSelector('span:has-text("Cline wants to edit this file:")')
|
||||
|
||||
// Cline should respond with a file edit request
|
||||
await expect(sidebar.getByText("Cline wants to edit this file:")).toBeVisible()
|
||||
|
||||
// Cline Diff Editor should open with the file name and diff
|
||||
await expect(page.getByText("test.ts: Original ↔ Cline's")).toBeVisible()
|
||||
|
||||
// Diff editor should show the original and modified content
|
||||
await expect(
|
||||
page.locator(
|
||||
".monaco-editor.modified-in-monaco-diff-editor > .overflow-guard > .monaco-scrollable-element.editor-scrollable > .lines-content > div:nth-child(4)",
|
||||
),
|
||||
).toBeVisible()
|
||||
})
|
||||
@@ -0,0 +1,79 @@
|
||||
export const E2E_REGISTERED_MOCK_ENDPOINTS = {
|
||||
"/api/v1": {
|
||||
GET: [
|
||||
"/generation",
|
||||
"/organizations/{orgId}/balance",
|
||||
"/organizations/{orgId}/members/{memberId}/usages",
|
||||
"/users/me",
|
||||
"/users/{userId}/balance",
|
||||
"/users/{userId}/usages",
|
||||
"/users/{userId}/payments",
|
||||
],
|
||||
POST: ["/chat/completions"],
|
||||
PUT: ["/users/active-account"],
|
||||
},
|
||||
"/.test": {
|
||||
GET: [],
|
||||
POST: ["/auth", "/setUserBalance", "/setUserHasOrganization", "/setOrgBalance"],
|
||||
PUT: [],
|
||||
},
|
||||
"/health": {
|
||||
POST: [],
|
||||
GET: ["/", "/ping"],
|
||||
PUT: [],
|
||||
},
|
||||
}
|
||||
|
||||
const replace_in_file = `I successfully replaced "john" with "cline" in the test.ts file. The change has been completed and the file now contains:
|
||||
|
||||
\`\`\`typescript
|
||||
export const name = "cline"
|
||||
\`\`\`
|
||||
|
||||
The TypeScript errors shown in the output are unrelated to this change - they appear to be existing issues in the broader codebase related to missing type definitions and dependencies. The specific task of updating the name in test.ts has been completed successfully.
|
||||
|
||||
<attempt_completion>
|
||||
<result>
|
||||
I have successfully replaced the name "john" with "cline" in the test.ts file. The file now exports:
|
||||
|
||||
\`\`\`typescript
|
||||
export const name = "cline"
|
||||
\`\`\`
|
||||
|
||||
The change has been applied and saved to the file.
|
||||
</result>
|
||||
</attempt_completion>`
|
||||
|
||||
const edit_request = `<thinking>
|
||||
The user wants me to replace the name "john" with "cline" in the test.ts file. I can see the file content provided:
|
||||
|
||||
\`\`\`typescript
|
||||
export const name = "john"
|
||||
\`\`\`
|
||||
|
||||
I need to change "john" to "cline". This is a simple targeted edit, so I should use the replace_in_file tool rather than write_to_file since I\'m only changing one small part of the file.
|
||||
|
||||
I need to:
|
||||
1. Use replace_in_file to change "john" to "cline" in the test.ts file
|
||||
2. The SEARCH block should match the exact content: \`export const name = "john"\`
|
||||
3. The REPLACE block should be: \`export const name = "cline"\`
|
||||
</thinking>
|
||||
|
||||
I\'ll replace "john" with "cline" in the test.ts file.
|
||||
|
||||
<replace_in_file>
|
||||
<path>test.ts</path>
|
||||
<diff>
|
||||
------- SEARCH
|
||||
export const name = "john"
|
||||
=======
|
||||
export const name = "cline"
|
||||
+++++++ REPLACE
|
||||
</diff>
|
||||
</replace_in_file>`
|
||||
|
||||
export const E2E_MOCK_API_RESPONSES = {
|
||||
DEFAULT: "Hello! I'm a mock Cline API response.",
|
||||
REPLACE_REQUEST: replace_in_file,
|
||||
EDIT_REQUEST: edit_request,
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user