mirror of
https://github.com/cline/cline.git
synced 2026-09-06 20:41:02 +08:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 815b839be5 | |||
| 63f3b40ef1 | |||
| 827d002ea6 | |||
| c040db6d71 | |||
| 8d5985c8fd | |||
| c970b8030e | |||
| e7ce38bd85 | |||
| 600a6e33ed | |||
| caf0d8aee2 | |||
| ee33e84c48 | |||
| 280138b259 | |||
| 924f235c31 | |||
| 06d5bc56bc | |||
| b1d82e163c | |||
| 9f73cde5e9 | |||
| be2d416359 | |||
| 42ffc30324 | |||
| b0c67e9f83 | |||
| fd366208a1 | |||
| 6c7bc58215 | |||
| f69a378ff4 | |||
| 309e3bd85c | |||
| eb6eb371e4 | |||
| 419e3e4677 | |||
| e95eecd65f | |||
| e83e71cc6d | |||
| a9e526e99b | |||
| 72f16a8c30 | |||
| 1a3ec9f024 | |||
| b3fa3ad0d3 | |||
| 45241fcccf | |||
| 2a3f0e9418 | |||
| 4334764903 | |||
| b3a10243b8 | |||
| 8e95c136a6 | |||
| 0b66faa1dd | |||
| 3d2dc1c5c4 | |||
| 028412579b | |||
| f04788c2ec |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Improve cerebras Qwen model performance by removing thinking tokens from the model input
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Change available Cerebras models - limit to Qwen and llama 3.3 70b
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Added checkpointTrackerErrorMessage to HistoryItem - restored with task, prevents re-initialization if timed out before
|
||||
Never re-init checkpoint tracker if it timed out before
|
||||
Warning at 7s that it's taking awhile, timeout and give up at 15s
|
||||
Fixed click to open settings - now opens to correct tab
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: mcp servers are not started when disabled
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Refactor Git commit message generation to support output streaming.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Introduce Claude Code support on Windows and fix E2BIG issues
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Improve the Claude Code error messages
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Change Cerebras Qwen 3 32b context window from 16k to 64k
|
||||
@@ -21,7 +21,6 @@
|
||||
"semi": "off",
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
"eslint-rules/no-protobuf-object-literals": "error",
|
||||
"eslint-rules/no-grpc-client-object-literals": "error",
|
||||
"eslint-rules/no-direct-vscode-api": "warn",
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
|
||||
@@ -56,6 +56,29 @@ jobs:
|
||||
path: webview-ui/node_modules
|
||||
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
|
||||
|
||||
# Cache VS Code installation
|
||||
- name: Cache VS Code
|
||||
uses: actions/cache@v4
|
||||
id: vscode-cache
|
||||
with:
|
||||
path: .vscode-test
|
||||
key: vscode-${{ runner.os }}-stable-${{ hashFiles('.vscode-test.mjs', 'package.json') }}
|
||||
restore-keys: |
|
||||
vscode-${{ runner.os }}-stable-
|
||||
|
||||
# Cache Playwright browsers
|
||||
- name: Cache Playwright browsers
|
||||
uses: actions/cache@v4
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: |
|
||||
~/.cache/ms-playwright
|
||||
~/Library/Caches/ms-playwright
|
||||
~/AppData/Local/ms-playwright
|
||||
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
|
||||
restore-keys: |
|
||||
playwright-browsers-${{ runner.os }}-
|
||||
|
||||
- name: Install root dependencies
|
||||
if: steps.root-cache.outputs.cache-hit != 'true'
|
||||
run: npm ci
|
||||
@@ -68,17 +91,14 @@ jobs:
|
||||
if: matrix.runner == 'ubuntu'
|
||||
run: sudo apt-get update && sudo apt-get install -y xvfb
|
||||
|
||||
# Build the extension before running tests
|
||||
- name: Build Tests and Extension
|
||||
run: npm run pretest
|
||||
|
||||
# Run optimized E2E tests (eliminates redundant builds)
|
||||
- name: Run E2E tests - Linux
|
||||
if: matrix.runner == 'ubuntu'
|
||||
run: xvfb-run -a npm run test:e2e
|
||||
run: xvfb-run -a npm run test:e2e:optimal
|
||||
|
||||
- name: Run E2E tests - Non-Linux
|
||||
if: matrix.runner != 'ubuntu'
|
||||
run: npm run test:e2e
|
||||
run: npm run test:e2e:optimal
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ failure() }}
|
||||
|
||||
@@ -94,6 +94,7 @@ jobs:
|
||||
env:
|
||||
VSCE_PAT: ${{ secrets.VSCE_PAT }}
|
||||
OVSX_PAT: ${{ secrets.OVSX_PAT }}
|
||||
CLINE_ENVIRONMENT: production
|
||||
run: |
|
||||
# Required to generate the .vsix
|
||||
vsce package --out "cline-${{ steps.get_version.outputs.version }}.vsix"
|
||||
|
||||
+1
-7
@@ -22,20 +22,14 @@ coverage
|
||||
|
||||
*evals.env
|
||||
|
||||
# Generated files
|
||||
## Generated files ##
|
||||
src/generated/
|
||||
# Core
|
||||
src/core/controller/*/methods.ts
|
||||
src/core/controller/*/index.ts
|
||||
src/core/controller/grpc-service-config.ts
|
||||
# Shared
|
||||
src/shared/proto/*.ts
|
||||
src/shared/proto/host/*.ts
|
||||
# Webview
|
||||
webview-ui/src/services/grpc-client.ts
|
||||
# Host bridge
|
||||
src/hosts/vscode/client/host-grpc-client.ts
|
||||
src/standalone/server-setup.ts
|
||||
|
||||
# E2E Tests
|
||||
test-results
|
||||
|
||||
+3
-1
@@ -5,4 +5,6 @@ webview-ui/build/
|
||||
package-lock.json
|
||||
src/core/prompts/system.ts
|
||||
src/core/prompts/model_prompts/claude4.ts
|
||||
evals/
|
||||
evals/
|
||||
docs/
|
||||
out/
|
||||
Vendored
+31
-3
@@ -6,7 +6,7 @@
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Run Extension",
|
||||
"name": "Run Extension (production)",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": ["--extensionDevelopmentPath=${workspaceFolder}", "--disable-workspace-trust", "${workspaceFolder}"],
|
||||
@@ -14,7 +14,34 @@
|
||||
"preLaunchTask": "${defaultBuildTask}",
|
||||
"env": {
|
||||
"IS_DEV": "true",
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}"
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
|
||||
"CLINE_ENVIRONMENT": "production"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Run Extension (staging)",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": ["--extensionDevelopmentPath=${workspaceFolder}", "--disable-workspace-trust", "${workspaceFolder}"],
|
||||
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||
"preLaunchTask": "${defaultBuildTask}",
|
||||
"env": {
|
||||
"IS_DEV": "true",
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
|
||||
"CLINE_ENVIRONMENT": "staging"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Run Extension (local)",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": ["--extensionDevelopmentPath=${workspaceFolder}", "--disable-workspace-trust", "${workspaceFolder}"],
|
||||
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||
"preLaunchTask": "${defaultBuildTask}",
|
||||
"env": {
|
||||
"IS_DEV": "true",
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
|
||||
"CLINE_ENVIRONMENT": "local"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -37,7 +64,8 @@
|
||||
"env": {
|
||||
"IS_DEV": "true",
|
||||
"TEMP_PROFILE": "true",
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}"
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
|
||||
"CLINE_ENVIRONMENT": "production"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## [3.19.7]
|
||||
|
||||
- Add Hugging Face as a new API provider with support for their inference API models
|
||||
- Improve Claude Code error messages with better guidance for common setup issues (Thanks @BarreiroT!)
|
||||
- Fix authentication sync issues when using multiple VSCode windows
|
||||
|
||||
## [3.19.6]
|
||||
|
||||
- Improve Kimi K2 model provider routing with additional provider options for better availability and performance
|
||||
|
||||
+2
-1
@@ -159,7 +159,8 @@
|
||||
"provider-config/openai",
|
||||
"provider-config/openai-compatible",
|
||||
"provider-config/openrouter",
|
||||
"provider-config/requesty"
|
||||
"provider-config/requesty",
|
||||
"provider-config/sap-aicore"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
Generated
-10118
File diff suppressed because it is too large
Load Diff
+4
-2
@@ -4,13 +4,15 @@
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"dev": "mintlify dev"
|
||||
"dev": "mintlify dev",
|
||||
"check": "mintlify broken-links",
|
||||
"rename": "mintlify rename"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"mintlify": "^4.0.538"
|
||||
"mintlify": "^4.2.23"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,26 +35,8 @@ First, you'll need to install and authenticate Claude Code on your system:
|
||||
<br />
|
||||
|
||||
<Accordion title="Windows Setup">
|
||||
On Windows, Cline supports integrating with Claude Code through WSL.
|
||||
Windows doesn't support long commands, and Claude Code only accepts the system prompt through a flag, which means we can't properly prompt Claude through Claude Code. Anthropic is [working on a workaround to streamline this](https://github.com/anthropics/claude-code/issues/3411).
|
||||
|
||||
1. **Make sure you have WSL set-up**. You can follow [this](https://code.visualstudio.com/docs/remote/wsl#_installation) guide to do it.
|
||||
|
||||
2. **Open VSCode from WSL** and verify it's properly set-up. You should see an indicator in the bottom left that says "WSL". You can find an image of the indicator [here](https://code.visualstudio.com/docs/remote/wsl#_from-the-wsl-terminal).
|
||||
|
||||
3. Install Cline within WSL and make sure it shows the following:
|
||||
|
||||
<Frame>
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/cline_wsl_installed_extension.webp"
|
||||
alt="Indicator that an extension is installed on WSL"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
4. Clone or [move](https://stackoverflow.com/a/42586455) your project over to WSL
|
||||
|
||||
5. Follow the [instructions on how to set up Claude Code normally](#setup), but from the WSL terminal.
|
||||
|
||||
Anthropic introduced full support for Claude Code on Windows. Follow the [instructions on how to set up Claude Code
|
||||
normally](#setup) and make sure you have the latest Claude Code and Cline versions.
|
||||
</Accordion>
|
||||
|
||||
### Finding your Claude Code path
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
title: "SAP AI Core"
|
||||
description: "Learn how to configure and use LLM models from Generative AI Hub in SAP AI Core with Cline."
|
||||
---
|
||||
|
||||
SAP AI Core and the generative AI hub help you to integrate LLMs and AI into new business processes in a cost-efficient manner.
|
||||
|
||||
**Website:** [SAP Help Portal](https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/what-is-sap-ai-core)
|
||||
|
||||
### Getting a Service Binding
|
||||
|
||||
> 💡 **Information**
|
||||
>
|
||||
> SAP AI Core, and Generative AI Hub, are offerings from SAP BTP.
|
||||
> You need an active SAP BTP contract and a existing subaccount with a SAP AI Core instance to perform these steps.
|
||||
|
||||
1. **Access:** Go to your subaccount via [BTP Cloud Cockpit](cockpit.btp.cloud.sap/cockpit)
|
||||
2. **Create a Service Binding:** Go to "Instances and Subscriptions", select your SAP AI Core service instance and click on Service Bindings > Create.
|
||||
3. **Copy the Service Binding:** Copy the service binding values.
|
||||
|
||||
### Supported Models
|
||||
|
||||
SAP AI Core supports a large and growing number of models.
|
||||
Refer to the [Generative AI Hub Supported Models page](https://me.sap.com/notes/3437766) for the complete and up-to-date list.
|
||||
|
||||
### Configuration in Cline
|
||||
|
||||
1. **Open Cline Settings:** Click the settings icon (⚙️) in the Cline panel.
|
||||
2. **Select Provider:** Choose "SAP AI Core" from the "API Provider" dropdown.
|
||||
3. **Enter Client Id:** Add the `.clientid` field from the service binding into the "AI Core Client Id" field.
|
||||
4. **Enter Client Secret:** Add the `.clientsecret` field from the service binding into the "AI Core Client Secret" field.
|
||||
5. **Enter Base URL:** Add the `.serviceurls.AI_API_URL` field from the service binding into the "AI Core Base URL" field.
|
||||
6. **Enter Auth URL:** Add the `.url` field from the service binding into the "AI Core Auth URL" field.
|
||||
7. **Enter Resource Group:** Add the resource group where you have your model deployments. See [Create a Deployment for a Generative AI Model](https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/create-deployment-for-generative-ai-model-in-sap-ai-core).
|
||||
8. **Select Model:** Choose your desired model from the "Model" dropdown.
|
||||
|
||||
### Tips and Notes
|
||||
|
||||
- **Model Selection:** SAP AI Core offers a wide range of models. You won't be able to use the model, even if selected, if a deployment doesn't exist in the provided resource group.
|
||||
@@ -1,174 +0,0 @@
|
||||
const { RuleTester: GrpcRuleTester } = require("eslint")
|
||||
const grpcRule = require("../no-grpc-client-object-literals")
|
||||
|
||||
const grpcRuleTester = new GrpcRuleTester({
|
||||
parser: require.resolve("@typescript-eslint/parser"),
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020,
|
||||
sourceType: "module",
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
grpcRuleTester.run("no-grpc-client-object-literals", grpcRule, {
|
||||
valid: [
|
||||
// Valid case: Using .create() method with gRPC client
|
||||
{
|
||||
code: `
|
||||
import { TogglePlanActModeRequest } from '@shared/proto/state';
|
||||
import { StateServiceClient } from '../services/grpc-client';
|
||||
|
||||
StateServiceClient.togglePlanActMode(
|
||||
TogglePlanActModeRequest.create({
|
||||
chatSettings: {
|
||||
mode: PlanActMode.PLAN,
|
||||
preferredLanguage: 'en',
|
||||
},
|
||||
})
|
||||
);
|
||||
`,
|
||||
},
|
||||
// Valid case: Using .fromPartial() method with gRPC client
|
||||
{
|
||||
code: `
|
||||
import { TogglePlanActModeRequest, ChatSettings } from '@shared/proto/state';
|
||||
import { StateServiceClient } from '../services/grpc-client';
|
||||
|
||||
const chatSettings = ChatSettings.fromPartial({
|
||||
mode: PlanActMode.PLAN,
|
||||
preferredLanguage: 'en',
|
||||
});
|
||||
|
||||
StateServiceClient.togglePlanActMode(
|
||||
TogglePlanActModeRequest.create({
|
||||
chatSettings: chatSettings,
|
||||
})
|
||||
);
|
||||
`,
|
||||
},
|
||||
// Valid case: Regular function call with object literal (not a gRPC client)
|
||||
{
|
||||
code: `
|
||||
function processData(data) {
|
||||
console.log(data);
|
||||
}
|
||||
|
||||
processData({
|
||||
id: 123,
|
||||
name: 'test',
|
||||
});
|
||||
`,
|
||||
},
|
||||
// Valid case: Using proper nested protobuf objects
|
||||
{
|
||||
code: `
|
||||
import { TogglePlanActModeRequest, ChatSettings } from '@shared/proto/state';
|
||||
import { StateServiceClient } from '../services/grpc-client';
|
||||
|
||||
// Using proper nested protobuf objects
|
||||
const chatSettings = ChatSettings.create({
|
||||
mode: 0,
|
||||
preferredLanguage: 'en',
|
||||
});
|
||||
|
||||
const request = TogglePlanActModeRequest.create({
|
||||
chatSettings: chatSettings,
|
||||
});
|
||||
|
||||
StateServiceClient.togglePlanActMode(request);
|
||||
`,
|
||||
},
|
||||
// Valid case: Object literal in second parameter (should not be checked)
|
||||
{
|
||||
code: `
|
||||
import { StateSubscribeRequest } from '@shared/proto/state';
|
||||
import { StateServiceClient } from '../services/grpc-client';
|
||||
|
||||
const request = StateSubscribeRequest.create({
|
||||
topics: ['apiConfig', 'tasks']
|
||||
});
|
||||
|
||||
// Second parameter is an object literal but should not trigger the rule
|
||||
StateServiceClient.subscribe(request, {
|
||||
metadata: {
|
||||
userId: 123,
|
||||
sessionId: "abc-123"
|
||||
}
|
||||
});
|
||||
`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
// Invalid case: Using object literal directly with gRPC client
|
||||
{
|
||||
code: `
|
||||
import { StateServiceClient } from '../services/grpc-client';
|
||||
|
||||
StateServiceClient.togglePlanActMode({
|
||||
chatSettings: {
|
||||
mode: 0,
|
||||
preferredLanguage: 'en',
|
||||
},
|
||||
});
|
||||
`,
|
||||
errors: [{ messageId: "useProtobufMethod" }],
|
||||
},
|
||||
// Invalid case: Using object literal with nested properties
|
||||
{
|
||||
code: `
|
||||
import { ChatSettings } from '@shared/proto/state';
|
||||
import { StateServiceClient } from '../services/grpc-client';
|
||||
|
||||
const chatSettings = ChatSettings.create({
|
||||
mode: 0,
|
||||
preferredLanguage: 'en',
|
||||
});
|
||||
|
||||
StateServiceClient.togglePlanActMode({
|
||||
chatSettings: {
|
||||
mode: 1,
|
||||
preferredLanguage: 'fr',
|
||||
},
|
||||
});
|
||||
`,
|
||||
errors: [{ messageId: "useProtobufMethod" }],
|
||||
},
|
||||
// Invalid case: Nested object literal in protobuf create method
|
||||
{
|
||||
code: `
|
||||
import { TogglePlanActModeRequest, ChatSettings } from '@shared/proto/state';
|
||||
import { StateServiceClient } from '../services/grpc-client';
|
||||
|
||||
// Using nested object literal instead of ChatSettings.create()
|
||||
const request = TogglePlanActModeRequest.create({
|
||||
chatSettings: {
|
||||
mode: 0,
|
||||
preferredLanguage: 'en',
|
||||
},
|
||||
});
|
||||
|
||||
StateServiceClient.togglePlanActMode(request);
|
||||
`,
|
||||
errors: [{ messageId: "useProtobufMethod" }],
|
||||
},
|
||||
// Invalid case: Object literal as first parameter to subscribe method
|
||||
{
|
||||
code: `
|
||||
import { StateServiceClient } from '../services/grpc-client';
|
||||
|
||||
// First parameter is an object literal, which should trigger the rule
|
||||
StateServiceClient.subscribe({
|
||||
topics: ['apiConfig', 'tasks']
|
||||
}, {
|
||||
metadata: {
|
||||
userId: 123,
|
||||
sessionId: "abc-123"
|
||||
}
|
||||
});
|
||||
`,
|
||||
errors: [{ messageId: "useProtobufMethod" }],
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -1,12 +1,10 @@
|
||||
// eslint-rules/index.js
|
||||
const noProtobufObjectLiterals = require("./no-protobuf-object-literals")
|
||||
const noGrpcClientObjectLiterals = require("./no-grpc-client-object-literals")
|
||||
const noDirectVscodeApi = require("./no-direct-vscode-api")
|
||||
|
||||
module.exports = {
|
||||
rules: {
|
||||
"no-protobuf-object-literals": noProtobufObjectLiterals,
|
||||
"no-grpc-client-object-literals": noGrpcClientObjectLiterals,
|
||||
"no-direct-vscode-api": noDirectVscodeApi,
|
||||
},
|
||||
configs: {
|
||||
@@ -14,7 +12,6 @@ module.exports = {
|
||||
plugins: ["local"],
|
||||
rules: {
|
||||
"local/no-protobuf-object-literals": "error",
|
||||
"local/no-grpc-client-object-literals": "error",
|
||||
"local/no-direct-vscode-api": "warn",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -11,8 +11,11 @@ const disallowedApis = {
|
||||
"vscode.workspace.fs.stat": {
|
||||
messageId: "useFsUtils",
|
||||
},
|
||||
"vscode.workspace.fs.writeFile": {
|
||||
messageId: "useFsUtils",
|
||||
},
|
||||
"vscode.workspace.workspaceFolders": {
|
||||
messageId: "useHostBridge",
|
||||
messageId: "useHostBridgeWorkspace",
|
||||
},
|
||||
"vscode.workspace.asRelativePath": {
|
||||
messageId: "usePathUtils",
|
||||
@@ -20,6 +23,29 @@ const disallowedApis = {
|
||||
"vscode.workspace.getWorkspaceFolder": {
|
||||
messageId: "usePathUtils",
|
||||
},
|
||||
"vscode.window.showTextDocument": {
|
||||
messageId: "useHostBridge",
|
||||
},
|
||||
"vscode.workspace.applyEdit": {
|
||||
messageId: "useHostBridge",
|
||||
},
|
||||
// "vscode.env.openExternal": {
|
||||
// messageId: "useUtils",
|
||||
// },
|
||||
// "vscode.window.showWarningMessage": {
|
||||
// messageId: "useHostBridgeShowMessage",
|
||||
// },
|
||||
"vscode.window.showOpenDialog": {
|
||||
messageId: "useHostBridgeShowMessage",
|
||||
},
|
||||
// There are too many warnings for these calls, uncomment the following
|
||||
// when the migration is finished.
|
||||
// "vscode.window.showErrorMessage": {
|
||||
// messageId: "useHostBridgeShowMessage",
|
||||
// },
|
||||
// "vscode.window.showInformationMessage": {
|
||||
// messageId: "useHostBridgeShowMessage",
|
||||
// },
|
||||
}
|
||||
|
||||
module.exports = createRule({
|
||||
@@ -37,16 +63,28 @@ module.exports = createRule({
|
||||
"Example: AccountServiceClient.methodName(RequestType.create({...})) instead of vscode.postMessage({type: '...'}).\n" +
|
||||
"Found: {{code}}",
|
||||
useFsUtils:
|
||||
"Use utilities in @/utils/fs instead of vscode.workspace.fs.stat.\n" +
|
||||
"Use utilities in @/utils/fs instead of vscode.workspace.fs\n" +
|
||||
"Example: import { isDirectory } from '@/utils/fs' or use the file system methods from the host bridge provider.\n" +
|
||||
"Found: {{code}}",
|
||||
useHostBridge:
|
||||
"Use getHostBridgeProvider().workspaceClient.getWorkspacePaths({}) instead of vscode.workspace.workspaceFolders.\n" +
|
||||
usePathUtils:
|
||||
"Use path utilities from @/utils/path instead of VSCode workspace path methods.\n" +
|
||||
"This provides consistent path handling across different environments.\n" +
|
||||
"Found: {{code}}",
|
||||
useHostBridgeWorkspace:
|
||||
"Use HostProvider.workspace.getWorkspacePaths({}) instead of vscode.workspace.workspaceFolders.\n" +
|
||||
"This provides a consistent abstraction across VSCode and standalone environments.\n" +
|
||||
"Found: {{code}}",
|
||||
usePathUtils:
|
||||
"Use path utilities from @/utils/path instead of direct VSCode workspace path methods.\n" +
|
||||
"This provides consistent path handling across different environments.\n" +
|
||||
useHostBridgeShowMessage:
|
||||
"Use HostProvider.window.showMessage instead of the vscode.window.showMessage.\n" +
|
||||
"This provides a consistent abstraction across VSCode and standalone environments.\n" +
|
||||
"Found: {{code}}",
|
||||
useHostBridge:
|
||||
"Use the host bridge instead of calling vscode APIs directly.\n" +
|
||||
"This provides a consistent abstraction across VSCode and standalone environments.\n" +
|
||||
"Found: {{code}}",
|
||||
useUtils:
|
||||
"Use utilities in @/utils instead of calling vscode APIs directly.\n" +
|
||||
"This provides a consistent abstraction across VSCode and standalone environments.\n" +
|
||||
"Found: {{code}}",
|
||||
},
|
||||
schema: [],
|
||||
@@ -54,17 +92,10 @@ module.exports = createRule({
|
||||
defaultOptions: [],
|
||||
|
||||
create(context) {
|
||||
// Check if current file is in an exception directory or is grpc-client-base.ts
|
||||
const filename = context.filename
|
||||
const isGrpcClientBase = path.basename(filename) === "grpc-client-base.ts"
|
||||
|
||||
// Skip checking files in src/hosts/vscode or standalone/runtime-files
|
||||
const isExceptionDirectory = filename.includes("/src/hosts/vscode/") || filename.includes("/standalone/runtime-files/")
|
||||
|
||||
// Pattern for checking memberExpressions like vscode.workspace.fs.stat
|
||||
function checkMemberExpression(node) {
|
||||
// Skip if this file is in an exception directory or is grpc-client-base.ts
|
||||
if (isGrpcClientBase || isExceptionDirectory) {
|
||||
if (isExcluded(context.filename)) {
|
||||
// Skip if this file is being excluded.
|
||||
return
|
||||
}
|
||||
|
||||
@@ -143,6 +174,20 @@ module.exports = createRule({
|
||||
})
|
||||
}
|
||||
|
||||
function isExcluded(filename) {
|
||||
// Check if current file is in an exception directory or is grpc-client-base.ts
|
||||
if (path.basename(filename) === "grpc-client-base.ts") {
|
||||
return true
|
||||
}
|
||||
// Skip checking files in src/hosts/vscode or standalone/runtime-files
|
||||
if (filename.includes("/src/hosts/vscode/")) {
|
||||
return true
|
||||
}
|
||||
if (filename.includes("/standalone/runtime-files/")) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// Detect basic member expressions (e.g., vscode.postMessage)
|
||||
MemberExpression(node) {
|
||||
@@ -152,7 +197,7 @@ module.exports = createRule({
|
||||
// Detect property access through destructuring
|
||||
VariableDeclarator(node) {
|
||||
// Skip if this file is in an exception directory or is grpc-client-base.ts
|
||||
if (isGrpcClientBase || isExceptionDirectory) {
|
||||
if (isExcluded(context.filename)) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -1,216 +0,0 @@
|
||||
const { ESLintUtils } = require("@typescript-eslint/utils")
|
||||
|
||||
const createRule = ESLintUtils.RuleCreator((name) => `https://cline.bot/eslint-rules/${name}`)
|
||||
|
||||
module.exports = createRule({
|
||||
name: "no-grpc-client-object-literals",
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description:
|
||||
"Enforce using .create() or .fromPartial() for gRPC service client parameters instead of object literals",
|
||||
recommended: "error",
|
||||
},
|
||||
messages: {
|
||||
useProtobufMethod:
|
||||
"Use the appropriate protobuf .create() or .fromPartial() method instead of " +
|
||||
"object literal for gRPC client parameters.\n" +
|
||||
"Found: {{code}}\n" +
|
||||
"gRPC client methods should always receive properly created protobuf objects.",
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
defaultOptions: [],
|
||||
|
||||
create(context) {
|
||||
// Check if a name matches the gRPC service client pattern using regex
|
||||
// Must start with an uppercase letter and end with ServiceClient
|
||||
const isGrpcServiceClient = (name) => {
|
||||
return typeof name === "string" && /^[A-Z].*ServiceClient$/.test(name)
|
||||
}
|
||||
|
||||
const safeObjectExpressions = new Map() // Track object expressions in create/fromPartial calls
|
||||
|
||||
return {
|
||||
// Skip object literals inside create() or fromPartial() method calls
|
||||
CallExpression(node) {
|
||||
if (
|
||||
node.callee &&
|
||||
node.callee.type === "MemberExpression" &&
|
||||
(node.callee.property.name === "create" || node.callee.property.name === "fromPartial") &&
|
||||
node.arguments.length > 0 &&
|
||||
node.arguments[0].type === "ObjectExpression"
|
||||
) {
|
||||
// Track this object expression as being used with create/fromPartial
|
||||
safeObjectExpressions.set(node.arguments[0], { isProblematic: false })
|
||||
}
|
||||
},
|
||||
|
||||
// Track create/fromPartial calls that contain nested object literals
|
||||
"CallExpression[callee.type='MemberExpression'][callee.property.name=/^(create|fromPartial)$/]"(node) {
|
||||
if (node.arguments.length > 0 && node.arguments[0].type === "ObjectExpression") {
|
||||
// Track problematic nested object literals
|
||||
const nestedObjectLiterals = new Map() // Map of object expressions to their containing property paths
|
||||
|
||||
// Search for nested object literals
|
||||
const queue = [
|
||||
...node.arguments[0].properties.map((prop) => ({
|
||||
property: prop,
|
||||
path: prop.key && prop.key.name ? prop.key.name : "unknown",
|
||||
})),
|
||||
]
|
||||
|
||||
while (queue.length > 0) {
|
||||
const { property, path } = queue.shift()
|
||||
|
||||
// Skip spread elements
|
||||
if (property.type !== "Property") continue
|
||||
|
||||
// If this is an object literal, mark it as problematic
|
||||
if (property.value.type === "ObjectExpression") {
|
||||
nestedObjectLiterals.set(property.value, path)
|
||||
|
||||
// Add nested properties to queue
|
||||
queue.push(
|
||||
...property.value.properties.map((prop) => ({
|
||||
property: prop,
|
||||
path: `${path}.${prop.key && prop.key.name ? prop.key.name : "unknown"}`,
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// For each problematic nested object, track it with its path
|
||||
nestedObjectLiterals.forEach((path, objectExpr) => {
|
||||
safeObjectExpressions.set(objectExpr, {
|
||||
isProblematic: true,
|
||||
path: path,
|
||||
parentNode: node,
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// Check calls to gRPC service clients
|
||||
"CallExpression[callee.type='MemberExpression']"(node) {
|
||||
// Get the object (left side) of the member expression
|
||||
const callee = node.callee
|
||||
if (callee.object && callee.object.type === "Identifier") {
|
||||
const objectName = callee.object.name
|
||||
|
||||
// Check if this is a call to one of our gRPC service clients
|
||||
if (isGrpcServiceClient(objectName)) {
|
||||
// Only check the first argument of gRPC service client calls
|
||||
if (node.arguments.length > 0) {
|
||||
const arg = node.arguments[0] // Only check the first parameter
|
||||
if (arg.type === "ObjectExpression" && !safeObjectExpressions.has(arg)) {
|
||||
// This is an object literal being passed directly to a gRPC client
|
||||
const sourceCode = context.getSourceCode()
|
||||
const callText = sourceCode.getText(node).trim()
|
||||
|
||||
context.report({
|
||||
node: arg,
|
||||
messageId: "useProtobufMethod",
|
||||
data: {
|
||||
code: callText,
|
||||
},
|
||||
})
|
||||
} else if (arg.type === "ObjectExpression") {
|
||||
// Search for nested object literals that aren't protected
|
||||
const queue = [...arg.properties]
|
||||
while (queue.length > 0) {
|
||||
const property = queue.shift()
|
||||
|
||||
// Skip spread elements
|
||||
if (property.type !== "Property") continue
|
||||
|
||||
// Check value
|
||||
if (
|
||||
property.value.type === "ObjectExpression" &&
|
||||
!safeObjectExpressions.has(property.value)
|
||||
) {
|
||||
// Found a nested object literal
|
||||
const sourceCode = context.getSourceCode()
|
||||
const propertyText = sourceCode.getText(property).trim()
|
||||
|
||||
context.report({
|
||||
node: property.value,
|
||||
messageId: "useProtobufMethod",
|
||||
data: {
|
||||
code: `${objectName}.${callee.property.name}(... ${propertyText} ...)`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Add any nested properties to the queue
|
||||
if (property.value.type === "ObjectExpression") {
|
||||
queue.push(...property.value.properties)
|
||||
}
|
||||
}
|
||||
} else if (arg.type === "Identifier") {
|
||||
// This is a variable - check if it references a problematic protobuf object
|
||||
const varName = arg.name
|
||||
const sourceCode = context.getSourceCode()
|
||||
const scope = sourceCode.getScope(node)
|
||||
|
||||
// Find the variable declaration
|
||||
const variable = scope.variables.find((v) => v.name === varName)
|
||||
if (variable && variable.references && variable.references.length > 0) {
|
||||
// Look for definitions
|
||||
const def = variable.defs.find(
|
||||
(d) => d.node && d.node.type === "VariableDeclarator" && d.node.init,
|
||||
)
|
||||
|
||||
if (
|
||||
def &&
|
||||
def.node.init.type === "CallExpression" &&
|
||||
def.node.init.callee.type === "MemberExpression" &&
|
||||
(def.node.init.callee.property.name === "create" ||
|
||||
def.node.init.callee.property.name === "fromPartial")
|
||||
) {
|
||||
// Flag if we find problematic nested object literals in this create/fromPartial call
|
||||
const callText = sourceCode.getText(node).trim()
|
||||
const initCallText = sourceCode.getText(def.node.init).trim()
|
||||
|
||||
// Check for nested object literals in init node
|
||||
let foundNestedLiteral = false
|
||||
if (
|
||||
def.node.init.arguments.length > 0 &&
|
||||
def.node.init.arguments[0].type === "ObjectExpression"
|
||||
) {
|
||||
// Find any nested object literals
|
||||
const queue = [...def.node.init.arguments[0].properties]
|
||||
while (queue.length > 0 && !foundNestedLiteral) {
|
||||
const property = queue.shift()
|
||||
|
||||
// Skip spread elements
|
||||
if (property.type !== "Property") continue
|
||||
|
||||
if (property.value.type === "ObjectExpression") {
|
||||
foundNestedLiteral = true
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: "useProtobufMethod",
|
||||
data: {
|
||||
code: `${callText} - using request created with nested object literal at: ${property.key.name}`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Add any nested properties to the queue
|
||||
if (property.value.type === "ObjectExpression") {
|
||||
queue.push(...property.value.properties)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
Generated
+1019
-12545
File diff suppressed because it is too large
Load Diff
+23
-8
@@ -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.19.6",
|
||||
"version": "3.19.7",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
@@ -186,6 +186,12 @@
|
||||
"category": "Cline",
|
||||
"icon": "$(robot)"
|
||||
},
|
||||
{
|
||||
"command": "cline.abortGitCommitMessage",
|
||||
"title": "Generate Commit Message with Cline - Stop",
|
||||
"category": "Cline",
|
||||
"icon": "$(debug-stop)"
|
||||
},
|
||||
{
|
||||
"command": "cline.explainCode",
|
||||
"title": "Explain with Cline",
|
||||
@@ -213,7 +219,7 @@
|
||||
},
|
||||
{
|
||||
"command": "cline.generateGitCommitMessage",
|
||||
"when": "scmProvider == git"
|
||||
"when": "config.git.enabled && scmProvider == git"
|
||||
},
|
||||
{
|
||||
"command": "cline.focusChatInput",
|
||||
@@ -306,13 +312,22 @@
|
||||
{
|
||||
"command": "cline.generateGitCommitMessage",
|
||||
"group": "navigation",
|
||||
"when": "scmProvider == git"
|
||||
"when": "config.git.enabled && scmProvider == git && !cline.isGeneratingCommit"
|
||||
},
|
||||
{
|
||||
"command": "cline.abortGitCommitMessage",
|
||||
"group": "navigation",
|
||||
"when": "config.git.enabled && scmProvider == git && cline.isGeneratingCommit"
|
||||
}
|
||||
],
|
||||
"commandPalette": [
|
||||
{
|
||||
"command": "cline.generateGitCommitMessage",
|
||||
"when": "scmProvider == git"
|
||||
"when": "config.git.enabled && scmProvider == git && !cline.isGeneratingCommit"
|
||||
},
|
||||
{
|
||||
"command": "cline.abortGitCommitMessage",
|
||||
"when": "config.git.enabled && scmProvider == git && cline.isGeneratingCommit"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -347,6 +362,7 @@
|
||||
"test:coverage": "vscode-test --coverage",
|
||||
"e2e": "playwright test -c playwright.config.ts",
|
||||
"test:e2e": "playwright install && vsce package --no-dependencies --out dist/e2e.vsix && node src/test/e2e/utils/build.js && playwright test",
|
||||
"test:e2e:optimal": "vsce package --no-dependencies --out dist/e2e.vsix && node src/test/e2e/utils/build.js && playwright test",
|
||||
"install:all": "npm install && cd webview-ui && npm install",
|
||||
"dev:webview": "cd webview-ui && npm run dev",
|
||||
"build:webview": "cd webview-ui && npm run build",
|
||||
@@ -356,9 +372,9 @@
|
||||
"prepare": "husky",
|
||||
"changeset": "changeset",
|
||||
"version-packages": "changeset version",
|
||||
"docs": "cd docs && mintlify dev",
|
||||
"docs:check-links": "cd docs && mintlify broken-links",
|
||||
"docs:rename-file": "cd docs && mintlify rename",
|
||||
"docs": "cd docs && npm run dev",
|
||||
"docs:check-links": "cd docs && npm run check",
|
||||
"docs:rename-file": "cd docs && npm run rename",
|
||||
"report-issue": "node scripts/report-issue.js"
|
||||
},
|
||||
"lint-staged": {
|
||||
@@ -396,7 +412,6 @@
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^16.1.0",
|
||||
"minimatch": "^3.0.3",
|
||||
"mintlify": "^4.0.515",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.3.3",
|
||||
"protoc-gen-ts": "^0.8.7",
|
||||
|
||||
@@ -53,6 +53,7 @@ message UserInfo {
|
||||
optional string display_name = 2;
|
||||
optional string email = 3;
|
||||
optional string photo_url = 4;
|
||||
optional string app_base_url = 5; // Cline app base URL
|
||||
}
|
||||
|
||||
message UserOrganization {
|
||||
|
||||
+40
-2
@@ -10,7 +10,16 @@ import "common.proto";
|
||||
service DiffService {
|
||||
// Open the diff view/editor.
|
||||
rpc openDiff(OpenDiffRequest) returns (OpenDiffResponse);
|
||||
// Get the contents of the diff view.
|
||||
rpc getDocumentText(GetDocumentTextRequest) returns (GetDocumentTextResponse);
|
||||
// Replace a text selection in the diff.
|
||||
rpc replaceText(ReplaceTextRequest) returns (ReplaceTextResponse);
|
||||
// Truncate the diff document.
|
||||
rpc truncateDocument(TruncateDocumentRequest) returns (TruncateDocumentResponse);
|
||||
// Save the diff document.
|
||||
rpc saveDocument(SaveDocumentRequest) returns (SaveDocumentResponse);
|
||||
// Close the diff editor UI.
|
||||
rpc closeDiff(CloseDiffRequest) returns (CloseDiffResponse);
|
||||
}
|
||||
|
||||
message OpenDiffRequest {
|
||||
@@ -26,6 +35,15 @@ message OpenDiffResponse {
|
||||
optional string diff_id = 1;
|
||||
}
|
||||
|
||||
message GetDocumentTextRequest {
|
||||
optional cline.Metadata metadata = 1;
|
||||
optional string diff_id = 2;
|
||||
}
|
||||
|
||||
message GetDocumentTextResponse {
|
||||
optional string content = 1;
|
||||
}
|
||||
|
||||
message ReplaceTextRequest {
|
||||
optional cline.Metadata metadata = 1;
|
||||
optional string diff_id = 2;
|
||||
@@ -34,6 +52,26 @@ message ReplaceTextRequest {
|
||||
optional int32 end_line = 5;
|
||||
}
|
||||
|
||||
message ReplaceTextResponse {
|
||||
// TBD
|
||||
message ReplaceTextResponse {}
|
||||
|
||||
message TruncateDocumentRequest {
|
||||
optional cline.Metadata metadata = 1;
|
||||
optional string diff_id = 2;
|
||||
optional int32 end_line = 5;
|
||||
}
|
||||
|
||||
message TruncateDocumentResponse {}
|
||||
|
||||
message CloseDiffRequest {
|
||||
optional cline.Metadata metadata = 1;
|
||||
optional string diff_id = 2;
|
||||
}
|
||||
|
||||
message CloseDiffResponse {}
|
||||
|
||||
message SaveDocumentRequest {
|
||||
optional cline.Metadata metadata = 1;
|
||||
optional string diff_id = 2;
|
||||
}
|
||||
|
||||
message SaveDocumentResponse {}
|
||||
|
||||
@@ -6,6 +6,10 @@ option java_multiple_files = true;
|
||||
|
||||
import "common.proto";
|
||||
|
||||
/**
|
||||
* The watch service is only here as example of a streaming rpc in the host bridge.
|
||||
* This being replaced with a native JS file watcher.
|
||||
*/
|
||||
// WatchService provides methods for watching files in the IDE
|
||||
service WatchService {
|
||||
// Subscribe to file changes
|
||||
|
||||
+118
-81
@@ -15,6 +15,8 @@ service ModelsService {
|
||||
rpc getVsCodeLmModels(EmptyRequest) returns (VsCodeLmModelsArray);
|
||||
// Refreshes and returns OpenRouter models
|
||||
rpc refreshOpenRouterModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
// Refreshes and returns Hugging Face models
|
||||
rpc refreshHuggingFaceModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
// Refreshes and returns OpenAI models
|
||||
rpc refreshOpenAiModels(OpenAiModelsRequest) returns (StringArray);
|
||||
// Refreshes and returns Requesty models
|
||||
@@ -126,6 +128,7 @@ enum ApiProvider {
|
||||
SAPAICORE = 25;
|
||||
CLAUDE_CODE = 26;
|
||||
MOONSHOT = 27;
|
||||
HUGGINGFACE = 28;
|
||||
}
|
||||
|
||||
// Model info for OpenAI-compatible models
|
||||
@@ -165,85 +168,119 @@ message LiteLLMModelInfo {
|
||||
|
||||
// Main ApiConfiguration message
|
||||
message ModelsApiConfiguration {
|
||||
// From ApiHandlerOptions (excluding onRetryAttempt function)
|
||||
optional string api_model_id = 1;
|
||||
optional string api_key = 2;
|
||||
optional string cline_account_id = 3;
|
||||
optional string task_id = 4;
|
||||
optional string lite_llm_base_url = 5;
|
||||
optional string lite_llm_model_id = 6;
|
||||
optional string lite_llm_api_key = 7;
|
||||
optional bool lite_llm_use_prompt_cache = 8;
|
||||
map<string, string> open_ai_headers = 9;
|
||||
optional LiteLLMModelInfo lite_llm_model_info = 10;
|
||||
optional string anthropic_base_url = 11;
|
||||
optional string open_router_api_key = 12;
|
||||
optional string open_router_model_id = 13;
|
||||
optional OpenRouterModelInfo open_router_model_info = 14;
|
||||
optional string open_router_provider_sorting = 15;
|
||||
optional string aws_access_key = 16;
|
||||
optional string aws_secret_key = 17;
|
||||
optional string aws_session_token = 18;
|
||||
optional string aws_region = 19;
|
||||
optional bool aws_use_cross_region_inference = 20;
|
||||
optional bool aws_bedrock_use_prompt_cache = 21;
|
||||
optional bool aws_use_profile = 22;
|
||||
optional string aws_profile = 23;
|
||||
optional string aws_bedrock_endpoint = 24;
|
||||
optional bool aws_bedrock_custom_selected = 25;
|
||||
optional string aws_bedrock_custom_model_base_id = 26;
|
||||
optional string vertex_project_id = 27;
|
||||
optional string vertex_region = 28;
|
||||
optional string open_ai_base_url = 29;
|
||||
optional string open_ai_api_key = 30;
|
||||
optional string open_ai_model_id = 31;
|
||||
optional OpenAiCompatibleModelInfo open_ai_model_info = 32;
|
||||
optional string ollama_model_id = 33;
|
||||
optional string ollama_base_url = 34;
|
||||
optional string ollama_api_options_ctx_num = 35;
|
||||
optional string lm_studio_model_id = 36;
|
||||
optional string lm_studio_base_url = 37;
|
||||
optional string gemini_api_key = 38;
|
||||
optional string gemini_base_url = 39;
|
||||
optional string open_ai_native_api_key = 40;
|
||||
optional string deep_seek_api_key = 41;
|
||||
optional string requesty_api_key = 42;
|
||||
optional string requesty_model_id = 43;
|
||||
optional OpenRouterModelInfo requesty_model_info = 44;
|
||||
optional string together_api_key = 45;
|
||||
optional string together_model_id = 46;
|
||||
optional string fireworks_api_key = 47;
|
||||
optional string fireworks_model_id = 48;
|
||||
optional int32 fireworks_model_max_completion_tokens = 49;
|
||||
optional int32 fireworks_model_max_tokens = 50;
|
||||
optional string qwen_api_key = 51;
|
||||
optional string doubao_api_key = 52;
|
||||
optional string mistral_api_key = 53;
|
||||
optional string azure_api_version = 54;
|
||||
optional LanguageModelChatSelector vs_code_lm_model_selector = 55;
|
||||
optional string qwen_api_line = 56;
|
||||
optional string nebius_api_key = 57;
|
||||
optional string asksage_api_url = 58;
|
||||
optional string asksage_api_key = 59;
|
||||
optional string xai_api_key = 60;
|
||||
optional int32 thinking_budget_tokens = 61;
|
||||
optional string reasoning_effort = 62;
|
||||
optional string sambanova_api_key = 63;
|
||||
optional string cerebras_api_key = 64;
|
||||
optional int32 request_timeout_ms = 65;
|
||||
optional ApiProvider api_provider = 66;
|
||||
repeated string favorited_model_ids = 67;
|
||||
optional string sap_ai_core_client_id = 68;
|
||||
optional string sap_ai_core_client_secret = 69;
|
||||
optional string sap_ai_resource_group = 70;
|
||||
optional string sap_ai_core_token_url = 71;
|
||||
optional string sap_ai_core_base_url = 72;
|
||||
optional string claude_code_path = 73;
|
||||
optional string aws_authentication = 74;
|
||||
optional string aws_bedrock_api_key = 75;
|
||||
optional string moonshot_api_key = 76;
|
||||
optional string moonshot_api_line = 77;
|
||||
optional string groq_api_key = 78;
|
||||
optional string groq_model_id = 79;
|
||||
optional OpenRouterModelInfo groq_model_info = 80;
|
||||
// Global configuration fields (not mode-specific)
|
||||
optional string api_key = 1;
|
||||
optional string cline_api_key = 2;
|
||||
optional string task_id = 3;
|
||||
optional string lite_llm_base_url = 4;
|
||||
optional string lite_llm_api_key = 5;
|
||||
optional bool lite_llm_use_prompt_cache = 6;
|
||||
map<string, string> open_ai_headers = 7;
|
||||
optional string anthropic_base_url = 8;
|
||||
optional string open_router_api_key = 9;
|
||||
optional string open_router_provider_sorting = 10;
|
||||
optional string aws_access_key = 11;
|
||||
optional string aws_secret_key = 12;
|
||||
optional string aws_session_token = 13;
|
||||
optional string aws_region = 14;
|
||||
optional bool aws_use_cross_region_inference = 15;
|
||||
optional bool aws_bedrock_use_prompt_cache = 16;
|
||||
optional bool aws_use_profile = 17;
|
||||
optional string aws_profile = 18;
|
||||
optional string aws_bedrock_endpoint = 19;
|
||||
optional string claude_code_path = 20;
|
||||
optional string vertex_project_id = 21;
|
||||
optional string vertex_region = 22;
|
||||
optional string open_ai_base_url = 23;
|
||||
optional string open_ai_api_key = 24;
|
||||
optional string ollama_base_url = 25;
|
||||
optional string ollama_api_options_ctx_num = 26;
|
||||
optional string lm_studio_base_url = 27;
|
||||
optional string gemini_api_key = 28;
|
||||
optional string gemini_base_url = 29;
|
||||
optional string open_ai_native_api_key = 30;
|
||||
optional string deep_seek_api_key = 31;
|
||||
optional string requesty_api_key = 32;
|
||||
optional string together_api_key = 33;
|
||||
optional string fireworks_api_key = 34;
|
||||
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;
|
||||
optional string azure_api_version = 40;
|
||||
optional string qwen_api_line = 41;
|
||||
optional string nebius_api_key = 42;
|
||||
optional string asksage_api_url = 43;
|
||||
optional string asksage_api_key = 44;
|
||||
optional string xai_api_key = 45;
|
||||
optional string sambanova_api_key = 46;
|
||||
optional string cerebras_api_key = 47;
|
||||
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;
|
||||
optional string sap_ai_core_token_url = 52;
|
||||
optional string sap_ai_core_base_url = 53;
|
||||
optional string moonshot_api_key = 54;
|
||||
optional string moonshot_api_line = 55;
|
||||
optional string aws_authentication = 56;
|
||||
optional string aws_bedrock_api_key = 57;
|
||||
optional string cline_account_id = 58;
|
||||
optional string groq_api_key = 59;
|
||||
optional string hugging_face_api_key = 60;
|
||||
|
||||
// Plan mode configurations
|
||||
optional ApiProvider plan_mode_api_provider = 100;
|
||||
optional string plan_mode_api_model_id = 101;
|
||||
optional int32 plan_mode_thinking_budget_tokens = 102;
|
||||
optional string plan_mode_reasoning_effort = 103;
|
||||
optional LanguageModelChatSelector plan_mode_vs_code_lm_model_selector = 104;
|
||||
optional bool plan_mode_aws_bedrock_custom_selected = 105;
|
||||
optional string plan_mode_aws_bedrock_custom_model_base_id = 106;
|
||||
optional string plan_mode_open_router_model_id = 107;
|
||||
optional OpenRouterModelInfo plan_mode_open_router_model_info = 108;
|
||||
optional string plan_mode_open_ai_model_id = 109;
|
||||
optional OpenAiCompatibleModelInfo plan_mode_open_ai_model_info = 110;
|
||||
optional string plan_mode_ollama_model_id = 111;
|
||||
optional string plan_mode_lm_studio_model_id = 112;
|
||||
optional string plan_mode_lite_llm_model_id = 113;
|
||||
optional LiteLLMModelInfo plan_mode_lite_llm_model_info = 114;
|
||||
optional string plan_mode_requesty_model_id = 115;
|
||||
optional OpenRouterModelInfo plan_mode_requesty_model_info = 116;
|
||||
optional string plan_mode_together_model_id = 117;
|
||||
optional string plan_mode_fireworks_model_id = 118;
|
||||
optional string plan_mode_sap_ai_core_model_id = 119;
|
||||
optional string plan_mode_groq_model_id = 120;
|
||||
optional OpenRouterModelInfo plan_mode_groq_model_info = 121;
|
||||
optional string plan_mode_hugging_face_model_id = 122;
|
||||
optional OpenRouterModelInfo plan_mode_hugging_face_model_info = 123;
|
||||
|
||||
// Act mode configurations
|
||||
optional ApiProvider act_mode_api_provider = 200;
|
||||
optional string act_mode_api_model_id = 201;
|
||||
optional int32 act_mode_thinking_budget_tokens = 202;
|
||||
optional string act_mode_reasoning_effort = 203;
|
||||
optional LanguageModelChatSelector act_mode_vs_code_lm_model_selector = 204;
|
||||
optional bool act_mode_aws_bedrock_custom_selected = 205;
|
||||
optional string act_mode_aws_bedrock_custom_model_base_id = 206;
|
||||
optional string act_mode_open_router_model_id = 207;
|
||||
optional OpenRouterModelInfo act_mode_open_router_model_info = 208;
|
||||
optional string act_mode_open_ai_model_id = 209;
|
||||
optional OpenAiCompatibleModelInfo act_mode_open_ai_model_info = 210;
|
||||
optional string act_mode_ollama_model_id = 211;
|
||||
optional string act_mode_lm_studio_model_id = 212;
|
||||
optional string act_mode_lite_llm_model_id = 213;
|
||||
optional LiteLLMModelInfo act_mode_lite_llm_model_info = 214;
|
||||
optional string act_mode_requesty_model_id = 215;
|
||||
optional OpenRouterModelInfo act_mode_requesty_model_info = 216;
|
||||
optional string act_mode_together_model_id = 217;
|
||||
optional string act_mode_fireworks_model_id = 218;
|
||||
optional string act_mode_sap_ai_core_model_id = 219;
|
||||
optional string act_mode_groq_model_id = 220;
|
||||
optional OpenRouterModelInfo act_mode_groq_model_info = 221;
|
||||
optional string act_mode_hugging_face_model_id = 222;
|
||||
optional OpenRouterModelInfo act_mode_hugging_face_model_info = 223;
|
||||
|
||||
repeated string favorited_model_ids = 300;
|
||||
}
|
||||
|
||||
+102
-115
@@ -118,126 +118,113 @@ message UpdateSettingsRequest {
|
||||
|
||||
// Complete API Configuration message
|
||||
message ApiConfiguration {
|
||||
// Core API fields
|
||||
optional string api_provider = 1;
|
||||
optional string api_model_id = 2;
|
||||
optional string api_key = 3; // anthropic
|
||||
optional string api_base_url = 4;
|
||||
// Global configuration fields (not mode-specific)
|
||||
optional string api_key = 1; // anthropic
|
||||
optional string cline_api_key = 2;
|
||||
optional string task_id = 3;
|
||||
optional string lite_llm_base_url = 4;
|
||||
optional string lite_llm_api_key = 5;
|
||||
optional bool lite_llm_use_prompt_cache = 6;
|
||||
optional string openai_headers = 7; // JSON string
|
||||
optional string anthropic_base_url = 8;
|
||||
optional string openrouter_api_key = 9;
|
||||
optional string openrouter_provider_sorting = 10;
|
||||
optional string aws_access_key = 11;
|
||||
optional string aws_secret_key = 12;
|
||||
optional string aws_session_token = 13;
|
||||
optional string aws_region = 14;
|
||||
optional bool aws_use_cross_region_inference = 15;
|
||||
optional bool aws_bedrock_use_prompt_cache = 16;
|
||||
optional bool aws_use_profile = 17;
|
||||
optional string aws_profile = 18;
|
||||
optional string aws_bedrock_endpoint = 19;
|
||||
optional string claude_code_path = 20;
|
||||
optional string vertex_project_id = 21;
|
||||
optional string vertex_region = 22;
|
||||
optional string openai_base_url = 23;
|
||||
optional string openai_api_key = 24;
|
||||
optional string ollama_base_url = 25;
|
||||
optional string ollama_api_options_ctx_num = 26;
|
||||
optional string lm_studio_base_url = 27;
|
||||
optional string gemini_api_key = 28;
|
||||
optional string gemini_base_url = 29;
|
||||
optional string openai_native_api_key = 30;
|
||||
optional string deep_seek_api_key = 31;
|
||||
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 string qwen_api_key = 37;
|
||||
optional string doubao_api_key = 38;
|
||||
optional string mistral_api_key = 39;
|
||||
optional string azure_api_version = 40;
|
||||
optional string qwen_api_line = 41;
|
||||
optional string nebius_api_key = 42;
|
||||
optional string asksage_api_url = 43;
|
||||
optional string asksage_api_key = 44;
|
||||
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 string sap_ai_core_client_id = 49;
|
||||
optional string sap_ai_core_client_secret = 50;
|
||||
optional string sap_ai_resource_group = 51;
|
||||
optional string sap_ai_core_token_url = 52;
|
||||
optional string sap_ai_core_base_url = 53;
|
||||
optional string moonshot_api_key = 54;
|
||||
optional string moonshot_api_line = 55;
|
||||
|
||||
// Provider-specific API keys
|
||||
optional string cline_account_id = 5;
|
||||
optional string openrouter_api_key = 6;
|
||||
optional string anthropic_base_url = 7;
|
||||
optional string openai_api_key = 8;
|
||||
optional string openai_native_api_key = 9;
|
||||
optional string gemini_api_key = 10;
|
||||
optional string deepseek_api_key = 11;
|
||||
optional string requesty_api_key = 12;
|
||||
optional string together_api_key = 13;
|
||||
optional string fireworks_api_key = 14;
|
||||
optional string qwen_api_key = 15;
|
||||
optional string doubao_api_key = 16;
|
||||
optional string mistral_api_key = 17;
|
||||
optional string nebius_api_key = 18;
|
||||
optional string asksage_api_key = 19;
|
||||
optional string xai_api_key = 20;
|
||||
optional string sambanova_api_key = 21;
|
||||
optional string cerebras_api_key = 22;
|
||||
// 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 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;
|
||||
optional string plan_mode_aws_bedrock_custom_model_base_id = 106;
|
||||
optional string plan_mode_openrouter_model_id = 107;
|
||||
optional string plan_mode_openrouter_model_info = 108; // JSON string
|
||||
optional string plan_mode_openai_model_id = 109;
|
||||
optional string plan_mode_openai_model_info = 110; // JSON string
|
||||
optional string plan_mode_ollama_model_id = 111;
|
||||
optional string plan_mode_lm_studio_model_id = 112;
|
||||
optional string plan_mode_lite_llm_model_id = 113;
|
||||
optional string plan_mode_lite_llm_model_info = 114; // JSON string
|
||||
optional string plan_mode_requesty_model_id = 115;
|
||||
optional string plan_mode_requesty_model_info = 116; // JSON string
|
||||
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;
|
||||
|
||||
// Model IDs
|
||||
optional string openrouter_model_id = 23;
|
||||
optional string openai_model_id = 24;
|
||||
optional string anthropic_model_id = 25;
|
||||
optional string bedrock_model_id = 26;
|
||||
optional string vertex_model_id = 27;
|
||||
optional string gemini_model_id = 28;
|
||||
optional string ollama_model_id = 29;
|
||||
optional string lm_studio_model_id = 30;
|
||||
optional string litellm_model_id = 31;
|
||||
optional string requesty_model_id = 32;
|
||||
optional string together_model_id = 33;
|
||||
optional string fireworks_model_id = 34;
|
||||
|
||||
// AWS Bedrock fields
|
||||
optional bool aws_bedrock_custom_selected = 35;
|
||||
optional string aws_bedrock_custom_model_base_id = 36;
|
||||
optional string aws_access_key = 37;
|
||||
optional string aws_secret_key = 38;
|
||||
optional string aws_session_token = 39;
|
||||
optional string aws_region = 40;
|
||||
optional bool aws_use_cross_region_inference = 41;
|
||||
optional bool aws_bedrock_use_prompt_cache = 42;
|
||||
optional bool aws_use_profile = 43;
|
||||
optional string aws_profile = 44;
|
||||
optional string aws_bedrock_endpoint = 45;
|
||||
|
||||
// Vertex AI fields
|
||||
optional string vertex_project_id = 46;
|
||||
optional string vertex_region = 47;
|
||||
|
||||
// Base URLs and endpoints
|
||||
optional string openai_base_url = 48;
|
||||
optional string ollama_base_url = 49;
|
||||
optional string lm_studio_base_url = 50;
|
||||
optional string gemini_base_url = 51;
|
||||
optional string litellm_base_url = 52;
|
||||
optional string asksage_api_url = 53;
|
||||
|
||||
// LiteLLM specific fields
|
||||
optional string litellm_api_key = 54;
|
||||
optional bool litellm_use_prompt_cache = 55;
|
||||
|
||||
// Model configuration
|
||||
optional int64 thinking_budget_tokens = 56;
|
||||
optional string reasoning_effort = 57;
|
||||
optional int64 request_timeout_ms = 58;
|
||||
|
||||
// Fireworks specific
|
||||
optional int64 fireworks_model_max_completion_tokens = 59;
|
||||
optional int64 fireworks_model_max_tokens = 60;
|
||||
|
||||
// Azure specific
|
||||
optional string azure_api_version = 61;
|
||||
|
||||
// Ollama specific
|
||||
optional string ollama_api_options_ctx_num = 62;
|
||||
|
||||
// Qwen specific
|
||||
optional string qwen_api_line = 63;
|
||||
|
||||
// OpenRouter specific
|
||||
optional string openrouter_provider_sorting = 64;
|
||||
|
||||
// VSCode LM (stored as JSON string due to complex type)
|
||||
optional string vscode_lm_model_selector = 65;
|
||||
|
||||
// Model info objects (stored as JSON strings)
|
||||
optional string openrouter_model_info = 66;
|
||||
optional string openai_model_info = 67;
|
||||
optional string requesty_model_info = 68;
|
||||
optional string litellm_model_info = 69;
|
||||
|
||||
// OpenAI headers (stored as JSON string)
|
||||
optional string openai_headers = 70;
|
||||
// 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 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;
|
||||
optional string act_mode_aws_bedrock_custom_model_base_id = 206;
|
||||
optional string act_mode_openrouter_model_id = 207;
|
||||
optional string act_mode_openrouter_model_info = 208; // JSON string
|
||||
optional string act_mode_openai_model_id = 209;
|
||||
optional string act_mode_openai_model_info = 210; // JSON string
|
||||
optional string act_mode_ollama_model_id = 211;
|
||||
optional string act_mode_lm_studio_model_id = 212;
|
||||
optional string act_mode_lite_llm_model_id = 213;
|
||||
optional string act_mode_lite_llm_model_info = 214; // JSON string
|
||||
optional string act_mode_requesty_model_id = 215;
|
||||
optional string act_mode_requesty_model_info = 216; // JSON string
|
||||
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;
|
||||
|
||||
// Favorited model IDs
|
||||
repeated string favorited_model_ids = 71;
|
||||
|
||||
// SAP AI Core specific
|
||||
optional string sap_ai_core_client_id = 72;
|
||||
optional string sap_ai_core_client_secret = 73;
|
||||
optional string sap_ai_core_base_url = 74;
|
||||
optional string sap_ai_core_token_url = 75;
|
||||
optional string sap_ai_resource_group = 76;
|
||||
|
||||
// Claude Code specific
|
||||
optional string claude_code_path = 77;
|
||||
repeated string favorited_model_ids = 300;
|
||||
|
||||
// Extension fields for Bedrock Api Keys
|
||||
optional string aws_authentication = 78;
|
||||
optional string aws_bedrock_api_key = 79;
|
||||
optional string aws_authentication = 301;
|
||||
optional string aws_bedrock_api_key = 302;
|
||||
|
||||
// Moonshot
|
||||
optional string moonshot_api_key = 80;
|
||||
optional string moonshot_api_line = 81;
|
||||
optional string cline_account_id = 303;
|
||||
}
|
||||
|
||||
+12
-61
@@ -42,6 +42,8 @@ const serviceDirs = Object.keys(serviceNameMap).map((serviceKey) => path.join("s
|
||||
async function main() {
|
||||
console.log(chalk.bold.blue("Starting Protocol Buffer code generation..."))
|
||||
|
||||
await cleanup()
|
||||
|
||||
// Check for Apple Silicon compatibility before proceeding
|
||||
checkAppleSiliconCompatibility()
|
||||
|
||||
@@ -50,8 +52,6 @@ async function main() {
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
}
|
||||
|
||||
await cleanup()
|
||||
|
||||
// Check for missing proto files for services in serviceNameMap
|
||||
await ensureProtoFilesExist()
|
||||
|
||||
@@ -86,7 +86,6 @@ async function main() {
|
||||
|
||||
await generateProtoBusServiceConfig()
|
||||
await generateProtoBusMethodRegistrations()
|
||||
await generateProtoBusGrpcClientConfig()
|
||||
|
||||
console.log(chalk.bold.blue("Finished Protocol Buffer code generation."))
|
||||
}
|
||||
@@ -111,51 +110,6 @@ async function tsProtoc(outDir, protoFiles, protoOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a gRPC client configuration file for the webview
|
||||
* This eliminates the need for manual imports and client creation in grpc-client.ts
|
||||
*/
|
||||
async function generateProtoBusGrpcClientConfig() {
|
||||
log_verbose(chalk.cyan("Generating gRPC client configuration..."))
|
||||
|
||||
const serviceImports = []
|
||||
const serviceClientCreations = []
|
||||
const serviceExports = []
|
||||
|
||||
// Process each service in the serviceNameMap
|
||||
for (const [dirName, _fullServiceName] of Object.entries(serviceNameMap)) {
|
||||
const capitalizedName = dirName.charAt(0).toUpperCase() + dirName.slice(1)
|
||||
|
||||
// Add import statement
|
||||
serviceImports.push(`import { ${capitalizedName}ServiceDefinition } from "@shared/proto/${dirName}"`)
|
||||
|
||||
// Add client creation
|
||||
serviceClientCreations.push(
|
||||
`const ${capitalizedName}ServiceClient = createGrpcClient(${capitalizedName}ServiceDefinition)`,
|
||||
)
|
||||
|
||||
// Add to exports
|
||||
serviceExports.push(`${capitalizedName}ServiceClient`)
|
||||
}
|
||||
|
||||
// Generate the file content
|
||||
const content = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by ${SCRIPT_NAME}
|
||||
|
||||
import { createGrpcClient } from "./grpc-client-base"
|
||||
${serviceImports.join("\n")}
|
||||
|
||||
${serviceClientCreations.join("\n")}
|
||||
|
||||
export {
|
||||
${serviceExports.join(",\n\t")}
|
||||
}`
|
||||
|
||||
const filePath = path.resolve("webview-ui/src/services/grpc-client.ts")
|
||||
await writeFileWithMkdirs(filePath, content)
|
||||
log_verbose(chalk.green(`Generated gRPC client at ${filePath}`))
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse proto files to extract streaming method information
|
||||
* @param protoFiles Array of proto file names
|
||||
@@ -420,20 +374,13 @@ service ${serviceClassName} {
|
||||
async function cleanup() {
|
||||
// Clean up existing generated files
|
||||
log_verbose(chalk.cyan("Cleaning up existing generated TypeScript files..."))
|
||||
const existingFiles = await globby("**/*.ts", { cwd: TS_OUT_DIR })
|
||||
for (const file of existingFiles) {
|
||||
await fs.unlink(path.join(TS_OUT_DIR, file))
|
||||
}
|
||||
await rmdir("src/generated")
|
||||
await rmrf(TS_OUT_DIR)
|
||||
await rmrf("src/generated")
|
||||
|
||||
// Clean up generated files that were moved.
|
||||
await fs.rm("src/standalone/services/host-grpc-client.ts", { force: true })
|
||||
await rmdir("src/standalone/services")
|
||||
await fs.rm("hosts/vscode", { force: true, recursive: true })
|
||||
await rmdir("hosts")
|
||||
|
||||
await fs.rm("src/standalone/server-setup.ts", { force: true })
|
||||
await fs.rm("src/hosts/vscode/host-grpc-service-config.ts", { force: true })
|
||||
await rmrf("src/standalone/services/host-grpc-client.ts")
|
||||
await rmrf("src/standalone/server-setup.ts")
|
||||
await rmrf("src/hosts/vscode/host-grpc-service-config.ts")
|
||||
const oldhostbridgefiles = [
|
||||
"src/hosts/vscode/workspace/methods.ts",
|
||||
"src/hosts/vscode/workspace/index.ts",
|
||||
@@ -449,7 +396,7 @@ async function cleanup() {
|
||||
"src/hosts/vscode/uri/index.ts",
|
||||
]
|
||||
for (const file of oldhostbridgefiles) {
|
||||
await fs.rm(file, { force: true })
|
||||
await rmrf(file)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,6 +422,10 @@ async function rmdir(path) {
|
||||
}
|
||||
}
|
||||
|
||||
async function rmrf(path) {
|
||||
await fs.rm(path, { force: true, recursive: true })
|
||||
}
|
||||
|
||||
// Check for Apple Silicon compatibility
|
||||
function checkAppleSiliconCompatibility() {
|
||||
// Only run check on macOS
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
|
||||
import * as fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import * as grpc from "@grpc/grpc-js"
|
||||
import * as protoLoader from "@grpc/proto-loader"
|
||||
import chalk from "chalk"
|
||||
|
||||
const DESCRIPTOR_SET = path.resolve("dist-standalone/proto/descriptor_set.pb")
|
||||
import { loadServicesFromProtoDescriptor, getFqn } from "./proto-utils.mjs"
|
||||
|
||||
// Contains the interface definitions for the host bridge clients.
|
||||
const TYPES_FILE = path.resolve("src/generated/hosts/host-bridge-client-types.ts")
|
||||
@@ -15,43 +12,11 @@ const EXTERNAL_CLIENT_FILE = path.resolve("src/generated/hosts/standalone/host-b
|
||||
// Contains the handler map for the external host bridge clients (using the custom service registry).
|
||||
const VSCODE_CLIENT_FILE = path.resolve("src/generated/hosts/vscode/hostbridge-grpc-service-config.ts")
|
||||
|
||||
const typeNameToFQN = new Map()
|
||||
|
||||
function addTypeNameToFqn(name, fqn) {
|
||||
if (typeNameToFQN.has(name)) {
|
||||
throw new Error(`Proto type ${name} redefined (${fqn}).`)
|
||||
}
|
||||
typeNameToFQN.set(name, fqn)
|
||||
}
|
||||
function getFqn(name) {
|
||||
if (!typeNameToFQN.has(name)) {
|
||||
throw Error(`No FQN for ${name}`)
|
||||
}
|
||||
return typeNameToFQN.get(name)
|
||||
}
|
||||
/**
|
||||
* Main function to generate the host bridge client
|
||||
*/
|
||||
async function main() {
|
||||
// Load service definitions from descriptor set
|
||||
const descriptorBuffer = await fs.readFile(DESCRIPTOR_SET)
|
||||
const packageDefinition = protoLoader.loadFileDescriptorSetFromBuffer(descriptorBuffer)
|
||||
const proto = grpc.loadPackageDefinition(packageDefinition)
|
||||
|
||||
// Extract host services and proto messages from the proto definition
|
||||
const hostServices = {}
|
||||
for (const [name, def] of Object.entries(proto.host)) {
|
||||
if (def && "service" in def) {
|
||||
hostServices[name] = def
|
||||
} else {
|
||||
addTypeNameToFqn(name, `proto.host.${name}`)
|
||||
}
|
||||
}
|
||||
for (const [name, def] of Object.entries(proto.cline)) {
|
||||
if (def && !("service" in def)) {
|
||||
addTypeNameToFqn(name, `proto.cline.${name}`)
|
||||
}
|
||||
}
|
||||
const { hostServices } = await loadServicesFromProtoDescriptor()
|
||||
|
||||
await generateTypesFile(hostServices)
|
||||
await generateExternalClientFile(hostServices)
|
||||
@@ -135,6 +100,7 @@ import * as niceGrpc from "@generated/nice-grpc/index"
|
||||
import { StreamingCallbacks } from "@hosts/host-provider-types"
|
||||
import * as proto from "@shared/proto/index"
|
||||
import { Channel, createClient } from "nice-grpc"
|
||||
import { BaseGrpcClient } from "@/hosts/external/grpc-types"
|
||||
|
||||
${imports.join("\n")}
|
||||
|
||||
@@ -158,31 +124,49 @@ function generateExternalClientSetup(serviceName, serviceDefinition) {
|
||||
const isStreamingResponse = methodDef.responseStream
|
||||
|
||||
if (!isStreamingResponse) {
|
||||
return ` ${methodName}(request: ${requestType}): Promise<${responseType}> {
|
||||
return this.client.${methodName}(request)
|
||||
}`
|
||||
return ` ${methodName}(request: ${requestType}): Promise<${responseType}> {
|
||||
return this.makeRequest((client) => client.${methodName}(request))
|
||||
}`
|
||||
} else {
|
||||
// Generate streaming method
|
||||
return ` ${methodName}(request: ${requestType}, callbacks: StreamingCallbacks<${responseType}>): () => void {
|
||||
const abortController = new AbortController()
|
||||
const stream: AsyncIterable<${responseType}> = this.client.${methodName}(request, {signal: abortController.signal})
|
||||
asyncIteratorToCallbacks(stream, callbacks)
|
||||
return () => {abortController.abort()}
|
||||
}`
|
||||
return ` ${methodName}(
|
||||
request: ${requestType},
|
||||
callbacks: StreamingCallbacks<${responseType}>,
|
||||
): () => void {
|
||||
const client = this.getClient()
|
||||
const abortController = new AbortController()
|
||||
const stream: AsyncIterable<${responseType}> = client.${methodName}(request, {
|
||||
signal: abortController.signal,
|
||||
})
|
||||
const wrappedCallbacks: StreamingCallbacks<${responseType}> = {
|
||||
...callbacks,
|
||||
onError: (error: any) => {
|
||||
if (error?.code === "UNAVAILABLE") {
|
||||
this.destroyClient()
|
||||
}
|
||||
callbacks.onError?.(error)
|
||||
},
|
||||
}
|
||||
asyncIteratorToCallbacks(stream, wrappedCallbacks)
|
||||
return () => {
|
||||
abortController.abort()
|
||||
}
|
||||
}\n`
|
||||
}
|
||||
})
|
||||
.join("\n\n")
|
||||
.join("\n")
|
||||
|
||||
// Generate the class
|
||||
return `/**
|
||||
* Type-safe client implementation for ${serviceName}.
|
||||
*/
|
||||
export class ${serviceName}ClientImpl implements ${serviceName}ClientInterface {
|
||||
private client: niceGrpc.host.${serviceName}Client
|
||||
export class ${serviceName}ClientImpl
|
||||
extends BaseGrpcClient<niceGrpc.host.${serviceName}Client>
|
||||
implements ${serviceName}ClientInterface {
|
||||
|
||||
constructor(channel: Channel) {
|
||||
this.client = createClient(niceGrpc.host.${serviceName}Definition, channel)
|
||||
}
|
||||
protected createClient(channel: Channel): niceGrpc.host.${serviceName}Client {
|
||||
return createClient(niceGrpc.host.${serviceName}Definition, channel)
|
||||
}
|
||||
|
||||
${methods}
|
||||
}`
|
||||
|
||||
Regular → Executable
+73
-36
@@ -1,30 +1,74 @@
|
||||
import * as grpc from "@grpc/grpc-js"
|
||||
import * as protoLoader from "@grpc/proto-loader"
|
||||
#!/usr/bin/env node
|
||||
|
||||
import * as fs from "fs"
|
||||
import * as health from "grpc-health-check"
|
||||
import path, { basename, dirname } from "path"
|
||||
import path, { dirname } from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
import { loadServicesFromProtoDescriptor, getFqn } from "./proto-utils.mjs"
|
||||
|
||||
const OUT_FILE = path.resolve("src/generated/standalone/server-setup.ts")
|
||||
const DESCRIPTOR_SET = path.resolve("dist-standalone/proto/descriptor_set.pb")
|
||||
const STANDALONE_SERVER_SETUP_FILE = path.resolve("src/generated/standalone/server-setup.ts")
|
||||
const WEBVIEW_CLIENTS_FILE = path.resolve("webview-ui/src/services/grpc-client.ts")
|
||||
|
||||
// Load service definitions.
|
||||
const clineDef = protoLoader.loadFileDescriptorSetFromBuffer(fs.readFileSync(DESCRIPTOR_SET))
|
||||
const healthDef = protoLoader.loadSync(health.protoPath)
|
||||
const packageDefinition = { ...clineDef, ...healthDef }
|
||||
const proto = grpc.loadPackageDefinition(packageDefinition)
|
||||
const SCRIPT_NAME = path.relative(process.cwd(), fileURLToPath(import.meta.url))
|
||||
|
||||
async function main() {
|
||||
const { protobusServices } = await loadServicesFromProtoDescriptor()
|
||||
await generateWebviewProtobusClients(protobusServices)
|
||||
await generateStandaloneProtobusServiceSetup(protobusServices)
|
||||
|
||||
console.log(`Generated ProtoBus files at:`)
|
||||
console.log(`- ${WEBVIEW_CLIENTS_FILE}`)
|
||||
console.log(`- ${STANDALONE_SERVER_SETUP_FILE}`)
|
||||
}
|
||||
|
||||
async function generateWebviewProtobusClients(protobusServices) {
|
||||
const clients = []
|
||||
|
||||
for (const [serviceName, def] of Object.entries(protobusServices)) {
|
||||
const rpcs = []
|
||||
for (const [rpcName, rpc] of Object.entries(def.service)) {
|
||||
const requestType = getFqn(rpc.requestType.type.name)
|
||||
const responseType = getFqn(rpc.responseType.type.name)
|
||||
|
||||
if (rpc.requestStream) {
|
||||
throw new Error("Request streaming is not supported")
|
||||
}
|
||||
if (!rpc.responseStream) {
|
||||
rpcs.push(` static async ${rpcName}(request: ${requestType}): Promise<${responseType}> {
|
||||
return this.makeRequest("${rpcName}", request, ${requestType}.toJSON, ${responseType}.fromJSON)
|
||||
}`)
|
||||
} else {
|
||||
rpcs.push(` static ${rpcName}(request: ${requestType}, callbacks: Callbacks<${responseType}>): ()=>void {
|
||||
return this.makeStreamingRequest("${rpcName}", request, callbacks, ${requestType}.toJSON, ${responseType}.fromJSON)
|
||||
}`)
|
||||
}
|
||||
}
|
||||
clients.push(`export class ${serviceName}Client extends ProtoBusClient {
|
||||
static override serviceName: string = "${serviceName}"
|
||||
${rpcs.join("\n")}
|
||||
}`)
|
||||
}
|
||||
|
||||
// Create output file
|
||||
let output = `// GENERATED CODE -- DO NOT EDIT!
|
||||
// Generated by ${SCRIPT_NAME}
|
||||
import * as proto from "@shared/proto/index"
|
||||
import { ProtoBusClient, Callbacks } from "./grpc-client-base"
|
||||
|
||||
${clients.join("\n")}
|
||||
`
|
||||
// Write output file
|
||||
fs.mkdirSync(dirname(WEBVIEW_CLIENTS_FILE), { recursive: true })
|
||||
fs.writeFileSync(WEBVIEW_CLIENTS_FILE, output)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate imports and function to add all the handlers to the server for all services defined in the proto files.
|
||||
*/
|
||||
function generateHandlersAndExports() {
|
||||
let imports = []
|
||||
let handlerSetup = []
|
||||
async function generateStandaloneProtobusServiceSetup(protobusServices) {
|
||||
const imports = []
|
||||
const handlerSetup = []
|
||||
|
||||
for (const [name, def] of Object.entries(proto.cline)) {
|
||||
if (!def || !("service" in def)) {
|
||||
continue
|
||||
}
|
||||
for (const [name, def] of Object.entries(protobusServices)) {
|
||||
const domain = name.replace(/Service$/, "")
|
||||
const dir = domain.charAt(0).toLowerCase() + domain.slice(1)
|
||||
imports.push(`// ${domain} Service`)
|
||||
@@ -47,35 +91,28 @@ function generateHandlersAndExports() {
|
||||
imports.push("")
|
||||
handlerSetup.push("")
|
||||
}
|
||||
return {
|
||||
imports: imports.join("\n"),
|
||||
handlerSetup: handlerSetup.join("\n"),
|
||||
}
|
||||
}
|
||||
|
||||
const { imports, handlerSetup } = generateHandlersAndExports()
|
||||
const scriptName = path.basename(fileURLToPath(import.meta.url))
|
||||
|
||||
// Create output file
|
||||
let output = `// GENERATED CODE -- DO NOT EDIT!
|
||||
// Generated by ${scriptName}
|
||||
// Create output file
|
||||
let output = `// GENERATED CODE -- DO NOT EDIT!
|
||||
// Generated by ${SCRIPT_NAME}
|
||||
import * as grpc from "@grpc/grpc-js"
|
||||
import { cline } from "@generated/grpc-js"
|
||||
import { Controller } from "@core/controller"
|
||||
import { GrpcHandlerWrapper, GrpcStreamingResponseHandlerWrapper } from "@/standalone/grpc-types"
|
||||
import { GrpcHandlerWrapper, GrpcStreamingResponseHandlerWrapper } from "@hosts/external/grpc-types"
|
||||
|
||||
${imports}
|
||||
${imports.join("\n")}
|
||||
export function addProtobusServices(
|
||||
server: grpc.Server,
|
||||
controller: Controller,
|
||||
wrapper: GrpcHandlerWrapper,
|
||||
wrapStreamingResponse: GrpcStreamingResponseHandlerWrapper,
|
||||
): void {
|
||||
${handlerSetup}
|
||||
${handlerSetup.join("\n")}
|
||||
}
|
||||
`
|
||||
// Write output file
|
||||
fs.mkdirSync(dirname(OUT_FILE), { recursive: true })
|
||||
fs.writeFileSync(OUT_FILE, output)
|
||||
// Write output file
|
||||
fs.mkdirSync(dirname(STANDALONE_SERVER_SETUP_FILE), { recursive: true })
|
||||
fs.writeFileSync(STANDALONE_SERVER_SETUP_FILE, output)
|
||||
}
|
||||
|
||||
console.log(`Generated service handlers in ${OUT_FILE}.`)
|
||||
main()
|
||||
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import * as fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import * as grpc from "@grpc/grpc-js"
|
||||
import * as protoLoader from "@grpc/proto-loader"
|
||||
|
||||
const DESCRIPTOR_SET = path.resolve("dist-standalone/proto/descriptor_set.pb")
|
||||
|
||||
const typeNameToFQN = new Map()
|
||||
|
||||
function addTypeNameToFqn(name, fqn) {
|
||||
if (typeNameToFQN.has(name)) {
|
||||
throw new Error(`Proto type ${name} redefined (${fqn}).`)
|
||||
}
|
||||
typeNameToFQN.set(name, fqn)
|
||||
}
|
||||
// Get the fully qualified name for a proto type, e.g. getFqn('StringRequest') returns 'cline.StringRequest'
|
||||
export function getFqn(name) {
|
||||
if (!typeNameToFQN.has(name)) {
|
||||
throw Error(`No FQN for ${name}`)
|
||||
}
|
||||
return typeNameToFQN.get(name)
|
||||
}
|
||||
|
||||
export async function loadServicesFromProtoDescriptor() {
|
||||
// Load service definitions from descriptor set
|
||||
const descriptorBuffer = await fs.readFile(DESCRIPTOR_SET)
|
||||
const packageDefinition = protoLoader.loadFileDescriptorSetFromBuffer(descriptorBuffer)
|
||||
const proto = grpc.loadPackageDefinition(packageDefinition)
|
||||
|
||||
// Extract host services and proto messages from the proto definition
|
||||
const hostServices = {}
|
||||
for (const [name, def] of Object.entries(proto.host)) {
|
||||
if (def && "service" in def) {
|
||||
hostServices[name] = def
|
||||
} else {
|
||||
addTypeNameToFqn(name, `proto.host.${name}`)
|
||||
}
|
||||
}
|
||||
const protobusServices = {}
|
||||
for (const [name, def] of Object.entries(proto.cline)) {
|
||||
if (def && "service" in def) {
|
||||
protobusServices[name] = def
|
||||
} else {
|
||||
addTypeNameToFqn(name, `proto.cline.${name}`)
|
||||
}
|
||||
}
|
||||
return { protobusServices, hostServices }
|
||||
}
|
||||
@@ -1,21 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eu #x
|
||||
# This compiles the cline-core app, installs it to the user's home directory,
|
||||
# and runs the service.
|
||||
# This installs the cline-core app to the user's home directory,
|
||||
# and starts the service.
|
||||
|
||||
CORE_DIR=~/.cline/core
|
||||
INSTALL_DIR=$CORE_DIR/0.0.1
|
||||
|
||||
# Build cline core
|
||||
npm run compile-standalone
|
||||
ZIP_FILE=standalone.zip
|
||||
ZIP=dist-standalone/${ZIP_FILE}
|
||||
|
||||
# Remove old unpacked versions to force reinstall
|
||||
rm -rf $CORE_DIR/* || true
|
||||
|
||||
mkdir -p $INSTALL_DIR
|
||||
cp dist-standalone/standalone.zip $INSTALL_DIR
|
||||
cp $ZIP $INSTALL_DIR
|
||||
cd $INSTALL_DIR
|
||||
unp standalone.zip > /dev/null
|
||||
unp $ZIP_FILE > /dev/null
|
||||
|
||||
pkill -f cline-core.js || true
|
||||
NODE_PATH=./node_modules node cline-core.js
|
||||
NODE_PATH=./node_modules DEV_WORKSPACE_FOLDER=/tmp/ node cline-core.js
|
||||
|
||||
+98
-63
@@ -29,6 +29,8 @@ 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 { HuggingFaceHandler } from "./providers/huggingface"
|
||||
|
||||
export interface ApiHandler {
|
||||
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream
|
||||
@@ -40,27 +42,33 @@ export interface SingleCompletionHandler {
|
||||
completePrompt(prompt: string): Promise<string>
|
||||
}
|
||||
|
||||
function createHandlerForProvider(apiProvider: string | undefined, options: Omit<ApiConfiguration, "apiProvider">): ApiHandler {
|
||||
function createHandlerForProvider(
|
||||
apiProvider: string | undefined,
|
||||
options: Omit<ApiConfiguration, "apiProvider">,
|
||||
mode: Mode,
|
||||
): ApiHandler {
|
||||
switch (apiProvider) {
|
||||
case "anthropic":
|
||||
return new AnthropicHandler({
|
||||
apiKey: options.apiKey,
|
||||
anthropicBaseUrl: options.anthropicBaseUrl,
|
||||
apiModelId: options.apiModelId,
|
||||
thinkingBudgetTokens: options.thinkingBudgetTokens,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
})
|
||||
case "openrouter":
|
||||
return new OpenRouterHandler({
|
||||
openRouterApiKey: options.openRouterApiKey,
|
||||
openRouterModelId: options.openRouterModelId,
|
||||
openRouterModelInfo: options.openRouterModelInfo,
|
||||
openRouterModelId: mode === "plan" ? options.planModeOpenRouterModelId : options.actModeOpenRouterModelId,
|
||||
openRouterModelInfo: mode === "plan" ? options.planModeOpenRouterModelInfo : options.actModeOpenRouterModelInfo,
|
||||
openRouterProviderSorting: options.openRouterProviderSorting,
|
||||
reasoningEffort: options.reasoningEffort,
|
||||
thinkingBudgetTokens: options.thinkingBudgetTokens,
|
||||
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
})
|
||||
case "bedrock":
|
||||
return new AwsBedrockHandler({
|
||||
apiModelId: options.apiModelId,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
awsAccessKey: options.awsAccessKey,
|
||||
awsSecretKey: options.awsSecretKey,
|
||||
awsSessionToken: options.awsSessionToken,
|
||||
@@ -72,16 +80,20 @@ function createHandlerForProvider(apiProvider: string | undefined, options: Omit
|
||||
awsUseProfile: options.awsUseProfile,
|
||||
awsProfile: options.awsProfile,
|
||||
awsBedrockEndpoint: options.awsBedrockEndpoint,
|
||||
awsBedrockCustomSelected: options.awsBedrockCustomSelected,
|
||||
awsBedrockCustomModelBaseId: options.awsBedrockCustomModelBaseId,
|
||||
thinkingBudgetTokens: options.thinkingBudgetTokens,
|
||||
awsBedrockCustomSelected:
|
||||
mode === "plan" ? options.planModeAwsBedrockCustomSelected : options.actModeAwsBedrockCustomSelected,
|
||||
awsBedrockCustomModelBaseId:
|
||||
mode === "plan" ? options.planModeAwsBedrockCustomModelBaseId : options.actModeAwsBedrockCustomModelBaseId,
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
})
|
||||
case "vertex":
|
||||
return new VertexHandler({
|
||||
vertexProjectId: options.vertexProjectId,
|
||||
vertexRegion: options.vertexRegion,
|
||||
apiModelId: options.apiModelId,
|
||||
thinkingBudgetTokens: options.thinkingBudgetTokens,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
geminiApiKey: options.geminiApiKey,
|
||||
geminiBaseUrl: options.geminiBaseUrl,
|
||||
taskId: options.taskId,
|
||||
@@ -92,21 +104,21 @@ function createHandlerForProvider(apiProvider: string | undefined, options: Omit
|
||||
openAiBaseUrl: options.openAiBaseUrl,
|
||||
azureApiVersion: options.azureApiVersion,
|
||||
openAiHeaders: options.openAiHeaders,
|
||||
openAiModelId: options.openAiModelId,
|
||||
openAiModelInfo: options.openAiModelInfo,
|
||||
reasoningEffort: options.reasoningEffort,
|
||||
openAiModelId: mode === "plan" ? options.planModeOpenAiModelId : options.actModeOpenAiModelId,
|
||||
openAiModelInfo: mode === "plan" ? options.planModeOpenAiModelInfo : options.actModeOpenAiModelInfo,
|
||||
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
|
||||
})
|
||||
case "ollama":
|
||||
return new OllamaHandler({
|
||||
ollamaBaseUrl: options.ollamaBaseUrl,
|
||||
ollamaModelId: options.ollamaModelId,
|
||||
ollamaModelId: mode === "plan" ? options.planModeOllamaModelId : options.actModeOllamaModelId,
|
||||
ollamaApiOptionsCtxNum: options.ollamaApiOptionsCtxNum,
|
||||
requestTimeoutMs: options.requestTimeoutMs,
|
||||
})
|
||||
case "lmstudio":
|
||||
return new LmStudioHandler({
|
||||
lmStudioBaseUrl: options.lmStudioBaseUrl,
|
||||
lmStudioModelId: options.lmStudioModelId,
|
||||
lmStudioModelId: mode === "plan" ? options.planModeLmStudioModelId : options.actModeLmStudioModelId,
|
||||
})
|
||||
case "gemini":
|
||||
return new GeminiHandler({
|
||||
@@ -114,78 +126,85 @@ function createHandlerForProvider(apiProvider: string | undefined, options: Omit
|
||||
vertexRegion: options.vertexRegion,
|
||||
geminiApiKey: options.geminiApiKey,
|
||||
geminiBaseUrl: options.geminiBaseUrl,
|
||||
thinkingBudgetTokens: options.thinkingBudgetTokens,
|
||||
apiModelId: options.apiModelId,
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
taskId: options.taskId,
|
||||
})
|
||||
case "openai-native":
|
||||
return new OpenAiNativeHandler({
|
||||
openAiNativeApiKey: options.openAiNativeApiKey,
|
||||
reasoningEffort: options.reasoningEffort,
|
||||
apiModelId: options.apiModelId,
|
||||
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
})
|
||||
case "deepseek":
|
||||
return new DeepSeekHandler({
|
||||
deepSeekApiKey: options.deepSeekApiKey,
|
||||
apiModelId: options.apiModelId,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
})
|
||||
case "requesty":
|
||||
return new RequestyHandler({
|
||||
requestyApiKey: options.requestyApiKey,
|
||||
reasoningEffort: options.reasoningEffort,
|
||||
thinkingBudgetTokens: options.thinkingBudgetTokens,
|
||||
requestyModelId: options.requestyModelId,
|
||||
requestyModelInfo: options.requestyModelInfo,
|
||||
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
requestyModelId: mode === "plan" ? options.planModeRequestyModelId : options.actModeRequestyModelId,
|
||||
requestyModelInfo: mode === "plan" ? options.planModeRequestyModelInfo : options.actModeRequestyModelInfo,
|
||||
})
|
||||
case "fireworks":
|
||||
return new FireworksHandler({
|
||||
fireworksApiKey: options.fireworksApiKey,
|
||||
fireworksModelId: options.fireworksModelId,
|
||||
fireworksModelId: mode === "plan" ? options.planModeFireworksModelId : options.actModeFireworksModelId,
|
||||
fireworksModelMaxCompletionTokens: options.fireworksModelMaxCompletionTokens,
|
||||
fireworksModelMaxTokens: options.fireworksModelMaxTokens,
|
||||
})
|
||||
case "together":
|
||||
return new TogetherHandler({
|
||||
togetherApiKey: options.togetherApiKey,
|
||||
togetherModelId: options.togetherModelId,
|
||||
togetherModelId: mode === "plan" ? options.planModeTogetherModelId : options.actModeTogetherModelId,
|
||||
})
|
||||
case "qwen":
|
||||
return new QwenHandler({
|
||||
qwenApiKey: options.qwenApiKey,
|
||||
qwenApiLine: options.qwenApiLine,
|
||||
apiModelId: options.apiModelId,
|
||||
thinkingBudgetTokens: options.thinkingBudgetTokens,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
})
|
||||
case "doubao":
|
||||
return new DoubaoHandler({
|
||||
doubaoApiKey: options.doubaoApiKey,
|
||||
apiModelId: options.apiModelId,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
})
|
||||
case "mistral":
|
||||
return new MistralHandler({
|
||||
mistralApiKey: options.mistralApiKey,
|
||||
apiModelId: options.apiModelId,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
})
|
||||
case "vscode-lm":
|
||||
return new VsCodeLmHandler({
|
||||
vsCodeLmModelSelector: options.vsCodeLmModelSelector,
|
||||
vsCodeLmModelSelector:
|
||||
mode === "plan" ? options.planModeVsCodeLmModelSelector : options.actModeVsCodeLmModelSelector,
|
||||
})
|
||||
case "cline":
|
||||
return new ClineHandler({
|
||||
clineAccountId: options.clineAccountId,
|
||||
taskId: options.taskId,
|
||||
reasoningEffort: options.reasoningEffort,
|
||||
thinkingBudgetTokens: options.thinkingBudgetTokens,
|
||||
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
openRouterProviderSorting: options.openRouterProviderSorting,
|
||||
openRouterModelId: options.openRouterModelId,
|
||||
openRouterModelInfo: options.openRouterModelInfo,
|
||||
openRouterModelId: mode === "plan" ? options.planModeOpenRouterModelId : options.actModeOpenRouterModelId,
|
||||
openRouterModelInfo: mode === "plan" ? options.planModeOpenRouterModelInfo : options.actModeOpenRouterModelInfo,
|
||||
})
|
||||
case "litellm":
|
||||
return new LiteLlmHandler({
|
||||
liteLlmApiKey: options.liteLlmApiKey,
|
||||
liteLlmBaseUrl: options.liteLlmBaseUrl,
|
||||
liteLlmModelId: options.liteLlmModelId,
|
||||
liteLlmModelInfo: options.liteLlmModelInfo,
|
||||
thinkingBudgetTokens: options.thinkingBudgetTokens,
|
||||
liteLlmModelId: mode === "plan" ? options.planModeLiteLlmModelId : options.actModeLiteLlmModelId,
|
||||
liteLlmModelInfo: mode === "plan" ? options.planModeLiteLlmModelInfo : options.actModeLiteLlmModelInfo,
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
liteLlmUsePromptCache: options.liteLlmUsePromptCache,
|
||||
taskId: options.taskId,
|
||||
})
|
||||
@@ -193,41 +212,48 @@ function createHandlerForProvider(apiProvider: string | undefined, options: Omit
|
||||
return new MoonshotHandler({
|
||||
moonshotApiKey: options.moonshotApiKey,
|
||||
moonshotApiLine: options.moonshotApiLine,
|
||||
apiModelId: options.apiModelId,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
})
|
||||
case "huggingface":
|
||||
return new HuggingFaceHandler({
|
||||
huggingFaceApiKey: options.huggingFaceApiKey,
|
||||
huggingFaceModelId: mode === "plan" ? options.planModeHuggingFaceModelId : options.actModeHuggingFaceModelId,
|
||||
huggingFaceModelInfo:
|
||||
mode === "plan" ? options.planModeHuggingFaceModelInfo : options.actModeHuggingFaceModelInfo,
|
||||
})
|
||||
case "nebius":
|
||||
return new NebiusHandler({
|
||||
nebiusApiKey: options.nebiusApiKey,
|
||||
apiModelId: options.apiModelId,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
})
|
||||
case "asksage":
|
||||
return new AskSageHandler({
|
||||
asksageApiKey: options.asksageApiKey,
|
||||
asksageApiUrl: options.asksageApiUrl,
|
||||
apiModelId: options.apiModelId,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
})
|
||||
case "xai":
|
||||
return new XAIHandler({
|
||||
xaiApiKey: options.xaiApiKey,
|
||||
reasoningEffort: options.reasoningEffort,
|
||||
apiModelId: options.apiModelId,
|
||||
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
})
|
||||
case "sambanova":
|
||||
return new SambanovaHandler({
|
||||
sambanovaApiKey: options.sambanovaApiKey,
|
||||
apiModelId: options.apiModelId,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
})
|
||||
case "cerebras":
|
||||
return new CerebrasHandler({
|
||||
cerebrasApiKey: options.cerebrasApiKey,
|
||||
apiModelId: options.apiModelId,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
})
|
||||
case "groq":
|
||||
return new GroqHandler({
|
||||
groqApiKey: options.groqApiKey,
|
||||
groqModelId: options.groqModelId,
|
||||
groqModelInfo: options.groqModelInfo,
|
||||
apiModelId: options.apiModelId,
|
||||
groqModelId: mode === "plan" ? options.planModeGroqModelId : options.actModeGroqModelId,
|
||||
groqModelInfo: mode === "plan" ? options.planModeGroqModelInfo : options.actModeGroqModelInfo,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
})
|
||||
case "sapaicore":
|
||||
return new SapAiCoreHandler({
|
||||
@@ -236,37 +262,46 @@ function createHandlerForProvider(apiProvider: string | undefined, options: Omit
|
||||
sapAiCoreTokenUrl: options.sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup: options.sapAiResourceGroup,
|
||||
sapAiCoreBaseUrl: options.sapAiCoreBaseUrl,
|
||||
apiModelId: options.apiModelId,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
})
|
||||
case "claude-code":
|
||||
return new ClaudeCodeHandler({
|
||||
claudeCodePath: options.claudeCodePath,
|
||||
apiModelId: options.apiModelId,
|
||||
thinkingBudgetTokens: options.thinkingBudgetTokens,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
})
|
||||
default:
|
||||
return new AnthropicHandler({
|
||||
apiKey: options.apiKey,
|
||||
anthropicBaseUrl: options.anthropicBaseUrl,
|
||||
apiModelId: options.apiModelId,
|
||||
thinkingBudgetTokens: options.thinkingBudgetTokens,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
|
||||
const { apiProvider, ...options } = configuration
|
||||
export function buildApiHandler(configuration: ApiConfiguration, mode: Mode): ApiHandler {
|
||||
const { planModeApiProvider, actModeApiProvider, ...options } = configuration
|
||||
|
||||
const apiProvider = mode === "plan" ? planModeApiProvider : actModeApiProvider
|
||||
|
||||
// Validate thinking budget tokens against model's maxTokens to prevent API errors
|
||||
// wrapped in a try-catch for safety, but this should never throw
|
||||
try {
|
||||
if (options.thinkingBudgetTokens && options.thinkingBudgetTokens > 0) {
|
||||
const handler = createHandlerForProvider(apiProvider, options)
|
||||
const thinkingBudgetTokens = mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens
|
||||
if (thinkingBudgetTokens && thinkingBudgetTokens > 0) {
|
||||
const handler = createHandlerForProvider(apiProvider, options, mode)
|
||||
|
||||
const modelInfo = handler.getModel().info
|
||||
if (modelInfo.maxTokens && options.thinkingBudgetTokens > modelInfo.maxTokens) {
|
||||
if (modelInfo.maxTokens && thinkingBudgetTokens > modelInfo.maxTokens) {
|
||||
const clippedValue = modelInfo.maxTokens - 1
|
||||
options.thinkingBudgetTokens = clippedValue
|
||||
if (mode === "plan") {
|
||||
options.planModeThinkingBudgetTokens = clippedValue
|
||||
} else {
|
||||
options.actModeThinkingBudgetTokens = clippedValue
|
||||
}
|
||||
} else {
|
||||
return handler // don't rebuild unless its necessary
|
||||
}
|
||||
@@ -275,5 +310,5 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
|
||||
console.error("buildApiHandler error:", error)
|
||||
}
|
||||
|
||||
return createHandlerForProvider(apiProvider, options)
|
||||
return createHandlerForProvider(apiProvider, options, mode)
|
||||
}
|
||||
|
||||
@@ -203,7 +203,7 @@ describe("AwsBedrockHandler", () => {
|
||||
})
|
||||
|
||||
const mockOptions: ApiHandlerOptions = {
|
||||
apiModelId: "anthropic.claude-3-7-sonnet-20250219-v1:0",
|
||||
actModeApiModelId: "anthropic.claude-3-7-sonnet-20250219-v1:0",
|
||||
awsRegion: "us-east-1",
|
||||
awsAccessKey: "test-key",
|
||||
awsSecretKey: "test-secret",
|
||||
@@ -214,9 +214,9 @@ describe("AwsBedrockHandler", () => {
|
||||
awsBedrockUsePromptCache: false,
|
||||
awsUseCrossRegionInference: false,
|
||||
awsBedrockEndpoint: "",
|
||||
awsBedrockCustomSelected: false,
|
||||
awsBedrockCustomModelBaseId: undefined,
|
||||
thinkingBudgetTokens: 1600,
|
||||
actModeAwsBedrockCustomSelected: false,
|
||||
actModeAwsBedrockCustomModelBaseId: undefined,
|
||||
actModeThinkingBudgetTokens: 1600,
|
||||
}
|
||||
|
||||
const mockModelInfo = {
|
||||
@@ -616,8 +616,8 @@ describe("AwsBedrockHandler", () => {
|
||||
it("should return raw model ID for custom models", async () => {
|
||||
const customOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
awsBedrockCustomSelected: true,
|
||||
apiModelId:
|
||||
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)
|
||||
@@ -631,8 +631,8 @@ describe("AwsBedrockHandler", () => {
|
||||
it("should not encode custom model IDs with slashes", async () => {
|
||||
const customOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
awsBedrockCustomSelected: true,
|
||||
apiModelId: "my-namespace/my-custom-model",
|
||||
actModeAwsBedrockCustomSelected: true,
|
||||
actModeApiModelId: "my-namespace/my-custom-model",
|
||||
}
|
||||
const customHandler = new AwsBedrockHandler(customOptions)
|
||||
|
||||
@@ -680,8 +680,8 @@ describe("AwsBedrockHandler", () => {
|
||||
it("should not apply cross-region prefix for custom models even when enabled", async () => {
|
||||
const customCrossRegionOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
awsBedrockCustomSelected: true,
|
||||
apiModelId: "arn:aws:bedrock:us-west-2:123456789012:custom-model/my-model",
|
||||
actModeAwsBedrockCustomSelected: true,
|
||||
actModeApiModelId: "arn:aws:bedrock:us-west-2:123456789012:custom-model/my-model",
|
||||
awsUseCrossRegionInference: true,
|
||||
}
|
||||
const customCrossRegionHandler = new AwsBedrockHandler(customCrossRegionOptions)
|
||||
@@ -693,10 +693,10 @@ describe("AwsBedrockHandler", () => {
|
||||
it("should handle UltraThink model ARN correctly", async () => {
|
||||
const ultraThinkOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
awsBedrockCustomSelected: true,
|
||||
apiModelId:
|
||||
actModeAwsBedrockCustomSelected: true,
|
||||
actModeApiModelId:
|
||||
"arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
|
||||
awsBedrockCustomModelBaseId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
|
||||
actModeAwsBedrockCustomModelBaseId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
|
||||
}
|
||||
const ultraThinkHandler = new AwsBedrockHandler(ultraThinkOptions)
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ describe("OllamaHandler", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
options = {
|
||||
ollamaModelId: "llama2",
|
||||
actModeOllamaModelId: "llama2",
|
||||
ollamaBaseUrl: "http://localhost:11434",
|
||||
}
|
||||
handler = new OllamaHandler(options)
|
||||
|
||||
@@ -13,7 +13,7 @@ interface AnthropicHandlerOptions {
|
||||
}
|
||||
|
||||
export class AnthropicHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private options: AnthropicHandlerOptions
|
||||
private client: Anthropic | undefined
|
||||
|
||||
constructor(options: AnthropicHandlerOptions) {
|
||||
|
||||
@@ -56,7 +56,7 @@ export class CerebrasHandler implements ApiHandler {
|
||||
|
||||
// Check if this is a reasoning model that uses thinking tags
|
||||
const modelId = this.getModel().id
|
||||
const isReasoningModel = modelId.includes("qwen") || modelId.includes("deepseek-r1-distill")
|
||||
const isReasoningModel = modelId.includes("qwen")
|
||||
|
||||
// Convert Anthropic messages to Cerebras format
|
||||
for (const message of messages) {
|
||||
|
||||
@@ -177,17 +177,7 @@ export class ClineHandler implements ApiHandler {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Cline API Error:", error)
|
||||
const requestId = error?.request_id ? `\n | Request ID: ${error.request_id}` : ""
|
||||
if (error.code === "ERR_BAD_REQUEST" || error.status === 401) {
|
||||
throw new Error(CLINE_ACCOUNT_AUTH_ERROR_MESSAGE + requestId)
|
||||
} else if (error.code === "insufficient_credits" || error.status === 402) {
|
||||
if (error.error) {
|
||||
throw new Error(JSON.stringify(error.error))
|
||||
}
|
||||
}
|
||||
const _error = error instanceof Error ? error : new Error(String(error))
|
||||
_error.message = _error.message + requestId
|
||||
throw _error
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ interface GeminiHandlerOptions {
|
||||
* 4. Separating immediate costs from ongoing costs to avoid double-counting
|
||||
*/
|
||||
export class GeminiHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private options: GeminiHandlerOptions
|
||||
private client: GoogleGenAI | undefined
|
||||
|
||||
constructor(options: GeminiHandlerOptions) {
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiHandler } from "../"
|
||||
import { ApiHandlerOptions, HuggingFaceModelId, ModelInfo, huggingFaceDefaultModelId, huggingFaceModels } from "@shared/api"
|
||||
import { calculateApiCostOpenAI } from "../../utils/cost"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
interface HuggingFaceHandlerOptions {
|
||||
huggingFaceApiKey?: string
|
||||
huggingFaceModelId?: string
|
||||
huggingFaceModelInfo?: ModelInfo
|
||||
}
|
||||
|
||||
export class HuggingFaceHandler implements ApiHandler {
|
||||
private options: HuggingFaceHandlerOptions
|
||||
private client: OpenAI | undefined
|
||||
private cachedModel: { id: HuggingFaceModelId; info: ModelInfo } | undefined
|
||||
|
||||
constructor(options: HuggingFaceHandlerOptions) {
|
||||
this.options = options
|
||||
}
|
||||
|
||||
private ensureClient(): OpenAI {
|
||||
if (!this.client) {
|
||||
if (!this.options.huggingFaceApiKey) {
|
||||
throw new Error("Hugging Face API key is required")
|
||||
}
|
||||
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://router.huggingface.co/v1",
|
||||
apiKey: this.options.huggingFaceApiKey,
|
||||
defaultHeaders: {
|
||||
"User-Agent": "Cline/1.0",
|
||||
},
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw new Error(`Error creating Hugging Face client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
|
||||
private async *yieldUsage(info: ModelInfo, usage: OpenAI.Completions.CompletionUsage | undefined): ApiStream {
|
||||
if (!usage) {
|
||||
return
|
||||
}
|
||||
|
||||
const inputTokens = usage.prompt_tokens || 0
|
||||
const outputTokens = usage.completion_tokens || 0
|
||||
const totalCost = calculateApiCostOpenAI(info, inputTokens, outputTokens)
|
||||
|
||||
const usageData = {
|
||||
type: "usage" as const,
|
||||
inputTokens: inputTokens,
|
||||
outputTokens: outputTokens,
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
totalCost: totalCost,
|
||||
}
|
||||
|
||||
yield usageData
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
try {
|
||||
const client = this.ensureClient()
|
||||
const model = this.getModel()
|
||||
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
const requestParams = {
|
||||
model: model.id,
|
||||
max_tokens: model.info.maxTokens,
|
||||
messages: openAiMessages,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
temperature: 0,
|
||||
}
|
||||
|
||||
const stream = (await client.chat.completions.create(requestParams)) as any
|
||||
|
||||
let chunkCount = 0
|
||||
let totalContent = ""
|
||||
|
||||
for await (const chunk of stream) {
|
||||
chunkCount++
|
||||
const delta = chunk.choices[0]?.delta
|
||||
if (delta?.content) {
|
||||
totalContent += delta.content
|
||||
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
yield* this.yieldUsage(model.info, chunk.usage)
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
getModel(): { id: HuggingFaceModelId; info: ModelInfo } {
|
||||
// Return cached model if available
|
||||
if (this.cachedModel) {
|
||||
return this.cachedModel
|
||||
}
|
||||
|
||||
const modelId = this.options.huggingFaceModelId
|
||||
|
||||
// List all available models for debugging
|
||||
const availableModels = Object.keys(huggingFaceModels)
|
||||
let result: { id: HuggingFaceModelId; info: ModelInfo }
|
||||
|
||||
if (modelId && modelId in huggingFaceModels) {
|
||||
const id = modelId as HuggingFaceModelId
|
||||
const modelInfo = huggingFaceModels[id]
|
||||
result = { id, info: modelInfo }
|
||||
} else {
|
||||
const defaultInfo = huggingFaceModels[huggingFaceDefaultModelId]
|
||||
result = {
|
||||
id: huggingFaceDefaultModelId,
|
||||
info: defaultInfo,
|
||||
}
|
||||
}
|
||||
|
||||
// Cache the result for future calls
|
||||
this.cachedModel = result
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,15 @@ interface OpenRouterHandlerOptions {
|
||||
thinkingBudgetTokens?: number
|
||||
}
|
||||
|
||||
interface OpenRouterHandlerOptions {
|
||||
openRouterApiKey?: string
|
||||
openRouterModelId?: string
|
||||
openRouterModelInfo?: ModelInfo
|
||||
openRouterProviderSorting?: string
|
||||
reasoningEffort?: string
|
||||
thinkingBudgetTokens?: number
|
||||
}
|
||||
|
||||
export class OpenRouterHandler implements ApiHandler {
|
||||
private options: OpenRouterHandlerOptions
|
||||
private client: OpenAI | undefined
|
||||
|
||||
@@ -14,6 +14,12 @@ interface XAIHandlerOptions {
|
||||
apiModelId?: string
|
||||
}
|
||||
|
||||
interface XAIHandlerOptions {
|
||||
xaiApiKey?: string
|
||||
reasoningEffort?: string
|
||||
apiModelId?: string
|
||||
}
|
||||
|
||||
export class XAIHandler implements ApiHandler {
|
||||
private options: XAIHandlerOptions
|
||||
private client: OpenAI | undefined
|
||||
|
||||
+63
-41
@@ -1,6 +1,8 @@
|
||||
export type Environment = "production" | "staging" | "local"
|
||||
|
||||
const CURRENT_ENVIRONMENT: Environment = "production"
|
||||
export enum Environment {
|
||||
production = "production",
|
||||
staging = "staging",
|
||||
local = "local",
|
||||
}
|
||||
|
||||
interface EnvironmentConfig {
|
||||
appBaseUrl: string
|
||||
@@ -16,43 +18,63 @@ interface EnvironmentConfig {
|
||||
}
|
||||
}
|
||||
|
||||
const configs: Record<Environment, EnvironmentConfig> = {
|
||||
production: {
|
||||
appBaseUrl: "https://app.cline.bot",
|
||||
apiBaseUrl: "https://api.cline.bot",
|
||||
mcpBaseUrl: "https://api.cline.bot/v1/mcp",
|
||||
firebase: {
|
||||
apiKey: "AIzaSyC5rx59Xt8UgwdU3PCfzUF7vCwmp9-K2vk",
|
||||
authDomain: "cline-prod.firebaseapp.com",
|
||||
projectId: "cline-prod",
|
||||
storageBucket: "cline-prod.firebasestorage.app",
|
||||
messagingSenderId: "941048379330",
|
||||
appId: "1:941048379330:web:45058eedeefc5cdfcc485b",
|
||||
},
|
||||
},
|
||||
staging: {
|
||||
appBaseUrl: "https://staging-app.cline.bot",
|
||||
apiBaseUrl: "https://core-api.staging.int.cline.bot",
|
||||
mcpBaseUrl: "https://api.cline.bot/v1/mcp",
|
||||
firebase: {
|
||||
apiKey: "AIzaSyASSwkwX1kSO8vddjZkE5N19QU9cVQ0CIk",
|
||||
authDomain: "cline-staging.firebaseapp.com",
|
||||
projectId: "cline-staging",
|
||||
storageBucket: "cline-staging.firebasestorage.app",
|
||||
messagingSenderId: "853479478430",
|
||||
appId: "1:853479478430:web:2de0dba1c63c3262d4578f",
|
||||
},
|
||||
},
|
||||
local: {
|
||||
appBaseUrl: "http://localhost:3000",
|
||||
apiBaseUrl: "http://localhost:7777",
|
||||
mcpBaseUrl: "https://api.cline.bot/v1/mcp",
|
||||
firebase: {
|
||||
apiKey: "AIzaSyD8wtkd1I-EICuAg6xgAQpRdwYTvwxZG2w",
|
||||
authDomain: "cline-preview.firebaseapp.com",
|
||||
projectId: "cline-preview",
|
||||
},
|
||||
},
|
||||
function getClineEnv(): Environment {
|
||||
const _env = process?.env?.CLINE_ENVIRONMENT
|
||||
if (_env && Object.values(Environment).includes(_env as Environment)) {
|
||||
return _env as Environment
|
||||
}
|
||||
return Environment.production
|
||||
}
|
||||
|
||||
export const clineEnvConfig = configs[CURRENT_ENVIRONMENT]
|
||||
// Config getter function to avoid storing all configs in memory
|
||||
function getEnvironmentConfig(env: Environment): EnvironmentConfig {
|
||||
switch (env) {
|
||||
case Environment.staging:
|
||||
return {
|
||||
appBaseUrl: "https://staging-app.cline.bot",
|
||||
apiBaseUrl: "https://core-api.staging.int.cline.bot",
|
||||
mcpBaseUrl: "https://api.cline.bot/v1/mcp",
|
||||
firebase: {
|
||||
apiKey: "AIzaSyASSwkwX1kSO8vddjZkE5N19QU9cVQ0CIk",
|
||||
authDomain: "cline-staging.firebaseapp.com",
|
||||
projectId: "cline-staging",
|
||||
storageBucket: "cline-staging.firebasestorage.app",
|
||||
messagingSenderId: "853479478430",
|
||||
appId: "1:853479478430:web:2de0dba1c63c3262d4578f",
|
||||
},
|
||||
}
|
||||
case Environment.local:
|
||||
return {
|
||||
appBaseUrl: "http://localhost:3000",
|
||||
apiBaseUrl: "http://localhost:7777",
|
||||
mcpBaseUrl: "https://api.cline.bot/v1/mcp",
|
||||
firebase: {
|
||||
apiKey: "AIzaSyD8wtkd1I-EICuAg6xgAQpRdwYTvwxZG2w",
|
||||
authDomain: "cline-preview.firebaseapp.com",
|
||||
projectId: "cline-preview",
|
||||
},
|
||||
}
|
||||
default:
|
||||
return {
|
||||
appBaseUrl: "https://app.cline.bot",
|
||||
apiBaseUrl: "https://api.cline.bot",
|
||||
mcpBaseUrl: "https://api.cline.bot/v1/mcp",
|
||||
firebase: {
|
||||
apiKey: "AIzaSyC5rx59Xt8UgwdU3PCfzUF7vCwmp9-K2vk",
|
||||
authDomain: "cline-prod.firebaseapp.com",
|
||||
projectId: "cline-prod",
|
||||
storageBucket: "cline-prod.firebasestorage.app",
|
||||
messagingSenderId: "941048379330",
|
||||
appId: "1:941048379330:web:45058eedeefc5cdfcc485b",
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get environment once at module load
|
||||
const CLINE_ENVIRONMENT = getClineEnv()
|
||||
const _configCache = getEnvironmentConfig(CLINE_ENVIRONMENT)
|
||||
|
||||
console.info("Cline environment:", CLINE_ENVIRONMENT)
|
||||
|
||||
export const clineEnvConfig = _configCache
|
||||
|
||||
@@ -6,8 +6,8 @@ import * as path from "path"
|
||||
import { FileContextTracker } from "./FileContextTracker"
|
||||
import * as diskModule from "@core/storage/disk"
|
||||
import type { TaskMetadata, FileMetadataEntry } from "./ContextTrackerTypes"
|
||||
import type { DiffViewProviderCreator, WebviewProviderCreator } from "@/hosts/host-providers"
|
||||
import * as hostProviders from "@hosts/host-providers"
|
||||
import type { DiffViewProviderCreator, WebviewProviderCreator } from "@/hosts/host-provider"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { vscodeHostBridgeClient } from "@/hosts/vscode/hostbridge/client/host-grpc-client"
|
||||
|
||||
describe("FileContextTracker", () => {
|
||||
@@ -53,7 +53,10 @@ describe("FileContextTracker", () => {
|
||||
mockTaskMetadata = { files_in_context: [], model_usage: [] }
|
||||
getTaskMetadataStub = sandbox.stub(diskModule, "getTaskMetadata").resolves(mockTaskMetadata)
|
||||
saveTaskMetadataStub = sandbox.stub(diskModule, "saveTaskMetadata").resolves()
|
||||
hostProviders.initializeHostProviders(
|
||||
|
||||
// Reset HostProvider before initializing to avoid "already initialized" errors
|
||||
HostProvider.reset()
|
||||
HostProvider.initialize(
|
||||
((_) => {}) as WebviewProviderCreator,
|
||||
(() => {}) as DiffViewProviderCreator,
|
||||
vscodeHostBridgeClient,
|
||||
@@ -66,6 +69,8 @@ describe("FileContextTracker", () => {
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore()
|
||||
// Reset HostProvider after each test to ensure clean state
|
||||
HostProvider.reset()
|
||||
})
|
||||
|
||||
it("should add a record when a file is read by a tool", async () => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { getWorkspaceState, updateWorkspaceState } from "@core/storage/state"
|
||||
import { getGlobalState } from "@core/storage/state"
|
||||
import type { FileMetadataEntry } from "./ContextTrackerTypes"
|
||||
import type { ClineMessage } from "@shared/ExtensionMessage"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { getCwd } from "@/utils/path"
|
||||
|
||||
// This class is responsible for tracking file operations that may result in stale context.
|
||||
|
||||
@@ -240,7 +240,7 @@ export async function deleteRuleFile(
|
||||
}
|
||||
|
||||
// Delete the file from disk
|
||||
await fs.unlink(rulePath)
|
||||
await fs.rm(rulePath, { force: true })
|
||||
|
||||
// Get the filename for messages
|
||||
const fileName = path.basename(rulePath)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Controller } from "../index"
|
||||
import { AuthService } from "@/services/auth/AuthService"
|
||||
import { EmptyRequest, String } from "../../../shared/proto/common"
|
||||
import { openExternal } from "@utils/env"
|
||||
|
||||
const authService = AuthService.getInstance()
|
||||
|
||||
@@ -13,6 +12,6 @@ const authService = AuthService.getInstance()
|
||||
* @param controller The controller instance.
|
||||
* @returns The login URL as a string.
|
||||
*/
|
||||
export async function accountLoginClicked(controller: Controller, _: EmptyRequest): Promise<String> {
|
||||
export async function accountLoginClicked(_controller: Controller, _: EmptyRequest): Promise<String> {
|
||||
return await authService.createAuthRequest()
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { Controller } from ".."
|
||||
import { ClineCheckpointRestore } from "../../../shared/WebviewMessage"
|
||||
import { CheckpointRestoreRequest } from "../../../shared/proto/checkpoints"
|
||||
import { Empty } from "../../../shared/proto/common"
|
||||
import pWaitFor from "p-wait-for"
|
||||
import { ShowMessageType } from "@/shared/proto/index.host"
|
||||
|
||||
export async function checkpointRestore(controller: Controller, request: CheckpointRestoreRequest): Promise<Empty> {
|
||||
await controller.cancelTask() // we cannot alter message history say if the task is active, as it could be in the middle of editing a file or running a command, which expect the ask to be responded to rather than being superseded by a new message eg add deleted_api_reqs
|
||||
@@ -11,8 +13,13 @@ export async function checkpointRestore(controller: Controller, request: Checkpo
|
||||
// wait for messages to be loaded
|
||||
await pWaitFor(() => controller.task?.taskState.isInitialized === true, {
|
||||
timeout: 3_000,
|
||||
}).catch(() => {
|
||||
console.error("Failed to init new cline instance")
|
||||
}).catch((error) => {
|
||||
console.log("Failed to init new Cline instance to restore checkpoint", error)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Failed to restore checkpoint",
|
||||
})
|
||||
throw error
|
||||
})
|
||||
|
||||
// NOTE: cancelTask awaits abortTask, which awaits diffViewProvider.revertChanges, which reverts any edited files, allowing us to reset to a checkpoint rather than running into a state where the revertChanges function is called alongside or after the checkpoint reset
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import * as vscode from "vscode"
|
||||
import { Controller } from ".."
|
||||
import { Empty, StringRequest } from "../../../shared/proto/common"
|
||||
import { Empty, StringRequest } from "@shared/proto/common"
|
||||
import { writeTextToClipboard } from "@/utils/env"
|
||||
|
||||
/**
|
||||
@@ -9,7 +8,7 @@ import { writeTextToClipboard } from "@/utils/env"
|
||||
* @param request The request containing the text to copy
|
||||
* @returns Empty response
|
||||
*/
|
||||
export async function copyToClipboard(controller: Controller, request: StringRequest): Promise<Empty> {
|
||||
export async function copyToClipboard(_controller: Controller, request: StringRequest): Promise<Empty> {
|
||||
try {
|
||||
if (request.value) {
|
||||
await writeTextToClipboard(request.value)
|
||||
|
||||
@@ -7,7 +7,7 @@ import * as path from "path"
|
||||
import { handleFileServiceRequest } from "./index"
|
||||
import { refreshWorkflowToggles } from "@/core/context/instructions/user-instructions/workflows"
|
||||
import { getCwd, getDesktopDir } from "@/utils/path"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
|
||||
/**
|
||||
@@ -44,12 +44,10 @@ export const createRuleFile: FileMethodHandler = async (controller: Controller,
|
||||
|
||||
if (fileExists) {
|
||||
const message = `${fileTypeName} file "${request.filename}" already exists.`
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.WARNING,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.WARNING,
|
||||
message,
|
||||
})
|
||||
// Still open it for editing
|
||||
await handleFileServiceRequest(controller, "openFile", { value: filePath })
|
||||
} else {
|
||||
@@ -63,12 +61,10 @@ export const createRuleFile: FileMethodHandler = async (controller: Controller,
|
||||
await handleFileServiceRequest(controller, "openFile", { value: filePath })
|
||||
|
||||
const message = `Created new ${request.isGlobal ? "global" : "workspace"} ${fileTypeName} file: ${request.filename}`
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message,
|
||||
})
|
||||
}
|
||||
|
||||
return RuleFile.create({
|
||||
|
||||
@@ -3,7 +3,7 @@ import { RuleFile, RuleFileRequest } from "@shared/proto/file"
|
||||
import * as path from "path"
|
||||
import { Controller } from ".."
|
||||
import { FileMethodHandler } from "./index"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
|
||||
/**
|
||||
@@ -46,12 +46,10 @@ export const deleteRuleFile: FileMethodHandler = async (controller: Controller,
|
||||
const fileTypeName = request.type === "workflow" ? "workflow" : "rule"
|
||||
|
||||
const message = `${fileTypeName} file "${fileName}" deleted successfully`
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message,
|
||||
})
|
||||
|
||||
return RuleFile.create({
|
||||
filePath: request.rulePath,
|
||||
|
||||
+73
-342
@@ -1,16 +1,19 @@
|
||||
import { clineEnvConfig } from "@/config"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { AuthService } from "@/services/auth/AuthService"
|
||||
import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
|
||||
import { ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { getCwd, getDesktopDir } from "@/utils/path"
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { buildApiHandler } from "@api/index"
|
||||
import { cleanupLegacyCheckpoints } from "@integrations/checkpoints/CheckpointMigration"
|
||||
import { extractCommitMessage } from "@integrations/git/commit-message-generator"
|
||||
import { downloadTask } from "@integrations/misc/export-markdown"
|
||||
import WorkspaceTracker from "@integrations/workspace/WorkspaceTracker"
|
||||
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, StoredChatSettings } from "@shared/ChatSettings"
|
||||
import { ChatSettings, Mode, StoredChatSettings } from "@shared/ChatSettings"
|
||||
import { ClineRulesToggles } from "@shared/cline-rules"
|
||||
import { ExtensionMessage, ExtensionState, Platform } from "@shared/ExtensionMessage"
|
||||
import { HistoryItem } from "@shared/HistoryItem"
|
||||
@@ -19,7 +22,6 @@ import { TelemetrySetting } from "@shared/TelemetrySetting"
|
||||
import { UserInfo } from "@shared/UserInfo"
|
||||
import { WebviewMessage } from "@shared/WebviewMessage"
|
||||
import { fileExistsAtPath } from "@utils/fs"
|
||||
import { getWorkingState } from "@utils/git"
|
||||
import axios from "axios"
|
||||
import fs from "fs/promises"
|
||||
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
|
||||
@@ -30,13 +32,9 @@ import { ensureMcpServersDirectoryExists, ensureSettingsDirectoryExists, GlobalF
|
||||
import { getAllExtensionState, getGlobalState, getWorkspaceState, storeSecret, updateGlobalState } from "../storage/state"
|
||||
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 { sendMcpMarketplaceCatalogEvent } from "./mcp/subscribeToMcpMarketplaceCatalog"
|
||||
import { AuthService } from "@/services/auth/AuthService"
|
||||
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { clineEnvConfig } from "@/config"
|
||||
|
||||
/*
|
||||
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
||||
@@ -50,6 +48,7 @@ export class Controller {
|
||||
|
||||
private disposables: vscode.Disposable[] = []
|
||||
task?: Task
|
||||
|
||||
workspaceTracker: WorkspaceTracker
|
||||
mcpHub: McpHub
|
||||
accountService: ClineAccountService
|
||||
@@ -83,8 +82,8 @@ export class Controller {
|
||||
})
|
||||
}
|
||||
|
||||
private async getCurrentMode(): Promise<"plan" | "act"> {
|
||||
return ((await getGlobalState(this.context, "mode")) as "plan" | "act" | undefined) || "act"
|
||||
async getCurrentMode(): Promise<Mode> {
|
||||
return ((await getGlobalState(this.context, "mode")) as Mode | undefined) || "act"
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -112,21 +111,20 @@ export class Controller {
|
||||
// TODO: update to clineAccountId and then move clineApiKey to a clear function.
|
||||
await storeSecret(this.context, "clineAccountId", undefined)
|
||||
await updateGlobalState(this.context, "userInfo", undefined)
|
||||
await updateGlobalState(this.context, "apiProvider", "openrouter")
|
||||
await Promise.all([
|
||||
updateGlobalState(this.context, "planModeApiProvider", "openrouter"),
|
||||
updateGlobalState(this.context, "actModeApiProvider", "openrouter"),
|
||||
])
|
||||
await this.postStateToWebview()
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Successfully logged out of Cline",
|
||||
}),
|
||||
)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Successfully logged out of Cline",
|
||||
})
|
||||
} catch (error) {
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Logout failed",
|
||||
}),
|
||||
)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Logout failed",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,153 +255,10 @@ export class Controller {
|
||||
// Capture mode switch telemetry | Capture regardless of if we know the taskId
|
||||
telemetryService.captureModeSwitch(this.task?.taskId ?? "0", chatSettings.mode)
|
||||
|
||||
// Get previous model info that we will revert to after saving current mode api info
|
||||
const {
|
||||
apiConfiguration,
|
||||
previousModeApiProvider: newApiProvider,
|
||||
previousModeModelId: newModelId,
|
||||
previousModeModelInfo: newModelInfo,
|
||||
previousModeVsCodeLmModelSelector: newVsCodeLmModelSelector,
|
||||
previousModeThinkingBudgetTokens: newThinkingBudgetTokens,
|
||||
previousModeReasoningEffort: newReasoningEffort,
|
||||
previousModeAwsBedrockCustomSelected: newAwsBedrockCustomSelected,
|
||||
previousModeAwsBedrockCustomModelBaseId: newAwsBedrockCustomModelBaseId,
|
||||
previousModeSapAiCoreModelId: newSapAiCoreModelId,
|
||||
planActSeparateModelsSetting,
|
||||
} = await getAllExtensionState(this.context)
|
||||
|
||||
const shouldSwitchModel = planActSeparateModelsSetting === true
|
||||
|
||||
if (shouldSwitchModel) {
|
||||
// Save the last model used in this mode
|
||||
await updateGlobalState(this.context, "previousModeApiProvider", apiConfiguration.apiProvider)
|
||||
await updateGlobalState(this.context, "previousModeThinkingBudgetTokens", apiConfiguration.thinkingBudgetTokens)
|
||||
await updateGlobalState(this.context, "previousModeReasoningEffort", apiConfiguration.reasoningEffort)
|
||||
switch (apiConfiguration.apiProvider) {
|
||||
case "anthropic":
|
||||
case "vertex":
|
||||
case "gemini":
|
||||
case "asksage":
|
||||
case "openai-native":
|
||||
case "qwen":
|
||||
case "deepseek":
|
||||
case "xai":
|
||||
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.apiModelId)
|
||||
break
|
||||
case "bedrock":
|
||||
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.apiModelId)
|
||||
await updateGlobalState(
|
||||
this.context,
|
||||
"previousModeAwsBedrockCustomSelected",
|
||||
apiConfiguration.awsBedrockCustomSelected,
|
||||
)
|
||||
await updateGlobalState(
|
||||
this.context,
|
||||
"previousModeAwsBedrockCustomModelBaseId",
|
||||
apiConfiguration.awsBedrockCustomModelBaseId,
|
||||
)
|
||||
break
|
||||
case "openrouter":
|
||||
case "cline":
|
||||
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.openRouterModelId)
|
||||
await updateGlobalState(this.context, "previousModeModelInfo", apiConfiguration.openRouterModelInfo)
|
||||
break
|
||||
case "vscode-lm":
|
||||
// Important we don't set modelId to this, as it's an object not string (webview expects model id to be a string)
|
||||
await updateGlobalState(
|
||||
this.context,
|
||||
"previousModeVsCodeLmModelSelector",
|
||||
apiConfiguration.vsCodeLmModelSelector,
|
||||
)
|
||||
break
|
||||
case "openai":
|
||||
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.openAiModelId)
|
||||
await updateGlobalState(this.context, "previousModeModelInfo", apiConfiguration.openAiModelInfo)
|
||||
break
|
||||
case "ollama":
|
||||
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.ollamaModelId)
|
||||
break
|
||||
case "lmstudio":
|
||||
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.lmStudioModelId)
|
||||
break
|
||||
case "litellm":
|
||||
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.liteLlmModelId)
|
||||
await updateGlobalState(this.context, "previousModeModelInfo", apiConfiguration.liteLlmModelInfo)
|
||||
break
|
||||
case "requesty":
|
||||
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.requestyModelId)
|
||||
await updateGlobalState(this.context, "previousModeModelInfo", apiConfiguration.requestyModelInfo)
|
||||
break
|
||||
case "sapaicore":
|
||||
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.apiModelId)
|
||||
await updateGlobalState(this.context, "previousModeSapAiCoreModelId", apiConfiguration.sapAiCoreModelId)
|
||||
break
|
||||
}
|
||||
|
||||
// Restore the model used in previous mode
|
||||
if (
|
||||
newApiProvider ||
|
||||
newModelId ||
|
||||
newThinkingBudgetTokens !== undefined ||
|
||||
newReasoningEffort ||
|
||||
newVsCodeLmModelSelector
|
||||
) {
|
||||
await updateGlobalState(this.context, "apiProvider", newApiProvider)
|
||||
await updateGlobalState(this.context, "thinkingBudgetTokens", newThinkingBudgetTokens)
|
||||
await updateGlobalState(this.context, "reasoningEffort", newReasoningEffort)
|
||||
switch (newApiProvider) {
|
||||
case "anthropic":
|
||||
case "vertex":
|
||||
case "gemini":
|
||||
case "asksage":
|
||||
case "openai-native":
|
||||
case "qwen":
|
||||
case "deepseek":
|
||||
case "xai":
|
||||
await updateGlobalState(this.context, "apiModelId", newModelId)
|
||||
break
|
||||
case "bedrock":
|
||||
await updateGlobalState(this.context, "apiModelId", newModelId)
|
||||
await updateGlobalState(this.context, "awsBedrockCustomSelected", newAwsBedrockCustomSelected)
|
||||
await updateGlobalState(this.context, "awsBedrockCustomModelBaseId", newAwsBedrockCustomModelBaseId)
|
||||
break
|
||||
case "openrouter":
|
||||
case "cline":
|
||||
await updateGlobalState(this.context, "openRouterModelId", newModelId)
|
||||
await updateGlobalState(this.context, "openRouterModelInfo", newModelInfo)
|
||||
break
|
||||
case "vscode-lm":
|
||||
await updateGlobalState(this.context, "vsCodeLmModelSelector", newVsCodeLmModelSelector)
|
||||
break
|
||||
case "openai":
|
||||
await updateGlobalState(this.context, "openAiModelId", newModelId)
|
||||
await updateGlobalState(this.context, "openAiModelInfo", newModelInfo)
|
||||
break
|
||||
case "ollama":
|
||||
await updateGlobalState(this.context, "ollamaModelId", newModelId)
|
||||
break
|
||||
case "lmstudio":
|
||||
await updateGlobalState(this.context, "lmStudioModelId", newModelId)
|
||||
break
|
||||
case "litellm":
|
||||
await updateGlobalState(this.context, "liteLlmModelId", newModelId)
|
||||
await updateGlobalState(this.context, "liteLlmModelInfo", newModelInfo)
|
||||
break
|
||||
case "requesty":
|
||||
await updateGlobalState(this.context, "requestyModelId", newModelId)
|
||||
await updateGlobalState(this.context, "requestyModelInfo", newModelInfo)
|
||||
break
|
||||
case "sapaicore":
|
||||
await updateGlobalState(this.context, "apiModelId", newModelId)
|
||||
await updateGlobalState(this.context, "sapAiCoreModelId", newSapAiCoreModelId)
|
||||
break
|
||||
}
|
||||
|
||||
if (this.task) {
|
||||
const { apiConfiguration: updatedApiConfiguration } = await getAllExtensionState(this.context)
|
||||
this.task.api = buildApiHandler(updatedApiConfiguration)
|
||||
}
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Save only non-mode properties to global storage
|
||||
@@ -467,30 +322,47 @@ export class Controller {
|
||||
await this.authService.handleAuthCallback(customToken, provider ? provider : "google")
|
||||
|
||||
const clineProvider: ApiProvider = "cline"
|
||||
await updateGlobalState(this.context, "apiProvider", clineProvider)
|
||||
|
||||
// Mark welcome view as completed since user has successfully logged in
|
||||
await updateGlobalState(this.context, "welcomeViewCompleted", true)
|
||||
// Get current settings to determine how to update providers
|
||||
const { planActSeparateModelsSetting } = await getAllExtensionState(this.context)
|
||||
const currentMode = await this.getCurrentMode()
|
||||
|
||||
if (planActSeparateModelsSetting) {
|
||||
// Only update the current mode's provider
|
||||
if (currentMode === "plan") {
|
||||
await updateGlobalState(this.context, "planModeApiProvider", clineProvider)
|
||||
} else {
|
||||
await updateGlobalState(this.context, "actModeApiProvider", clineProvider)
|
||||
}
|
||||
} else {
|
||||
// Update both modes to keep them in sync
|
||||
await Promise.all([
|
||||
updateGlobalState(this.context, "planModeApiProvider", clineProvider),
|
||||
updateGlobalState(this.context, "actModeApiProvider", clineProvider),
|
||||
])
|
||||
}
|
||||
|
||||
// Get the updated API configuration (now includes the updated providers)
|
||||
const { apiConfiguration } = await getAllExtensionState(this.context)
|
||||
const updatedConfig = {
|
||||
...apiConfiguration,
|
||||
apiProvider: clineProvider,
|
||||
}
|
||||
|
||||
// Mark welcome view as completed since user has successfully logged in
|
||||
await updateGlobalState(this.context, "welcomeViewCompleted", true)
|
||||
|
||||
if (this.task) {
|
||||
this.task.api = buildApiHandler(updatedConfig)
|
||||
this.task.api = buildApiHandler({ ...updatedConfig, taskId: this.task.taskId }, currentMode)
|
||||
}
|
||||
|
||||
await this.postStateToWebview()
|
||||
} catch (error) {
|
||||
console.error("Failed to handle auth callback:", error)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Failed to log in to Cline",
|
||||
}),
|
||||
)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Failed to log in to Cline",
|
||||
})
|
||||
// Even on login failure, we preserve any existing tokens
|
||||
// Only clear tokens on explicit logout
|
||||
}
|
||||
@@ -525,12 +397,10 @@ export class Controller {
|
||||
console.error("Failed to fetch MCP marketplace:", error)
|
||||
if (!silent) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Failed to fetch MCP marketplace"
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: errorMessage,
|
||||
}),
|
||||
)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: errorMessage,
|
||||
})
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
@@ -614,12 +484,10 @@ export class Controller {
|
||||
} catch (error) {
|
||||
console.error("Failed to handle cached MCP marketplace:", error)
|
||||
const errorMessage = error instanceof Error ? error.message : "Failed to handle cached MCP marketplace"
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: errorMessage,
|
||||
}),
|
||||
)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: errorMessage,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -640,14 +508,22 @@ export class Controller {
|
||||
}
|
||||
|
||||
const openrouter: ApiProvider = "openrouter"
|
||||
await updateGlobalState(this.context, "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)
|
||||
await this.postStateToWebview()
|
||||
if (this.task) {
|
||||
this.task.api = buildApiHandler({
|
||||
apiProvider: openrouter,
|
||||
// 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)
|
||||
}
|
||||
// await this.postMessageToWebview({ type: "action", action: "settingsButtonClicked" }) // bad ux if user is on welcome
|
||||
}
|
||||
@@ -975,150 +851,5 @@ export class Controller {
|
||||
|
||||
// secrets
|
||||
|
||||
// Git commit message generation
|
||||
|
||||
async generateGitCommitMessage() {
|
||||
try {
|
||||
// Check if there's a workspace folder open
|
||||
const cwd = await getCwd()
|
||||
if (!cwd) {
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "No workspace folder open",
|
||||
}),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the git diff
|
||||
const gitDiff = await getWorkingState(cwd)
|
||||
if (gitDiff === "No changes in working directory") {
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "No changes in workspace for commit message",
|
||||
}),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Show a progress notification
|
||||
await vscode.window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: "Generating commit message...",
|
||||
cancellable: false,
|
||||
},
|
||||
async (progress, token) => {
|
||||
try {
|
||||
// Format the git diff into a prompt
|
||||
const prompt = `Based on the following git diff, generate a concise and descriptive commit message:
|
||||
|
||||
${gitDiff.length > 5000 ? gitDiff.substring(0, 5000) + "\n\n[Diff truncated due to size]" : gitDiff}
|
||||
|
||||
The commit message should:
|
||||
1. Start with a short summary (50-72 characters)
|
||||
2. Use the imperative mood (e.g., "Add feature" not "Added feature")
|
||||
3. Describe what was changed and why
|
||||
4. Be clear and descriptive
|
||||
|
||||
Commit message:`
|
||||
|
||||
// Get the current API configuration
|
||||
const { apiConfiguration } = await getAllExtensionState(this.context)
|
||||
|
||||
// Build the API handler
|
||||
const apiHandler = buildApiHandler(apiConfiguration)
|
||||
|
||||
// Create a system prompt
|
||||
const systemPrompt =
|
||||
"You are a helpful assistant that generates concise and descriptive git commit messages based on git diffs."
|
||||
|
||||
// Create a message for the API
|
||||
const messages = [
|
||||
{
|
||||
role: "user" as const,
|
||||
content: prompt,
|
||||
},
|
||||
]
|
||||
|
||||
// Call the API directly
|
||||
const stream = apiHandler.createMessage(systemPrompt, messages)
|
||||
|
||||
// Collect the response
|
||||
let response = ""
|
||||
for await (const chunk of stream) {
|
||||
if (chunk.type === "text") {
|
||||
response += chunk.text
|
||||
}
|
||||
}
|
||||
|
||||
// Extract the commit message
|
||||
const commitMessage = extractCommitMessage(response)
|
||||
|
||||
// Apply the commit message to the Git input box
|
||||
if (commitMessage) {
|
||||
// Get the Git extension API
|
||||
const gitExtension = vscode.extensions.getExtension("vscode.git")?.exports
|
||||
if (gitExtension) {
|
||||
const api = gitExtension.getAPI(1)
|
||||
if (api && api.repositories.length > 0) {
|
||||
const repo = api.repositories[0]
|
||||
repo.inputBox.value = commitMessage
|
||||
const message = "Commit message generated and applied"
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
const message = "No Git repositories found"
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
const message = "Git extension not found"
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
const message = "Failed to generate commit message"
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
}
|
||||
} catch (innerError) {
|
||||
const innerErrorMessage = innerError instanceof Error ? innerError.message : String(innerError)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Failed to generate commit message: ${innerErrorMessage}`,
|
||||
}),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Failed to generate commit message: ${errorMessage}`,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
// dev
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { Controller } from ".."
|
||||
import { EmptyRequest } from "../../../shared/proto/common"
|
||||
import { OpenRouterCompatibleModelInfo, OpenRouterModelInfo } from "../../../shared/proto/models"
|
||||
import axios from "axios"
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { fileExistsAtPath } from "@utils/fs"
|
||||
import { GlobalFileNames } from "@core/storage/disk"
|
||||
import { huggingFaceModels } from "@shared/api"
|
||||
|
||||
/**
|
||||
* Ensures the cache directory exists and returns its path
|
||||
*/
|
||||
async function ensureCacheDirectoryExists(controller: Controller): Promise<string> {
|
||||
const cacheDir = path.join(controller.context.globalStorageUri.fsPath, "cache")
|
||||
try {
|
||||
await fs.mkdir(cacheDir, { recursive: true })
|
||||
} catch (error) {
|
||||
// Directory might already exist
|
||||
}
|
||||
return cacheDir
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the Hugging Face models and returns the updated model list
|
||||
* @param controller The controller instance
|
||||
* @param request Empty request object
|
||||
* @returns Response containing the Hugging Face models
|
||||
*/
|
||||
export async function refreshHuggingFaceModels(
|
||||
controller: Controller,
|
||||
_request: EmptyRequest,
|
||||
): Promise<OpenRouterCompatibleModelInfo> {
|
||||
const huggingFaceModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), "huggingface_models.json")
|
||||
|
||||
let models: Record<string, OpenRouterModelInfo> = {}
|
||||
|
||||
try {
|
||||
// Fetch models from Hugging Face API
|
||||
const response = await axios.get("https://router.huggingface.co/v1/models", {
|
||||
timeout: 10000,
|
||||
})
|
||||
|
||||
if (response.data?.data) {
|
||||
const rawModels = response.data.data
|
||||
|
||||
// Transform HF models to OpenRouter-compatible format
|
||||
for (const rawModel of rawModels) {
|
||||
const modelInfo = OpenRouterModelInfo.create({
|
||||
maxTokens: 8192, // HF doesn't provide max_tokens, use default
|
||||
contextWindow: 128_000, // FIXME: HF doesn't provide context window, use default
|
||||
supportsImages: false, // Most models don't support images
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0, // Will be set based on providers
|
||||
outputPrice: 0, // Will be set based on providers
|
||||
cacheWritesPrice: 0,
|
||||
cacheReadsPrice: 0,
|
||||
description: `Available on providers: ${rawModel.providers?.join(", ") || "unknown"}`,
|
||||
})
|
||||
|
||||
// Add model-specific configurations if we have them in our static models
|
||||
if (rawModel.id in huggingFaceModels) {
|
||||
const staticModel = huggingFaceModels[rawModel.id as keyof typeof huggingFaceModels]
|
||||
modelInfo.maxTokens = staticModel.maxTokens
|
||||
modelInfo.contextWindow = staticModel.contextWindow
|
||||
modelInfo.supportsImages = staticModel.supportsImages
|
||||
modelInfo.supportsPromptCache = staticModel.supportsPromptCache
|
||||
modelInfo.inputPrice = staticModel.inputPrice
|
||||
modelInfo.outputPrice = staticModel.outputPrice
|
||||
modelInfo.description = staticModel.description || modelInfo.description
|
||||
}
|
||||
|
||||
models[rawModel.id] = modelInfo
|
||||
}
|
||||
|
||||
// Save to cache
|
||||
await fs.writeFile(huggingFaceModelsFilePath, JSON.stringify(models, null, 2))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching Hugging Face models:", error)
|
||||
|
||||
// Try to load from cache
|
||||
try {
|
||||
if (await fileExistsAtPath(huggingFaceModelsFilePath)) {
|
||||
const cachedModels = await fs.readFile(huggingFaceModelsFilePath, "utf-8")
|
||||
const parsedModels = JSON.parse(cachedModels)
|
||||
models = parsedModels
|
||||
}
|
||||
} catch (cacheError) {
|
||||
console.error("Error loading cached Hugging Face models:", cacheError)
|
||||
}
|
||||
|
||||
// If no cache available, use static models as fallback
|
||||
if (Object.keys(models).length === 0) {
|
||||
for (const [modelId, modelInfo] of Object.entries(huggingFaceModels)) {
|
||||
models[modelId] = OpenRouterModelInfo.create({
|
||||
maxTokens: modelInfo.maxTokens,
|
||||
contextWindow: modelInfo.contextWindow,
|
||||
supportsImages: modelInfo.supportsImages,
|
||||
supportsPromptCache: modelInfo.supportsPromptCache,
|
||||
inputPrice: modelInfo.inputPrice,
|
||||
outputPrice: modelInfo.outputPrice,
|
||||
cacheWritesPrice: (modelInfo as any).cacheWritesPrice || 0,
|
||||
cacheReadsPrice: (modelInfo as any).cacheReadsPrice || 0,
|
||||
description: modelInfo.description || "",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return OpenRouterCompatibleModelInfo.create({ models })
|
||||
}
|
||||
@@ -29,7 +29,8 @@ export async function updateApiConfigurationProto(
|
||||
|
||||
// Update the task's API handler if there's an active task
|
||||
if (controller.task) {
|
||||
controller.task.api = buildApiHandler(appApiConfiguration)
|
||||
const currentMode = await controller.getCurrentMode()
|
||||
controller.task.api = buildApiHandler({ ...appApiConfiguration, taskId: controller.task.taskId }, currentMode)
|
||||
}
|
||||
|
||||
// Post updated state to webview
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ResetStateRequest } from "../../../shared/proto/state"
|
||||
import { resetGlobalState, resetWorkspaceState } from "../../../core/storage/state"
|
||||
import { sendChatButtonClickedEvent } from "../ui/subscribeToChatButtonClicked"
|
||||
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
|
||||
/**
|
||||
* Resets the extension state to its defaults
|
||||
@@ -15,20 +15,16 @@ import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
export async function resetState(controller: Controller, request: ResetStateRequest): Promise<Empty> {
|
||||
try {
|
||||
if (request.global) {
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Resetting global state...",
|
||||
}),
|
||||
)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Resetting global state...",
|
||||
})
|
||||
await resetGlobalState(controller.context)
|
||||
} else {
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Resetting workspace state...",
|
||||
}),
|
||||
)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Resetting workspace state...",
|
||||
})
|
||||
await resetWorkspaceState(controller.context)
|
||||
}
|
||||
|
||||
@@ -37,12 +33,10 @@ export async function resetState(controller: Controller, request: ResetStateRequ
|
||||
controller.task = undefined
|
||||
}
|
||||
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "State reset",
|
||||
}),
|
||||
)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "State reset",
|
||||
})
|
||||
await controller.postStateToWebview()
|
||||
|
||||
await sendChatButtonClickedEvent(controller.id)
|
||||
@@ -50,12 +44,10 @@ export async function resetState(controller: Controller, request: ResetStateRequ
|
||||
return Empty.create()
|
||||
} catch (error) {
|
||||
console.error("Error resetting state:", error)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Failed to reset state: ${error instanceof Error ? error.message : String(error)}`,
|
||||
}),
|
||||
)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Failed to reset state: ${error instanceof Error ? error.message : String(error)}`,
|
||||
})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Controller } from "../index"
|
||||
import * as proto from "@/shared/proto"
|
||||
import { updateGlobalState } from "../../storage/state"
|
||||
import { TerminalInfo } from "@/integrations/terminal/TerminalRegistry"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
|
||||
export async function updateDefaultTerminalProfile(
|
||||
@@ -27,12 +27,10 @@ export async function updateDefaultTerminalProfile(
|
||||
// Show information message if terminals were closed
|
||||
if (closedCount > 0) {
|
||||
const message = `Closed ${closedCount} ${closedCount === 1 ? "terminal" : "terminals"} with different profile.`
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message,
|
||||
})
|
||||
}
|
||||
|
||||
// Show warning if there are busy terminals that couldn't be closed
|
||||
@@ -40,12 +38,10 @@ export async function updateDefaultTerminalProfile(
|
||||
const message =
|
||||
`${busyTerminals.length} busy ${busyTerminals.length === 1 ? "terminal has" : "terminals have"} a different profile. ` +
|
||||
`Close ${busyTerminals.length === 1 ? "it" : "them"} to use the new profile for all commands.`
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.WARNING,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.WARNING,
|
||||
message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,8 @@ export async function updateSettings(controller: Controller, request: UpdateSett
|
||||
await updateApiConfiguration(controller.context, apiConfiguration)
|
||||
|
||||
if (controller.task) {
|
||||
controller.task.api = buildApiHandler(apiConfiguration)
|
||||
const currentMode = await controller.getCurrentMode()
|
||||
controller.task.api = buildApiHandler({ ...apiConfiguration, taskId: controller.task.taskId }, currentMode)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { DeleteAllTaskHistoryCount } from "../../../shared/proto/task"
|
||||
import { getGlobalState, updateGlobalState } from "../../storage/state"
|
||||
import { fileExistsAtPath } from "../../../utils/fs"
|
||||
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
|
||||
/**
|
||||
* Deletes all task history, with an option to preserve favorites
|
||||
@@ -23,7 +23,7 @@ export async function deleteAllTaskHistory(controller: Controller): Promise<Dele
|
||||
const totalTasks = taskHistory.length
|
||||
|
||||
const userChoice = (
|
||||
await getHostBridgeProvider().windowClient.showMessage(
|
||||
await HostProvider.window.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.WARNING,
|
||||
message: "What would you like to delete?",
|
||||
@@ -33,7 +33,7 @@ export async function deleteAllTaskHistory(controller: Controller): Promise<Dele
|
||||
},
|
||||
}),
|
||||
)
|
||||
)?.selectedOption
|
||||
).selectedOption
|
||||
|
||||
// Default VS Code Cancel button returns `undefined` - don't delete anything
|
||||
if (userChoice === undefined) {
|
||||
@@ -67,17 +67,15 @@ export async function deleteAllTaskHistory(controller: Controller): Promise<Dele
|
||||
} else {
|
||||
// No favorited tasks found - show warning and ask user what to do
|
||||
const answer = (
|
||||
await getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.WARNING,
|
||||
message: "No favorited tasks found. Would you like to delete all tasks anyway?",
|
||||
options: {
|
||||
modal: true,
|
||||
items: ["Delete All Tasks"],
|
||||
},
|
||||
}),
|
||||
)
|
||||
)?.selectedOption
|
||||
await HostProvider.window.showMessage({
|
||||
type: ShowMessageType.WARNING,
|
||||
message: "No favorited tasks found. Would you like to delete all tasks anyway?",
|
||||
options: {
|
||||
modal: true,
|
||||
items: ["Delete All Tasks"],
|
||||
},
|
||||
})
|
||||
).selectedOption
|
||||
|
||||
// User cancelled - don't delete anything
|
||||
if (answer === undefined) {
|
||||
@@ -105,12 +103,10 @@ export async function deleteAllTaskHistory(controller: Controller): Promise<Dele
|
||||
await fs.rm(checkpointsDirPath, { recursive: true, force: true })
|
||||
}
|
||||
} catch (error) {
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Encountered error while deleting task history, there may be some files left behind. Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
}),
|
||||
)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Encountered error while deleting task history, there may be some files left behind. Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
})
|
||||
}
|
||||
|
||||
// Update webview
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Controller } from ".."
|
||||
import { Empty, StringArrayRequest } from "../../../shared/proto/common"
|
||||
import { TaskMethodHandler } from "./index"
|
||||
import { fileExistsAtPath } from "../../../utils/fs"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
|
||||
/**
|
||||
@@ -28,13 +28,11 @@ export const deleteTasksWithIds: TaskMethodHandler = async (
|
||||
? "Are you sure you want to delete this task? This action cannot be undone."
|
||||
: `Are you sure you want to delete these ${taskCount} tasks? This action cannot be undone.`
|
||||
|
||||
const userChoice = await getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.WARNING,
|
||||
message,
|
||||
options: { modal: true, items: ["Delete"] },
|
||||
}),
|
||||
)
|
||||
const userChoice = await HostProvider.window.showMessage({
|
||||
type: ShowMessageType.WARNING,
|
||||
message,
|
||||
options: { modal: true, items: ["Delete"] },
|
||||
})
|
||||
|
||||
if (userChoice === undefined) {
|
||||
return Empty.create()
|
||||
@@ -76,10 +74,7 @@ async function deleteTaskWithId(controller: Controller, id: string): Promise<voi
|
||||
contextHistoryFilePath,
|
||||
taskMetadataFilePath,
|
||||
]) {
|
||||
const fileExists = await fileExistsAtPath(filePath)
|
||||
if (fileExists) {
|
||||
await fs.unlink(filePath)
|
||||
}
|
||||
await fs.rm(filePath, { force: true })
|
||||
}
|
||||
|
||||
// Remove empty task directory
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Controller } from "../index"
|
||||
import { EmptyRequest, Empty, String } from "@shared/proto/common"
|
||||
import * as hostProviders from "@hosts/host-providers"
|
||||
import { EmptyRequest, String } from "@shared/proto/common"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { WebviewProviderType } from "@/shared/webview/types"
|
||||
|
||||
/**
|
||||
@@ -10,7 +10,7 @@ import { WebviewProviderType } from "@/shared/webview/types"
|
||||
* @returns Empty response
|
||||
*/
|
||||
export async function getWebviewHtml(_controller: Controller, _: EmptyRequest): Promise<String> {
|
||||
const webviewProvider = hostProviders.createWebviewProvider(WebviewProviderType.SIDEBAR)
|
||||
const webviewProvider = HostProvider.get().createWebviewProvider(WebviewProviderType.SIDEBAR)
|
||||
|
||||
return Promise.resolve(String.create({ value: webviewProvider.getHtmlContent() }))
|
||||
}
|
||||
|
||||
@@ -30,25 +30,77 @@ export async function initializeWebview(controller: Controller, request: EmptyRe
|
||||
handleModelsServiceRequest(controller, "refreshOpenRouterModels", 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 } = await getAllExtensionState(controller.context)
|
||||
if (apiConfiguration.openRouterModelId && response.models[apiConfiguration.openRouterModelId]) {
|
||||
await updateGlobalState(
|
||||
controller.context,
|
||||
"openRouterModelInfo",
|
||||
response.models[apiConfiguration.openRouterModelId],
|
||||
)
|
||||
await controller.postStateToWebview()
|
||||
const { apiConfiguration, planActSeparateModelsSetting } = await getAllExtensionState(controller.context)
|
||||
const currentMode = await controller.getCurrentMode()
|
||||
|
||||
if (planActSeparateModelsSetting) {
|
||||
// Separate models: update only current mode
|
||||
const modelIdField = currentMode === "plan" ? "planModeOpenRouterModelId" : "actModeOpenRouterModelId"
|
||||
const modelInfoField = currentMode === "plan" ? "planModeOpenRouterModelInfo" : "actModeOpenRouterModelInfo"
|
||||
const modelId = apiConfiguration[modelIdField]
|
||||
|
||||
if (modelId && response.models[modelId]) {
|
||||
await updateGlobalState(controller.context, modelInfoField, response.models[modelId])
|
||||
await controller.postStateToWebview()
|
||||
}
|
||||
} else {
|
||||
// Shared models: update both plan and act modes
|
||||
const planModelId = apiConfiguration.planModeOpenRouterModelId
|
||||
const actModelId = apiConfiguration.actModeOpenRouterModelId
|
||||
|
||||
// Update plan mode model info if we have a model ID
|
||||
if (planModelId && response.models[planModelId]) {
|
||||
await updateGlobalState(controller.context, "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])
|
||||
}
|
||||
|
||||
// Post state update if we updated any model info
|
||||
if ((planModelId && response.models[planModelId]) || (actModelId && response.models[actModelId])) {
|
||||
await controller.postStateToWebview()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
handleModelsServiceRequest(controller, "refreshGroqModels", EmptyRequest.create()).then(async (response) => {
|
||||
if (response && response.models) {
|
||||
// update model info in state for Groq
|
||||
const { apiConfiguration } = await getAllExtensionState(controller.context)
|
||||
if (apiConfiguration.groqModelId && response.models[apiConfiguration.groqModelId]) {
|
||||
await updateGlobalState(controller.context, "groqModelInfo", response.models[apiConfiguration.groqModelId])
|
||||
await controller.postStateToWebview()
|
||||
// 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 currentMode = await controller.getCurrentMode()
|
||||
|
||||
if (planActSeparateModelsSetting) {
|
||||
// Separate models: update only current mode
|
||||
const modelIdField = currentMode === "plan" ? "planModeGroqModelId" : "actModeGroqModelId"
|
||||
const modelInfoField = currentMode === "plan" ? "planModeGroqModelInfo" : "actModeGroqModelInfo"
|
||||
const modelId = apiConfiguration[modelIdField]
|
||||
|
||||
if (modelId && response.models[modelId]) {
|
||||
await updateGlobalState(controller.context, modelInfoField, response.models[modelId])
|
||||
await controller.postStateToWebview()
|
||||
}
|
||||
} else {
|
||||
// Shared models: update both plan and act modes
|
||||
const planModelId = apiConfiguration.planModeGroqModelId
|
||||
const actModelId = apiConfiguration.actModeGroqModelId
|
||||
|
||||
// Update plan mode model info if we have a model ID
|
||||
if (planModelId && response.models[planModelId]) {
|
||||
await updateGlobalState(controller.context, "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])
|
||||
}
|
||||
|
||||
// Post state update if we updated any model info
|
||||
if ((planModelId && response.models[planModelId]) || (actModelId && response.models[actModelId])) {
|
||||
await controller.postStateToWebview()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import * as vscode from "vscode"
|
||||
import type { Controller } from "../index"
|
||||
import type { EmptyRequest } from "../../../shared/proto/common"
|
||||
import { Empty } from "../../../shared/proto/common"
|
||||
import { telemetryService } from "../../../services/posthog/telemetry/TelemetryService"
|
||||
|
||||
/**
|
||||
* Opens the Cline walkthrough in VSCode
|
||||
* @param controller The controller instance
|
||||
* @param request Empty request
|
||||
* @returns Empty response
|
||||
*/
|
||||
export async function openWalkthrough(controller: Controller, request: EmptyRequest): Promise<Empty> {
|
||||
try {
|
||||
await vscode.commands.executeCommand("workbench.action.openWalkthrough", "saoudrizwan.claude-dev#ClineWalkthrough")
|
||||
telemetryService.captureButtonClick("webview_openWalkthrough")
|
||||
return Empty.create({})
|
||||
} catch (error) {
|
||||
console.error(`Failed to open walkthrough: ${error}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import { getWorkingState } from "@utils/git"
|
||||
import { FileContextTracker } from "../context/context-tracking/FileContextTracker"
|
||||
import { getCwd } from "@/utils/path"
|
||||
import { openExternal } from "@utils/env"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
|
||||
export async function openMention(mention?: string): Promise<void> {
|
||||
@@ -78,12 +78,10 @@ export async function parseMentions(
|
||||
await urlContentFetcher.launchBrowser()
|
||||
} catch (error) {
|
||||
launchBrowserError = error
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Error fetching content for ${urlMention}: ${error.message}`,
|
||||
}),
|
||||
)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Error fetching content for ${urlMention}: ${error.message}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,12 +98,10 @@ export async function parseMentions(
|
||||
const markdown = await urlContentFetcher.urlToMarkdown(mention)
|
||||
result = markdown
|
||||
} catch (error) {
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Error fetching content for ${mention}: ${error.message}`,
|
||||
}),
|
||||
)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: `Error fetching content for ${mention}: ${error.message}`,
|
||||
})
|
||||
result = `Error fetching content: ${error.message}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +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"
|
||||
|
||||
export const formatResponse = {
|
||||
duplicateFileReadNotice: () =>
|
||||
@@ -147,7 +148,7 @@ Otherwise, if you have not completed the task and do not need additional informa
|
||||
},
|
||||
|
||||
taskResumption: (
|
||||
mode: "plan" | "act",
|
||||
mode: Mode,
|
||||
agoText: string,
|
||||
cwd: string,
|
||||
wasRecent: boolean | 0 | undefined,
|
||||
|
||||
@@ -21,6 +21,7 @@ export type SecretKey =
|
||||
| "asksageApiKey"
|
||||
| "xaiApiKey"
|
||||
| "moonshotApiKey"
|
||||
| "huggingFaceApiKey"
|
||||
| "nebiusApiKey"
|
||||
| "sambanovaApiKey"
|
||||
| "cerebrasApiKey"
|
||||
@@ -42,12 +43,9 @@ export type GlobalStateKey =
|
||||
| "lastShownAnnouncementId"
|
||||
| "taskHistory"
|
||||
| "openAiBaseUrl"
|
||||
| "openAiModelId"
|
||||
| "openAiModelInfo"
|
||||
| "openAiHeaders"
|
||||
| "ollamaBaseUrl"
|
||||
| "ollamaApiOptionsCtxNum"
|
||||
| "lmStudioModelId"
|
||||
| "lmStudioBaseUrl"
|
||||
| "anthropicBaseUrl"
|
||||
| "geminiBaseUrl"
|
||||
@@ -87,38 +85,55 @@ export type GlobalStateKey =
|
||||
// Settings around plan/act and ephemeral model configuration
|
||||
| "chatSettings"
|
||||
| "mode"
|
||||
// Current active model configuration (per workspace)
|
||||
| "apiProvider"
|
||||
| "apiModelId"
|
||||
| "thinkingBudgetTokens"
|
||||
| "reasoningEffort"
|
||||
| "vsCodeLmModelSelector"
|
||||
| "awsBedrockCustomSelected"
|
||||
| "awsBedrockCustomModelBaseId"
|
||||
| "openRouterModelId"
|
||||
| "openRouterModelInfo"
|
||||
| "openAiModelId"
|
||||
| "openAiModelInfo"
|
||||
| "ollamaModelId"
|
||||
| "lmStudioModelId"
|
||||
| "liteLlmModelId"
|
||||
| "liteLlmModelInfo"
|
||||
| "requestyModelId"
|
||||
| "requestyModelInfo"
|
||||
| "togetherModelId"
|
||||
| "fireworksModelId"
|
||||
| "sapAiCoreModelId"
|
||||
// Previous mode saved configurations (per workspace)
|
||||
| "previousModeApiProvider"
|
||||
| "previousModeModelId"
|
||||
| "previousModeModelInfo"
|
||||
| "previousModeVsCodeLmModelSelector"
|
||||
| "previousModeThinkingBudgetTokens"
|
||||
| "previousModeReasoningEffort"
|
||||
| "previousModeAwsBedrockCustomSelected"
|
||||
| "previousModeAwsBedrockCustomModelBaseId"
|
||||
| "previousModeSapAiCoreModelId"
|
||||
| "groqModelId"
|
||||
| "groqModelInfo"
|
||||
// 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"
|
||||
|
||||
export type LocalStateKey = "localClineRulesToggles" | "localCursorRulesToggles" | "localWindsurfRulesToggles" | "workflowToggles"
|
||||
|
||||
@@ -32,6 +32,10 @@ export async function migrateWorkspaceToGlobalStorage(context: vscode.ExtensionC
|
||||
"togetherModelId",
|
||||
"fireworksModelId",
|
||||
"sapAiCoreModelId",
|
||||
"groqModelId",
|
||||
"groqModelInfo",
|
||||
"huggingFaceModelId",
|
||||
"huggingFaceModelInfo",
|
||||
|
||||
// Previous mode settings
|
||||
"previousModeApiProvider",
|
||||
@@ -53,8 +57,8 @@ export async function migrateWorkspaceToGlobalStorage(context: vscode.ExtensionC
|
||||
if (workspaceValue !== undefined && globalValue === undefined) {
|
||||
console.log(`[Storage Migration] migrating key: ${key} to global storage. Current value: ${workspaceValue}`)
|
||||
|
||||
// Move to global storage
|
||||
await updateGlobalState(context, key as GlobalStateKey, workspaceValue)
|
||||
// Move to global storage using raw VSCode method to avoid type errors
|
||||
await context.globalState.update(key, workspaceValue)
|
||||
// Remove from workspace storage
|
||||
await context.workspaceState.update(key, undefined)
|
||||
const newWorkspaceValue = await context.workspaceState.get(key)
|
||||
@@ -169,6 +173,375 @@ export async function migrateModeFromWorkspaceStorageToControllerState(context:
|
||||
}
|
||||
}
|
||||
|
||||
export async function migrateLegacyApiConfigurationToModeSpecific(context: vscode.ExtensionContext) {
|
||||
try {
|
||||
// Check if migration is needed - if planModeApiProvider already exists, skip migration
|
||||
const planModeApiProvider = await context.globalState.get("planModeApiProvider")
|
||||
if (planModeApiProvider !== undefined) {
|
||||
console.log("Legacy API configuration migration already completed, skipping...")
|
||||
return
|
||||
}
|
||||
|
||||
console.log("Starting legacy API configuration migration to mode-specific keys...")
|
||||
|
||||
// Get the planActSeparateModelsSetting to determine migration strategy
|
||||
const planActSeparateModelsSetting = (await context.globalState.get("planActSeparateModelsSetting")) as
|
||||
| boolean
|
||||
| undefined
|
||||
|
||||
// Read legacy values directly
|
||||
const apiProvider = await context.globalState.get("apiProvider")
|
||||
const apiModelId = await context.globalState.get("apiModelId")
|
||||
const thinkingBudgetTokens = await context.globalState.get("thinkingBudgetTokens")
|
||||
const reasoningEffort = await context.globalState.get("reasoningEffort")
|
||||
const vsCodeLmModelSelector = await context.globalState.get("vsCodeLmModelSelector")
|
||||
const awsBedrockCustomSelected = await context.globalState.get("awsBedrockCustomSelected")
|
||||
const awsBedrockCustomModelBaseId = await context.globalState.get("awsBedrockCustomModelBaseId")
|
||||
const openRouterModelId = await context.globalState.get("openRouterModelId")
|
||||
const openRouterModelInfo = await context.globalState.get("openRouterModelInfo")
|
||||
const openAiModelId = await context.globalState.get("openAiModelId")
|
||||
const openAiModelInfo = await context.globalState.get("openAiModelInfo")
|
||||
const ollamaModelId = await context.globalState.get("ollamaModelId")
|
||||
const lmStudioModelId = await context.globalState.get("lmStudioModelId")
|
||||
const liteLlmModelId = await context.globalState.get("liteLlmModelId")
|
||||
const liteLlmModelInfo = await context.globalState.get("liteLlmModelInfo")
|
||||
const requestyModelId = await context.globalState.get("requestyModelId")
|
||||
const requestyModelInfo = await context.globalState.get("requestyModelInfo")
|
||||
const togetherModelId = await context.globalState.get("togetherModelId")
|
||||
const fireworksModelId = await context.globalState.get("fireworksModelId")
|
||||
const sapAiCoreModelId = await context.globalState.get("sapAiCoreModelId")
|
||||
const groqModelId = await context.globalState.get("groqModelId")
|
||||
const groqModelInfo = await context.globalState.get("groqModelInfo")
|
||||
const huggingFaceModelId = await context.globalState.get("huggingFaceModelId")
|
||||
const huggingFaceModelInfo = await context.globalState.get("huggingFaceModelInfo")
|
||||
|
||||
// Read previous mode values
|
||||
const previousModeApiProvider = await context.globalState.get("previousModeApiProvider")
|
||||
const previousModeModelId = await context.globalState.get("previousModeModelId")
|
||||
const previousModeModelInfo = await context.globalState.get("previousModeModelInfo")
|
||||
const previousModeVsCodeLmModelSelector = await context.globalState.get("previousModeVsCodeLmModelSelector")
|
||||
const previousModeThinkingBudgetTokens = await context.globalState.get("previousModeThinkingBudgetTokens")
|
||||
const previousModeReasoningEffort = await context.globalState.get("previousModeReasoningEffort")
|
||||
const previousModeAwsBedrockCustomSelected = await context.globalState.get("previousModeAwsBedrockCustomSelected")
|
||||
const previousModeAwsBedrockCustomModelBaseId = await context.globalState.get("previousModeAwsBedrockCustomModelBaseId")
|
||||
const previousModeSapAiCoreModelId = await context.globalState.get("previousModeSapAiCoreModelId")
|
||||
|
||||
// Migrate based on planActSeparateModelsSetting
|
||||
if (planActSeparateModelsSetting === false) {
|
||||
console.log("Migrating with separate models DISABLED - using current values for both modes")
|
||||
|
||||
// Use current values for both plan and act modes
|
||||
if (apiProvider !== undefined) {
|
||||
await context.globalState.update("planModeApiProvider", apiProvider)
|
||||
await context.globalState.update("actModeApiProvider", apiProvider)
|
||||
}
|
||||
if (apiModelId !== undefined) {
|
||||
await context.globalState.update("planModeApiModelId", apiModelId)
|
||||
await context.globalState.update("actModeApiModelId", apiModelId)
|
||||
}
|
||||
if (thinkingBudgetTokens !== undefined) {
|
||||
await context.globalState.update("planModeThinkingBudgetTokens", thinkingBudgetTokens)
|
||||
await context.globalState.update("actModeThinkingBudgetTokens", thinkingBudgetTokens)
|
||||
}
|
||||
if (reasoningEffort !== undefined) {
|
||||
await context.globalState.update("planModeReasoningEffort", reasoningEffort)
|
||||
await context.globalState.update("actModeReasoningEffort", reasoningEffort)
|
||||
}
|
||||
if (vsCodeLmModelSelector !== undefined) {
|
||||
await context.globalState.update("planModeVsCodeLmModelSelector", vsCodeLmModelSelector)
|
||||
await context.globalState.update("actModeVsCodeLmModelSelector", vsCodeLmModelSelector)
|
||||
}
|
||||
if (awsBedrockCustomSelected !== undefined) {
|
||||
await context.globalState.update("planModeAwsBedrockCustomSelected", awsBedrockCustomSelected)
|
||||
await context.globalState.update("actModeAwsBedrockCustomSelected", awsBedrockCustomSelected)
|
||||
}
|
||||
if (awsBedrockCustomModelBaseId !== undefined) {
|
||||
await context.globalState.update("planModeAwsBedrockCustomModelBaseId", awsBedrockCustomModelBaseId)
|
||||
await context.globalState.update("actModeAwsBedrockCustomModelBaseId", awsBedrockCustomModelBaseId)
|
||||
}
|
||||
if (openRouterModelId !== undefined) {
|
||||
await context.globalState.update("planModeOpenRouterModelId", openRouterModelId)
|
||||
await context.globalState.update("actModeOpenRouterModelId", openRouterModelId)
|
||||
}
|
||||
if (openRouterModelInfo !== undefined) {
|
||||
await context.globalState.update("planModeOpenRouterModelInfo", openRouterModelInfo)
|
||||
await context.globalState.update("actModeOpenRouterModelInfo", openRouterModelInfo)
|
||||
}
|
||||
if (openAiModelId !== undefined) {
|
||||
await context.globalState.update("planModeOpenAiModelId", openAiModelId)
|
||||
await context.globalState.update("actModeOpenAiModelId", openAiModelId)
|
||||
}
|
||||
if (openAiModelInfo !== undefined) {
|
||||
await context.globalState.update("planModeOpenAiModelInfo", openAiModelInfo)
|
||||
await context.globalState.update("actModeOpenAiModelInfo", openAiModelInfo)
|
||||
}
|
||||
if (ollamaModelId !== undefined) {
|
||||
await context.globalState.update("planModeOllamaModelId", ollamaModelId)
|
||||
await context.globalState.update("actModeOllamaModelId", ollamaModelId)
|
||||
}
|
||||
if (lmStudioModelId !== undefined) {
|
||||
await context.globalState.update("planModeLmStudioModelId", lmStudioModelId)
|
||||
await context.globalState.update("actModeLmStudioModelId", lmStudioModelId)
|
||||
}
|
||||
if (liteLlmModelId !== undefined) {
|
||||
await context.globalState.update("planModeLiteLlmModelId", liteLlmModelId)
|
||||
await context.globalState.update("actModeLiteLlmModelId", liteLlmModelId)
|
||||
}
|
||||
if (liteLlmModelInfo !== undefined) {
|
||||
await context.globalState.update("planModeLiteLlmModelInfo", liteLlmModelInfo)
|
||||
await context.globalState.update("actModeLiteLlmModelInfo", liteLlmModelInfo)
|
||||
}
|
||||
if (requestyModelId !== undefined) {
|
||||
await context.globalState.update("planModeRequestyModelId", requestyModelId)
|
||||
await context.globalState.update("actModeRequestyModelId", requestyModelId)
|
||||
}
|
||||
if (requestyModelInfo !== undefined) {
|
||||
await context.globalState.update("planModeRequestyModelInfo", requestyModelInfo)
|
||||
await context.globalState.update("actModeRequestyModelInfo", requestyModelInfo)
|
||||
}
|
||||
if (togetherModelId !== undefined) {
|
||||
await context.globalState.update("planModeTogetherModelId", togetherModelId)
|
||||
await context.globalState.update("actModeTogetherModelId", togetherModelId)
|
||||
}
|
||||
if (fireworksModelId !== undefined) {
|
||||
await context.globalState.update("planModeFireworksModelId", fireworksModelId)
|
||||
await context.globalState.update("actModeFireworksModelId", fireworksModelId)
|
||||
}
|
||||
if (sapAiCoreModelId !== undefined) {
|
||||
await context.globalState.update("planModeSapAiCoreModelId", sapAiCoreModelId)
|
||||
await context.globalState.update("actModeSapAiCoreModelId", sapAiCoreModelId)
|
||||
}
|
||||
if (groqModelId !== undefined) {
|
||||
await context.globalState.update("planModeGroqModelId", groqModelId)
|
||||
await context.globalState.update("actModeGroqModelId", groqModelId)
|
||||
}
|
||||
if (groqModelInfo !== undefined) {
|
||||
await context.globalState.update("planModeGroqModelInfo", groqModelInfo)
|
||||
await context.globalState.update("actModeGroqModelInfo", groqModelInfo)
|
||||
}
|
||||
if (huggingFaceModelId !== undefined) {
|
||||
await context.globalState.update("planModeHuggingFaceModelId", huggingFaceModelId)
|
||||
await context.globalState.update("actModeHuggingFaceModelId", huggingFaceModelId)
|
||||
}
|
||||
if (huggingFaceModelInfo !== undefined) {
|
||||
await context.globalState.update("planModeHuggingFaceModelInfo", huggingFaceModelInfo)
|
||||
await context.globalState.update("actModeHuggingFaceModelInfo", huggingFaceModelInfo)
|
||||
}
|
||||
} else {
|
||||
console.log("Migrating with separate models ENABLED - using current->plan, previous->act")
|
||||
|
||||
// Use current values for plan mode
|
||||
if (apiProvider !== undefined) {
|
||||
await context.globalState.update("planModeApiProvider", apiProvider)
|
||||
}
|
||||
if (apiModelId !== undefined) {
|
||||
await context.globalState.update("planModeApiModelId", apiModelId)
|
||||
}
|
||||
if (thinkingBudgetTokens !== undefined) {
|
||||
await context.globalState.update("planModeThinkingBudgetTokens", thinkingBudgetTokens)
|
||||
}
|
||||
if (reasoningEffort !== undefined) {
|
||||
await context.globalState.update("planModeReasoningEffort", reasoningEffort)
|
||||
}
|
||||
if (vsCodeLmModelSelector !== undefined) {
|
||||
await context.globalState.update("planModeVsCodeLmModelSelector", vsCodeLmModelSelector)
|
||||
}
|
||||
if (awsBedrockCustomSelected !== undefined) {
|
||||
await context.globalState.update("planModeAwsBedrockCustomSelected", awsBedrockCustomSelected)
|
||||
}
|
||||
if (awsBedrockCustomModelBaseId !== undefined) {
|
||||
await context.globalState.update("planModeAwsBedrockCustomModelBaseId", awsBedrockCustomModelBaseId)
|
||||
}
|
||||
if (openRouterModelId !== undefined) {
|
||||
await context.globalState.update("planModeOpenRouterModelId", openRouterModelId)
|
||||
}
|
||||
if (openRouterModelInfo !== undefined) {
|
||||
await context.globalState.update("planModeOpenRouterModelInfo", openRouterModelInfo)
|
||||
}
|
||||
if (openAiModelId !== undefined) {
|
||||
await context.globalState.update("planModeOpenAiModelId", openAiModelId)
|
||||
}
|
||||
if (openAiModelInfo !== undefined) {
|
||||
await context.globalState.update("planModeOpenAiModelInfo", openAiModelInfo)
|
||||
}
|
||||
if (ollamaModelId !== undefined) {
|
||||
await context.globalState.update("planModeOllamaModelId", ollamaModelId)
|
||||
}
|
||||
if (lmStudioModelId !== undefined) {
|
||||
await context.globalState.update("planModeLmStudioModelId", lmStudioModelId)
|
||||
}
|
||||
if (liteLlmModelId !== undefined) {
|
||||
await context.globalState.update("planModeLiteLlmModelId", liteLlmModelId)
|
||||
}
|
||||
if (liteLlmModelInfo !== undefined) {
|
||||
await context.globalState.update("planModeLiteLlmModelInfo", liteLlmModelInfo)
|
||||
}
|
||||
if (requestyModelId !== undefined) {
|
||||
await context.globalState.update("planModeRequestyModelId", requestyModelId)
|
||||
}
|
||||
if (requestyModelInfo !== undefined) {
|
||||
await context.globalState.update("planModeRequestyModelInfo", requestyModelInfo)
|
||||
}
|
||||
if (togetherModelId !== undefined) {
|
||||
await context.globalState.update("planModeTogetherModelId", togetherModelId)
|
||||
}
|
||||
if (fireworksModelId !== undefined) {
|
||||
await context.globalState.update("planModeFireworksModelId", fireworksModelId)
|
||||
}
|
||||
if (sapAiCoreModelId !== undefined) {
|
||||
await context.globalState.update("planModeSapAiCoreModelId", sapAiCoreModelId)
|
||||
}
|
||||
if (groqModelId !== undefined) {
|
||||
await context.globalState.update("planModeGroqModelId", groqModelId)
|
||||
}
|
||||
if (groqModelInfo !== undefined) {
|
||||
await context.globalState.update("planModeGroqModelInfo", groqModelInfo)
|
||||
}
|
||||
if (huggingFaceModelId !== undefined) {
|
||||
await context.globalState.update("planModeHuggingFaceModelId", huggingFaceModelId)
|
||||
}
|
||||
if (huggingFaceModelInfo !== undefined) {
|
||||
await context.globalState.update("planModeHuggingFaceModelInfo", huggingFaceModelInfo)
|
||||
}
|
||||
|
||||
// Use previous values for act mode (with fallback to current values)
|
||||
if (previousModeApiProvider !== undefined) {
|
||||
await context.globalState.update("actModeApiProvider", previousModeApiProvider)
|
||||
} else if (apiProvider !== undefined) {
|
||||
await context.globalState.update("actModeApiProvider", apiProvider)
|
||||
}
|
||||
if (previousModeModelId !== undefined) {
|
||||
await context.globalState.update("actModeApiModelId", previousModeModelId)
|
||||
} else if (apiModelId !== undefined) {
|
||||
await context.globalState.update("actModeApiModelId", apiModelId)
|
||||
}
|
||||
if (previousModeThinkingBudgetTokens !== undefined) {
|
||||
await context.globalState.update("actModeThinkingBudgetTokens", previousModeThinkingBudgetTokens)
|
||||
} else if (thinkingBudgetTokens !== undefined) {
|
||||
await context.globalState.update("actModeThinkingBudgetTokens", thinkingBudgetTokens)
|
||||
}
|
||||
if (previousModeReasoningEffort !== undefined) {
|
||||
await context.globalState.update("actModeReasoningEffort", previousModeReasoningEffort)
|
||||
} else if (reasoningEffort !== undefined) {
|
||||
await context.globalState.update("actModeReasoningEffort", reasoningEffort)
|
||||
}
|
||||
if (previousModeVsCodeLmModelSelector !== undefined) {
|
||||
await context.globalState.update("actModeVsCodeLmModelSelector", previousModeVsCodeLmModelSelector)
|
||||
} else if (vsCodeLmModelSelector !== undefined) {
|
||||
await context.globalState.update("actModeVsCodeLmModelSelector", vsCodeLmModelSelector)
|
||||
}
|
||||
if (previousModeAwsBedrockCustomSelected !== undefined) {
|
||||
await context.globalState.update("actModeAwsBedrockCustomSelected", previousModeAwsBedrockCustomSelected)
|
||||
} else if (awsBedrockCustomSelected !== undefined) {
|
||||
await context.globalState.update("actModeAwsBedrockCustomSelected", awsBedrockCustomSelected)
|
||||
}
|
||||
if (previousModeAwsBedrockCustomModelBaseId !== undefined) {
|
||||
await context.globalState.update("actModeAwsBedrockCustomModelBaseId", previousModeAwsBedrockCustomModelBaseId)
|
||||
} else if (awsBedrockCustomModelBaseId !== undefined) {
|
||||
await context.globalState.update("actModeAwsBedrockCustomModelBaseId", awsBedrockCustomModelBaseId)
|
||||
}
|
||||
if (previousModeSapAiCoreModelId !== undefined) {
|
||||
await context.globalState.update("actModeSapAiCoreModelId", previousModeSapAiCoreModelId)
|
||||
} else if (sapAiCoreModelId !== undefined) {
|
||||
await context.globalState.update("actModeSapAiCoreModelId", sapAiCoreModelId)
|
||||
}
|
||||
|
||||
// For fields without previous variants, use current values for act mode
|
||||
if (previousModeModelInfo !== undefined) {
|
||||
await context.globalState.update("actModeOpenRouterModelInfo", previousModeModelInfo)
|
||||
} else if (openRouterModelInfo !== undefined) {
|
||||
await context.globalState.update("actModeOpenRouterModelInfo", openRouterModelInfo)
|
||||
}
|
||||
if (openRouterModelId !== undefined) {
|
||||
await context.globalState.update("actModeOpenRouterModelId", openRouterModelId)
|
||||
}
|
||||
if (openAiModelId !== undefined) {
|
||||
await context.globalState.update("actModeOpenAiModelId", openAiModelId)
|
||||
}
|
||||
if (openAiModelInfo !== undefined) {
|
||||
await context.globalState.update("actModeOpenAiModelInfo", openAiModelInfo)
|
||||
}
|
||||
if (ollamaModelId !== undefined) {
|
||||
await context.globalState.update("actModeOllamaModelId", ollamaModelId)
|
||||
}
|
||||
if (lmStudioModelId !== undefined) {
|
||||
await context.globalState.update("actModeLmStudioModelId", lmStudioModelId)
|
||||
}
|
||||
if (liteLlmModelId !== undefined) {
|
||||
await context.globalState.update("actModeLiteLlmModelId", liteLlmModelId)
|
||||
}
|
||||
if (liteLlmModelInfo !== undefined) {
|
||||
await context.globalState.update("actModeLiteLlmModelInfo", liteLlmModelInfo)
|
||||
}
|
||||
if (requestyModelId !== undefined) {
|
||||
await context.globalState.update("actModeRequestyModelId", requestyModelId)
|
||||
}
|
||||
if (requestyModelInfo !== undefined) {
|
||||
await context.globalState.update("actModeRequestyModelInfo", requestyModelInfo)
|
||||
}
|
||||
if (togetherModelId !== undefined) {
|
||||
await context.globalState.update("actModeTogetherModelId", togetherModelId)
|
||||
}
|
||||
if (fireworksModelId !== undefined) {
|
||||
await context.globalState.update("actModeFireworksModelId", fireworksModelId)
|
||||
}
|
||||
if (groqModelId !== undefined) {
|
||||
await context.globalState.update("actModeGroqModelId", groqModelId)
|
||||
}
|
||||
if (groqModelInfo !== undefined) {
|
||||
await context.globalState.update("actModeGroqModelInfo", groqModelInfo)
|
||||
}
|
||||
if (huggingFaceModelId !== undefined) {
|
||||
await context.globalState.update("actModeHuggingFaceModelId", huggingFaceModelId)
|
||||
}
|
||||
if (huggingFaceModelInfo !== undefined) {
|
||||
await context.globalState.update("actModeHuggingFaceModelInfo", huggingFaceModelInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up legacy keys after successful migration
|
||||
console.log("Cleaning up legacy keys...")
|
||||
await context.globalState.update("apiProvider", undefined)
|
||||
await context.globalState.update("apiModelId", undefined)
|
||||
await context.globalState.update("thinkingBudgetTokens", undefined)
|
||||
await context.globalState.update("reasoningEffort", undefined)
|
||||
await context.globalState.update("vsCodeLmModelSelector", undefined)
|
||||
await context.globalState.update("awsBedrockCustomSelected", undefined)
|
||||
await context.globalState.update("awsBedrockCustomModelBaseId", undefined)
|
||||
await context.globalState.update("openRouterModelId", undefined)
|
||||
await context.globalState.update("openRouterModelInfo", undefined)
|
||||
await context.globalState.update("openAiModelId", undefined)
|
||||
await context.globalState.update("openAiModelInfo", undefined)
|
||||
await context.globalState.update("ollamaModelId", undefined)
|
||||
await context.globalState.update("lmStudioModelId", undefined)
|
||||
await context.globalState.update("liteLlmModelId", undefined)
|
||||
await context.globalState.update("liteLlmModelInfo", undefined)
|
||||
await context.globalState.update("requestyModelId", undefined)
|
||||
await context.globalState.update("requestyModelInfo", undefined)
|
||||
await context.globalState.update("togetherModelId", undefined)
|
||||
await context.globalState.update("fireworksModelId", undefined)
|
||||
await context.globalState.update("sapAiCoreModelId", undefined)
|
||||
await context.globalState.update("groqModelId", undefined)
|
||||
await context.globalState.update("groqModelInfo", undefined)
|
||||
await context.globalState.update("huggingFaceModelId", undefined)
|
||||
await context.globalState.update("huggingFaceModelInfo", undefined)
|
||||
await context.globalState.update("previousModeApiProvider", undefined)
|
||||
await context.globalState.update("previousModeModelId", undefined)
|
||||
await context.globalState.update("previousModeModelInfo", undefined)
|
||||
await context.globalState.update("previousModeVsCodeLmModelSelector", undefined)
|
||||
await context.globalState.update("previousModeThinkingBudgetTokens", undefined)
|
||||
await context.globalState.update("previousModeReasoningEffort", undefined)
|
||||
await context.globalState.update("previousModeAwsBedrockCustomSelected", undefined)
|
||||
await context.globalState.update("previousModeAwsBedrockCustomModelBaseId", undefined)
|
||||
await context.globalState.update("previousModeSapAiCoreModelId", undefined)
|
||||
|
||||
console.log("Successfully migrated legacy API configuration to mode-specific keys")
|
||||
} catch (error) {
|
||||
console.error("Failed to migrate legacy API configuration to mode-specific keys:", error)
|
||||
// Continue execution - migration failure shouldn't break extension startup
|
||||
}
|
||||
}
|
||||
|
||||
export async function migrateWelcomeViewCompleted(context: vscode.ExtensionContext) {
|
||||
try {
|
||||
// Check if welcomeViewCompleted is already set
|
||||
@@ -190,8 +563,10 @@ export async function migrateWelcomeViewCompleted(context: vscode.ExtensionConte
|
||||
config.awsRegion,
|
||||
config.vertexProjectId,
|
||||
config.openAiApiKey,
|
||||
config.ollamaModelId,
|
||||
config.lmStudioModelId,
|
||||
config.planModeOllamaModelId,
|
||||
config.planModeLmStudioModelId,
|
||||
config.actModeOllamaModelId,
|
||||
config.actModeLmStudioModelId,
|
||||
config.liteLlmApiKey,
|
||||
config.geminiApiKey,
|
||||
config.openAiNativeApiKey,
|
||||
@@ -201,7 +576,8 @@ export async function migrateWelcomeViewCompleted(context: vscode.ExtensionConte
|
||||
config.qwenApiKey,
|
||||
config.doubaoApiKey,
|
||||
config.mistralApiKey,
|
||||
config.vsCodeLmModelSelector,
|
||||
config.planModeVsCodeLmModelSelector,
|
||||
config.actModeVsCodeLmModelSelector,
|
||||
config.clineAccountId,
|
||||
config.asksageApiKey,
|
||||
config.xaiApiKey,
|
||||
|
||||
+262
-144
@@ -1,5 +1,5 @@
|
||||
import * as vscode from "vscode"
|
||||
import { DEFAULT_CHAT_SETTINGS } from "@shared/ChatSettings"
|
||||
import { DEFAULT_CHAT_SETTINGS, Mode } from "@shared/ChatSettings"
|
||||
import { DEFAULT_BROWSER_SETTINGS } from "@shared/BrowserSettings"
|
||||
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "@shared/AutoApprovalSettings"
|
||||
import { GlobalStateKey, LocalStateKey, SecretKey } from "./state-keys"
|
||||
@@ -170,6 +170,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
groqApiKey,
|
||||
moonshotApiKey,
|
||||
nebiusApiKey,
|
||||
huggingFaceApiKey,
|
||||
planActSeparateModelsSettingRaw,
|
||||
favoritedModelIds,
|
||||
globalClineRulesToggles,
|
||||
@@ -189,8 +190,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup,
|
||||
claudeCodePath,
|
||||
groqModelId,
|
||||
groqModelInfo,
|
||||
] = await Promise.all([
|
||||
getGlobalState(context, "isNewUser") as Promise<boolean | undefined>,
|
||||
getGlobalState(context, "welcomeViewCompleted") as Promise<boolean | undefined>,
|
||||
@@ -250,6 +249,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
getSecret(context, "groqApiKey") as Promise<string | undefined>,
|
||||
getSecret(context, "moonshotApiKey") as Promise<string | undefined>,
|
||||
getSecret(context, "nebiusApiKey") as Promise<string | undefined>,
|
||||
getSecret(context, "huggingFaceApiKey") as Promise<string | undefined>,
|
||||
getGlobalState(context, "planActSeparateModelsSetting") as Promise<boolean | undefined>,
|
||||
getGlobalState(context, "favoritedModelIds") as Promise<string[] | undefined>,
|
||||
getGlobalState(context, "globalClineRulesToggles") as Promise<ClineRulesToggles | undefined>,
|
||||
@@ -269,8 +269,6 @@ 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>,
|
||||
getGlobalState(context, "groqModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "groqModelInfo") as Promise<ModelInfo | undefined>,
|
||||
])
|
||||
|
||||
const localClineRulesToggles = (await getWorkspaceState(context, "localClineRulesToggles")) as ClineRulesToggles
|
||||
@@ -279,74 +277,115 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
const [
|
||||
chatSettings,
|
||||
currentMode,
|
||||
storedApiProvider,
|
||||
apiModelId,
|
||||
thinkingBudgetTokens,
|
||||
reasoningEffort,
|
||||
vsCodeLmModelSelector,
|
||||
awsBedrockCustomSelected,
|
||||
awsBedrockCustomModelBaseId,
|
||||
openRouterModelId,
|
||||
openRouterModelInfo,
|
||||
openAiModelId,
|
||||
openAiModelInfo,
|
||||
ollamaModelId,
|
||||
lmStudioModelId,
|
||||
liteLlmModelId,
|
||||
liteLlmModelInfo,
|
||||
requestyModelId,
|
||||
requestyModelInfo,
|
||||
togetherModelId,
|
||||
fireworksModelId,
|
||||
previousModeApiProvider,
|
||||
previousModeModelId,
|
||||
previousModeModelInfo,
|
||||
previousModeVsCodeLmModelSelector,
|
||||
previousModeThinkingBudgetTokens,
|
||||
previousModeReasoningEffort,
|
||||
previousModeAwsBedrockCustomSelected,
|
||||
previousModeAwsBedrockCustomModelBaseId,
|
||||
previousModeSapAiCoreModelId,
|
||||
sapAiCoreModelId,
|
||||
// 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,
|
||||
] = await Promise.all([
|
||||
getGlobalState(context, "chatSettings") as Promise<StoredChatSettings | undefined>,
|
||||
getGlobalState(context, "mode") as Promise<"plan" | "act" | undefined>,
|
||||
getGlobalState(context, "apiProvider") as Promise<ApiProvider | undefined>,
|
||||
getGlobalState(context, "apiModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "thinkingBudgetTokens") as Promise<number | undefined>,
|
||||
getGlobalState(context, "reasoningEffort") as Promise<string | undefined>,
|
||||
getGlobalState(context, "vsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
|
||||
getGlobalState(context, "awsBedrockCustomSelected") as Promise<boolean | undefined>,
|
||||
getGlobalState(context, "awsBedrockCustomModelBaseId") as Promise<BedrockModelId | undefined>,
|
||||
getGlobalState(context, "openRouterModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "openRouterModelInfo") as Promise<ModelInfo | undefined>,
|
||||
getGlobalState(context, "openAiModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "openAiModelInfo") as Promise<ModelInfo | undefined>,
|
||||
getGlobalState(context, "ollamaModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "lmStudioModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "liteLlmModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "liteLlmModelInfo") as Promise<ModelInfo | undefined>,
|
||||
getGlobalState(context, "requestyModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "requestyModelInfo") as Promise<ModelInfo | undefined>,
|
||||
getGlobalState(context, "togetherModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "fireworksModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "previousModeApiProvider") as Promise<ApiProvider | undefined>,
|
||||
getGlobalState(context, "previousModeModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "previousModeModelInfo") as Promise<ModelInfo | undefined>,
|
||||
getGlobalState(context, "previousModeVsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
|
||||
getGlobalState(context, "previousModeThinkingBudgetTokens") as Promise<number | undefined>,
|
||||
getGlobalState(context, "previousModeReasoningEffort") as Promise<string | undefined>,
|
||||
getGlobalState(context, "previousModeAwsBedrockCustomSelected") as Promise<boolean | undefined>,
|
||||
getGlobalState(context, "previousModeAwsBedrockCustomModelBaseId") as Promise<BedrockModelId | undefined>,
|
||||
getGlobalState(context, "previousModeSapAiCoreModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "sapAiCoreModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "mode") as Promise<Mode | undefined>,
|
||||
// Plan mode configurations
|
||||
getGlobalState(context, "planModeApiProvider") as Promise<ApiProvider | undefined>,
|
||||
getGlobalState(context, "planModeApiModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "planModeThinkingBudgetTokens") as Promise<number | undefined>,
|
||||
getGlobalState(context, "planModeReasoningEffort") as Promise<string | undefined>,
|
||||
getGlobalState(context, "planModeVsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
|
||||
getGlobalState(context, "planModeAwsBedrockCustomSelected") as Promise<boolean | undefined>,
|
||||
getGlobalState(context, "planModeAwsBedrockCustomModelBaseId") as Promise<BedrockModelId | undefined>,
|
||||
getGlobalState(context, "planModeOpenRouterModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "planModeOpenRouterModelInfo") as Promise<ModelInfo | undefined>,
|
||||
getGlobalState(context, "planModeOpenAiModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "planModeOpenAiModelInfo") as Promise<ModelInfo | undefined>,
|
||||
getGlobalState(context, "planModeOllamaModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "planModeLmStudioModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "planModeLiteLlmModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "planModeLiteLlmModelInfo") as Promise<ModelInfo | undefined>,
|
||||
getGlobalState(context, "planModeRequestyModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "planModeRequestyModelInfo") as Promise<ModelInfo | undefined>,
|
||||
getGlobalState(context, "planModeTogetherModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "planModeFireworksModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "planModeSapAiCoreModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "planModeGroqModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "planModeGroqModelInfo") as Promise<ModelInfo | undefined>,
|
||||
getGlobalState(context, "planModeHuggingFaceModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "planModeHuggingFaceModelInfo") as Promise<ModelInfo | undefined>,
|
||||
// Act mode configurations
|
||||
getGlobalState(context, "actModeApiProvider") as Promise<ApiProvider | undefined>,
|
||||
getGlobalState(context, "actModeApiModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "actModeThinkingBudgetTokens") as Promise<number | undefined>,
|
||||
getGlobalState(context, "actModeReasoningEffort") as Promise<string | undefined>,
|
||||
getGlobalState(context, "actModeVsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
|
||||
getGlobalState(context, "actModeAwsBedrockCustomSelected") as Promise<boolean | undefined>,
|
||||
getGlobalState(context, "actModeAwsBedrockCustomModelBaseId") as Promise<BedrockModelId | undefined>,
|
||||
getGlobalState(context, "actModeOpenRouterModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "actModeOpenRouterModelInfo") as Promise<ModelInfo | undefined>,
|
||||
getGlobalState(context, "actModeOpenAiModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "actModeOpenAiModelInfo") as Promise<ModelInfo | undefined>,
|
||||
getGlobalState(context, "actModeOllamaModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "actModeLmStudioModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "actModeLiteLlmModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "actModeLiteLlmModelInfo") as Promise<ModelInfo | undefined>,
|
||||
getGlobalState(context, "actModeRequestyModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "actModeRequestyModelInfo") as Promise<ModelInfo | undefined>,
|
||||
getGlobalState(context, "actModeTogetherModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "actModeFireworksModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "actModeSapAiCoreModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "actModeGroqModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "actModeGroqModelInfo") as Promise<ModelInfo | undefined>,
|
||||
getGlobalState(context, "actModeHuggingFaceModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "actModeHuggingFaceModelInfo") as Promise<ModelInfo | undefined>,
|
||||
])
|
||||
|
||||
const processingStart = performance.now()
|
||||
let apiProvider: ApiProvider
|
||||
if (storedApiProvider) {
|
||||
// Use the explicitly stored provider - this respects user's selection
|
||||
apiProvider = storedApiProvider
|
||||
if (planModeApiProvider) {
|
||||
apiProvider = planModeApiProvider
|
||||
} else {
|
||||
// Either new user or legacy user that doesn't have the apiProvider stored in state
|
||||
// (If they're using OpenRouter or Bedrock, then apiProvider state will exist)
|
||||
@@ -369,7 +408,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
planActSeparateModelsSetting = planActSeparateModelsSettingRaw
|
||||
} else {
|
||||
// default to true for existing users
|
||||
if (storedApiProvider) {
|
||||
if (planModeApiProvider) {
|
||||
planActSeparateModelsSetting = true
|
||||
} else {
|
||||
// default to false for new users
|
||||
@@ -382,8 +421,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
|
||||
return {
|
||||
apiConfiguration: {
|
||||
apiProvider,
|
||||
apiModelId,
|
||||
apiKey,
|
||||
openRouterApiKey,
|
||||
clineAccountId,
|
||||
@@ -399,19 +436,13 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
awsBedrockApiKey,
|
||||
awsUseProfile,
|
||||
awsAuthentication,
|
||||
awsBedrockCustomSelected,
|
||||
awsBedrockCustomModelBaseId,
|
||||
vertexProjectId,
|
||||
vertexRegion,
|
||||
openAiBaseUrl,
|
||||
openAiApiKey,
|
||||
openAiModelId,
|
||||
openAiModelInfo,
|
||||
openAiHeaders: openAiHeaders || {},
|
||||
ollamaModelId,
|
||||
ollamaBaseUrl,
|
||||
ollamaApiOptionsCtxNum,
|
||||
lmStudioModelId,
|
||||
lmStudioBaseUrl,
|
||||
anthropicBaseUrl,
|
||||
geminiApiKey,
|
||||
@@ -419,29 +450,18 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
openAiNativeApiKey,
|
||||
deepSeekApiKey,
|
||||
requestyApiKey,
|
||||
requestyModelId,
|
||||
requestyModelInfo,
|
||||
togetherApiKey,
|
||||
togetherModelId,
|
||||
qwenApiKey,
|
||||
qwenApiLine,
|
||||
moonshotApiLine,
|
||||
doubaoApiKey,
|
||||
mistralApiKey,
|
||||
azureApiVersion,
|
||||
openRouterModelId,
|
||||
openRouterModelInfo,
|
||||
openRouterProviderSorting,
|
||||
vsCodeLmModelSelector,
|
||||
thinkingBudgetTokens,
|
||||
reasoningEffort,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmModelId,
|
||||
liteLlmModelInfo,
|
||||
liteLlmApiKey,
|
||||
liteLlmUsePromptCache,
|
||||
fireworksApiKey,
|
||||
fireworksModelId,
|
||||
fireworksModelMaxCompletionTokens,
|
||||
fireworksModelMaxTokens,
|
||||
asksageApiKey,
|
||||
@@ -450,8 +470,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
sambanovaApiKey,
|
||||
cerebrasApiKey,
|
||||
groqApiKey,
|
||||
groqModelId,
|
||||
groqModelInfo,
|
||||
moonshotApiKey,
|
||||
nebiusApiKey,
|
||||
favoritedModelIds,
|
||||
@@ -461,7 +479,57 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
sapAiCoreBaseUrl,
|
||||
sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup,
|
||||
sapAiCoreModelId,
|
||||
huggingFaceApiKey,
|
||||
// Plan mode configurations
|
||||
planModeApiProvider: planModeApiProvider || apiProvider,
|
||||
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: actModeApiProvider || apiProvider,
|
||||
actModeApiModelId,
|
||||
actModeThinkingBudgetTokens,
|
||||
actModeReasoningEffort,
|
||||
actModeVsCodeLmModelSelector,
|
||||
actModeAwsBedrockCustomSelected,
|
||||
actModeAwsBedrockCustomModelBaseId,
|
||||
actModeOpenRouterModelId,
|
||||
actModeOpenRouterModelInfo,
|
||||
actModeOpenAiModelId,
|
||||
actModeOpenAiModelInfo,
|
||||
actModeOllamaModelId,
|
||||
actModeLmStudioModelId,
|
||||
actModeLiteLlmModelId,
|
||||
actModeLiteLlmModelInfo,
|
||||
actModeRequestyModelId,
|
||||
actModeRequestyModelInfo,
|
||||
actModeTogetherModelId,
|
||||
actModeFireworksModelId,
|
||||
actModeSapAiCoreModelId,
|
||||
actModeGroqModelId,
|
||||
actModeGroqModelInfo,
|
||||
actModeHuggingFaceModelId,
|
||||
actModeHuggingFaceModelInfo,
|
||||
},
|
||||
isNewUser: isNewUser ?? true,
|
||||
welcomeViewCompleted,
|
||||
@@ -477,15 +545,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
mode: currentMode || "act", // Merge mode from global state
|
||||
},
|
||||
userInfo,
|
||||
previousModeApiProvider,
|
||||
previousModeModelId,
|
||||
previousModeModelInfo,
|
||||
previousModeVsCodeLmModelSelector,
|
||||
previousModeThinkingBudgetTokens,
|
||||
previousModeReasoningEffort,
|
||||
previousModeAwsBedrockCustomSelected,
|
||||
previousModeAwsBedrockCustomModelBaseId,
|
||||
previousModeSapAiCoreModelId,
|
||||
mcpMarketplaceEnabled: mcpMarketplaceEnabled,
|
||||
mcpDisplayMode: mcpDisplayMode ?? DEFAULT_MCP_DISPLAY_MODE,
|
||||
mcpResponsesCollapsed: mcpResponsesCollapsed,
|
||||
@@ -502,8 +561,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
|
||||
export async function updateApiConfiguration(context: vscode.ExtensionContext, apiConfiguration: ApiConfiguration) {
|
||||
const {
|
||||
apiProvider,
|
||||
apiModelId,
|
||||
apiKey,
|
||||
openRouterApiKey,
|
||||
awsAccessKey,
|
||||
@@ -517,19 +574,13 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
awsProfile,
|
||||
awsUseProfile,
|
||||
awsAuthentication,
|
||||
awsBedrockCustomSelected,
|
||||
awsBedrockCustomModelBaseId,
|
||||
vertexProjectId,
|
||||
vertexRegion,
|
||||
openAiBaseUrl,
|
||||
openAiApiKey,
|
||||
openAiModelId,
|
||||
openAiModelInfo,
|
||||
openAiHeaders,
|
||||
ollamaModelId,
|
||||
ollamaBaseUrl,
|
||||
ollamaApiOptionsCtxNum,
|
||||
lmStudioModelId,
|
||||
lmStudioBaseUrl,
|
||||
anthropicBaseUrl,
|
||||
geminiApiKey,
|
||||
@@ -537,21 +588,13 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
openAiNativeApiKey,
|
||||
deepSeekApiKey,
|
||||
requestyApiKey,
|
||||
requestyModelId,
|
||||
requestyModelInfo,
|
||||
togetherApiKey,
|
||||
togetherModelId,
|
||||
qwenApiKey,
|
||||
doubaoApiKey,
|
||||
mistralApiKey,
|
||||
azureApiVersion,
|
||||
openRouterModelId,
|
||||
openRouterModelInfo,
|
||||
openRouterProviderSorting,
|
||||
vsCodeLmModelSelector,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmModelId,
|
||||
liteLlmModelInfo,
|
||||
liteLlmApiKey,
|
||||
liteLlmUsePromptCache,
|
||||
qwenApiLine,
|
||||
@@ -559,19 +602,14 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
asksageApiKey,
|
||||
asksageApiUrl,
|
||||
xaiApiKey,
|
||||
thinkingBudgetTokens,
|
||||
reasoningEffort,
|
||||
clineAccountId,
|
||||
sambanovaApiKey,
|
||||
cerebrasApiKey,
|
||||
groqApiKey,
|
||||
groqModelId,
|
||||
groqModelInfo,
|
||||
moonshotApiKey,
|
||||
nebiusApiKey,
|
||||
favoritedModelIds,
|
||||
fireworksApiKey,
|
||||
fireworksModelId,
|
||||
fireworksModelMaxCompletionTokens,
|
||||
fireworksModelMaxTokens,
|
||||
sapAiCoreClientId,
|
||||
@@ -579,35 +617,113 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
sapAiCoreBaseUrl,
|
||||
sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup,
|
||||
sapAiCoreModelId,
|
||||
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
|
||||
|
||||
// OPTIMIZED: Batch all global state updates into 2 operations instead of 47
|
||||
const batchedGlobalUpdates = {
|
||||
// Ephemeral model config updates (20 keys)
|
||||
apiProvider,
|
||||
apiModelId,
|
||||
thinkingBudgetTokens,
|
||||
reasoningEffort,
|
||||
vsCodeLmModelSelector,
|
||||
awsBedrockCustomSelected,
|
||||
awsBedrockCustomModelBaseId,
|
||||
openRouterModelId,
|
||||
openRouterModelInfo,
|
||||
openAiModelId,
|
||||
openAiModelInfo,
|
||||
ollamaModelId,
|
||||
lmStudioModelId,
|
||||
liteLlmModelId,
|
||||
liteLlmModelInfo,
|
||||
requestyModelId,
|
||||
requestyModelInfo,
|
||||
togetherModelId,
|
||||
fireworksModelId,
|
||||
groqModelId,
|
||||
groqModelInfo,
|
||||
sapAiCoreModelId,
|
||||
// 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,
|
||||
@@ -672,6 +788,7 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
nebiusApiKey,
|
||||
sapAiCoreClientId,
|
||||
sapAiCoreClientSecret,
|
||||
huggingFaceApiKey,
|
||||
}
|
||||
|
||||
// Execute batched operations in parallel for maximum performance
|
||||
@@ -715,6 +832,7 @@ export async function resetGlobalState(context: vscode.ExtensionContext) {
|
||||
"groqApiKey",
|
||||
"moonshotApiKey",
|
||||
"nebiusApiKey",
|
||||
"huggingFaceApiKey",
|
||||
]
|
||||
for (const key of secretKeys) {
|
||||
await storeSecret(context, key, undefined)
|
||||
|
||||
@@ -54,6 +54,7 @@ import { TaskState } from "./TaskState"
|
||||
import { MessageStateHandler } from "./message-state"
|
||||
import { AutoApprove } from "./tools/autoApprove"
|
||||
import { showNotificationForApprovalIfAutoApprovalEnabled } from "./utils"
|
||||
import { ChatSettings } from "@/shared/ChatSettings"
|
||||
|
||||
export class ToolExecutor {
|
||||
private autoApprover: AutoApprove
|
||||
@@ -90,6 +91,7 @@ export class ToolExecutor {
|
||||
private browserSettings: BrowserSettings,
|
||||
private cwd: string,
|
||||
private taskId: string,
|
||||
private chatSettings: ChatSettings,
|
||||
|
||||
// Callbacks to the Task (Entity)
|
||||
private say: (
|
||||
@@ -634,7 +636,7 @@ export class ToolExecutor {
|
||||
}
|
||||
await this.diffViewProvider.update(newContent, true)
|
||||
await setTimeoutPromise(300) // wait for diff view to update
|
||||
this.diffViewProvider.scrollToFirstDiff()
|
||||
await this.diffViewProvider.scrollToFirstDiff()
|
||||
// showOmissionWarning(this.diffViewProvider.originalContent || "", newContent)
|
||||
|
||||
const completeMessage = JSON.stringify({
|
||||
@@ -1917,7 +1919,12 @@ 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 providerAndModel = `${await getGlobalState(this.context, "apiProvider")} / ${this.api.getModel().id}`
|
||||
const currentMode = this.chatSettings.mode
|
||||
const apiProvider =
|
||||
currentMode === "plan"
|
||||
? await getGlobalState(this.context, "planModeApiProvider")
|
||||
: await getGlobalState(this.context, "actModeApiProvider")
|
||||
const providerAndModel = `${apiProvider} / ${this.api.getModel().id}`
|
||||
|
||||
// Ask user for confirmation
|
||||
const bugReportData = JSON.stringify({
|
||||
|
||||
+131
-63
@@ -4,8 +4,9 @@ import { AnthropicHandler } from "@api/providers/anthropic"
|
||||
import { ClineHandler } from "@api/providers/cline"
|
||||
import { OpenRouterHandler } from "@api/providers/openrouter"
|
||||
import { ApiStream } from "@api/transform/stream"
|
||||
import { DIFF_VIEW_URI_SCHEME } from "@hosts/vscode/VscodeDiffViewProvider"
|
||||
import CheckpointTracker from "@integrations/checkpoints/CheckpointTracker"
|
||||
import { DIFF_VIEW_URI_SCHEME, DiffViewProvider } from "@integrations/editor/DiffViewProvider"
|
||||
import { DiffViewProvider } from "@integrations/editor/DiffViewProvider"
|
||||
import { formatContentBlockToMarkdown } from "@integrations/misc/export-markdown"
|
||||
import { showSystemNotification } from "@integrations/notifications"
|
||||
import { TerminalManager } from "@integrations/terminal/TerminalManager"
|
||||
@@ -36,6 +37,9 @@ import pWaitFor from "p-wait-for"
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { ClineErrorType } from "@/services/error/ClineError"
|
||||
import { ErrorService } from "@/services/error/ErrorService"
|
||||
import { parseAssistantMessageV2, parseAssistantMessageV3, ToolUseName } from "@core/assistant-message"
|
||||
import {
|
||||
checkIsAnthropicContextWindowError,
|
||||
@@ -81,9 +85,7 @@ import { refreshWorkflowToggles } from "../context/instructions/user-instruction
|
||||
import { MessageStateHandler } from "./message-state"
|
||||
import { TaskState } from "./TaskState"
|
||||
import { ToolExecutor } from "./ToolExecutor"
|
||||
import { extractErrorDetails, formatErrorWithStatusCode, updateApiReqMsg } from "./utils"
|
||||
import { createDiffViewProvider, getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
|
||||
import { updateApiReqMsg } from "./utils"
|
||||
export const USE_EXPERIMENTAL_CLAUDE4_FEATURES = false
|
||||
|
||||
export type ToolResponse = string | Array<Anthropic.TextBlockParam | Anthropic.ImageBlockParam>
|
||||
@@ -165,8 +167,19 @@ export class Task {
|
||||
this.reinitExistingTaskFromId = reinitExistingTaskFromId
|
||||
this.cancelTask = cancelTask
|
||||
this.clineIgnoreController = new ClineIgnoreController(cwd)
|
||||
// Initialization moved to startTask/resumeTaskFromHistory
|
||||
this.terminalManager = new TerminalManager()
|
||||
|
||||
// TODO(ae) this is a hack to replace the terminal manager for standalone,
|
||||
// until we have proper host bridge support for terminal execution. The
|
||||
// standaloneTerminalManager is defined in the vscode-impls and injected
|
||||
// during compilation of the standalone manager only, so this variable only
|
||||
// exists in that case
|
||||
if ((global as any).standaloneTerminalManager) {
|
||||
console.log("[DEBUG] Using vscode-impls.js terminal manager")
|
||||
this.terminalManager = (global as any).standaloneTerminalManager
|
||||
} else {
|
||||
console.log("[DEBUG] Using built in terminal manager")
|
||||
this.terminalManager = new TerminalManager()
|
||||
}
|
||||
this.terminalManager.setShellIntegrationTimeout(shellIntegrationTimeout)
|
||||
this.terminalManager.setTerminalReuseEnabled(terminalReuseEnabled ?? true)
|
||||
this.terminalManager.setTerminalOutputLineLimit(terminalOutputLineLimit)
|
||||
@@ -175,7 +188,7 @@ export class Task {
|
||||
this.urlContentFetcher = new UrlContentFetcher(context)
|
||||
this.browserSession = new BrowserSession(context, browserSettings)
|
||||
this.contextManager = new ContextManager()
|
||||
this.diffViewProvider = createDiffViewProvider()
|
||||
this.diffViewProvider = HostProvider.get().createDiffViewProvider()
|
||||
this.autoApprovalSettings = autoApprovalSettings
|
||||
this.browserSettings = browserSettings
|
||||
this.chatSettings = chatSettings
|
||||
@@ -193,6 +206,9 @@ export class Task {
|
||||
this.taskId = historyItem.id
|
||||
this.taskIsFavorited = historyItem.isFavorited
|
||||
this.taskState.conversationHistoryDeletedRange = historyItem.conversationHistoryDeletedRange
|
||||
if (historyItem.checkpointTrackerErrorMessage) {
|
||||
this.taskState.checkpointTrackerErrorMessage = historyItem.checkpointTrackerErrorMessage
|
||||
}
|
||||
} else if (task || images || files) {
|
||||
this.taskId = Date.now().toString()
|
||||
} else {
|
||||
@@ -249,12 +265,19 @@ export class Task {
|
||||
},
|
||||
}
|
||||
|
||||
if (apiConfiguration.apiProvider === "openai" || apiConfiguration.apiProvider === "openai-native") {
|
||||
effectiveApiConfiguration.reasoningEffort = chatSettings.openAIReasoningEffort
|
||||
const currentProvider =
|
||||
chatSettings.mode === "plan" ? apiConfiguration.planModeApiProvider : apiConfiguration.actModeApiProvider
|
||||
|
||||
if (currentProvider === "openai" || currentProvider === "openai-native") {
|
||||
if (chatSettings.mode === "plan") {
|
||||
effectiveApiConfiguration.planModeReasoningEffort = chatSettings.openAIReasoningEffort
|
||||
} else {
|
||||
effectiveApiConfiguration.actModeReasoningEffort = chatSettings.openAIReasoningEffort
|
||||
}
|
||||
}
|
||||
|
||||
// Now that taskId is initialized, we can build the API handler
|
||||
this.api = buildApiHandler(effectiveApiConfiguration)
|
||||
this.api = buildApiHandler(effectiveApiConfiguration, chatSettings.mode)
|
||||
|
||||
// Set taskId on browserSession for telemetry tracking
|
||||
this.browserSession.setTaskId(this.taskId)
|
||||
@@ -269,10 +292,10 @@ export class Task {
|
||||
// initialize telemetry
|
||||
if (historyItem) {
|
||||
// Open task from history
|
||||
telemetryService.captureTaskRestarted(this.taskId, apiConfiguration.apiProvider)
|
||||
telemetryService.captureTaskRestarted(this.taskId, currentProvider)
|
||||
} else {
|
||||
// New task started
|
||||
telemetryService.captureTaskCreated(this.taskId, apiConfiguration.apiProvider)
|
||||
telemetryService.captureTaskCreated(this.taskId, currentProvider)
|
||||
}
|
||||
|
||||
this.toolExecutor = new ToolExecutor(
|
||||
@@ -292,6 +315,7 @@ export class Task {
|
||||
this.browserSettings,
|
||||
cwd,
|
||||
this.taskId,
|
||||
this.chatSettings,
|
||||
this.say.bind(this),
|
||||
this.ask.bind(this),
|
||||
this.saveCheckpoint.bind(this),
|
||||
@@ -1042,7 +1066,9 @@ export class Task {
|
||||
let responseFiles: string[] | undefined
|
||||
if (response === "messageResponse") {
|
||||
await this.say("user_feedback", text, images, files)
|
||||
await this.saveCheckpoint()
|
||||
if (!this.taskState.checkpointTrackerErrorMessage?.includes("Checkpoints initialization timed out.")) {
|
||||
await this.saveCheckpoint()
|
||||
}
|
||||
responseText = text
|
||||
responseImages = images
|
||||
responseFiles = files
|
||||
@@ -1192,8 +1218,9 @@ export class Task {
|
||||
await this.browserSession.dispose()
|
||||
this.clineIgnoreController.dispose()
|
||||
this.fileContextTracker.dispose()
|
||||
await this.diffViewProvider.revertChanges() // need to await for when we want to make sure directories/files are reverted before re-starting the task from a checkpoint
|
||||
|
||||
// need to await for when we want to make sure directories/files are reverted before
|
||||
// re-starting the task from a checkpoint
|
||||
await this.diffViewProvider.revertChanges()
|
||||
// Clear the notification callback when task is aborted
|
||||
this.mcpHub.clearNotificationCallback()
|
||||
}
|
||||
@@ -1201,8 +1228,11 @@ export class Task {
|
||||
// Checkpoints
|
||||
|
||||
async saveCheckpoint(isAttemptCompletionMessage: boolean = false) {
|
||||
if (!this.enableCheckpoints) {
|
||||
// If checkpoints are disabled, do nothing.
|
||||
if (
|
||||
!this.enableCheckpoints ||
|
||||
this.taskState.checkpointTrackerErrorMessage?.includes("Checkpoints initialization timed out.")
|
||||
) {
|
||||
// If checkpoints are disabled or previously encountered a timeout error, do nothing.
|
||||
return
|
||||
}
|
||||
// Set isCheckpointCheckedOut to false for all checkpoint_created messages
|
||||
@@ -1256,8 +1286,11 @@ export class Task {
|
||||
//
|
||||
} else {
|
||||
// attempt completion requires checkpoint to be sync so that we can present button after attempt_completion
|
||||
// Check if checkpoint tracker exists, if not, create it
|
||||
if (!this.checkpointTracker) {
|
||||
// Check if checkpoint tracker exists, if not, create it. Skip if there was a previous checkpoints initialization timeout error.
|
||||
if (
|
||||
!this.checkpointTracker &&
|
||||
!this.taskState.checkpointTrackerErrorMessage?.includes("Checkpoints initialization timed out.")
|
||||
) {
|
||||
try {
|
||||
this.checkpointTracker = await CheckpointTracker.create(
|
||||
this.taskId,
|
||||
@@ -1272,7 +1305,10 @@ export class Task {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.checkpointTracker) {
|
||||
if (
|
||||
this.checkpointTracker &&
|
||||
!this.taskState.checkpointTrackerErrorMessage?.includes("Checkpoints initialization timed out.")
|
||||
) {
|
||||
const commitHash = await this.checkpointTracker.commit()
|
||||
|
||||
// For attempt_completion, find the last completion_result message and set its checkpoint hash. This will be used to present the 'see new changes' button
|
||||
@@ -1413,7 +1449,7 @@ export class Task {
|
||||
Logger.info("Executing command in Node: " + command)
|
||||
return this.executeCommandInNode(command)
|
||||
}
|
||||
Logger.info("Executing command in VS code terminal: " + command)
|
||||
Logger.info("Executing command in terminal: " + command)
|
||||
|
||||
const terminalInfo = await this.terminalManager.getOrCreateTerminal(this.cwd)
|
||||
terminalInfo.terminal.show() // weird visual bug when creating new terminals (even manually) where there's an empty space at the top.
|
||||
@@ -1577,6 +1613,15 @@ export class Task {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
return { modelId, providerId }
|
||||
}
|
||||
|
||||
async *attemptApiRequest(previousApiReqIndex: number): ApiStream {
|
||||
// Wait for MCP servers to be connected before generating system prompt
|
||||
await pWaitFor(() => this.mcpHub.isConnecting !== true, { timeout: 10_000 }).catch(() => {
|
||||
@@ -1671,16 +1716,17 @@ export class Task {
|
||||
const isAnthropic = this.api instanceof AnthropicHandler
|
||||
const isOpenRouterContextWindowError = checkIsOpenRouterContextWindowError(error) && isOpenRouter
|
||||
const isAnthropicContextWindowError = checkIsAnthropicContextWindowError(error) && isAnthropic
|
||||
const { modelId, providerId } = await this.getCurrentProviderInfo()
|
||||
const clineError = ErrorService.toClineError(error, modelId, providerId)
|
||||
|
||||
const { statusCode, message, requestId } = extractErrorDetails(error)
|
||||
|
||||
// Capture provider failure telemetry
|
||||
// Capture provider failure telemetry using clineError
|
||||
// TODO: Move into ErrorService
|
||||
telemetryService.captureProviderApiError({
|
||||
taskId: this.taskId,
|
||||
model: modelInfo.id,
|
||||
errorMessage: message,
|
||||
errorStatus: statusCode,
|
||||
requestId,
|
||||
errorMessage: clineError.message,
|
||||
errorStatus: clineError._error?.status,
|
||||
requestId: clineError._error?.request_id,
|
||||
})
|
||||
|
||||
if (isAnthropic && isAnthropicContextWindowError && !this.taskState.didAutomaticallyRetryFailedApiRequest) {
|
||||
@@ -1726,12 +1772,12 @@ export class Task {
|
||||
// If the conversation has more than 3 messages, we can truncate again. If not, then the conversation is bricked.
|
||||
// ToDo: Allow the user to change their input if this is the case.
|
||||
if (truncatedConversationHistory.length > 3) {
|
||||
error = new Error("Context window exceeded. Click retry to truncate the conversation and try again.")
|
||||
clineError.message = "Context window exceeded. Click retry to truncate the conversation and try again."
|
||||
this.taskState.didAutomaticallyRetryFailedApiRequest = false
|
||||
}
|
||||
}
|
||||
|
||||
const errorMessage = formatErrorWithStatusCode(error)
|
||||
const streamingFailedMessage = clineError.serialize()
|
||||
|
||||
// Update the 'api_req_started' message to reflect final failure before asking user to manually retry
|
||||
const lastApiReqStartedIndex = findLastIndex(
|
||||
@@ -1747,19 +1793,24 @@ export class Task {
|
||||
text: JSON.stringify({
|
||||
...currentApiReqInfo, // Spread the modified info (with retryStatus removed)
|
||||
// cancelReason: "retries_exhausted", // Indicate that automatic retries failed
|
||||
streamingFailedMessage: errorMessage,
|
||||
streamingFailedMessage,
|
||||
} satisfies ClineApiReqInfo),
|
||||
})
|
||||
// this.ask will trigger postStateToWebview, so this change should be picked up.
|
||||
}
|
||||
|
||||
const { response } = await this.ask("api_req_failed", errorMessage)
|
||||
const { response } = await this.ask("api_req_failed", streamingFailedMessage)
|
||||
|
||||
if (response !== "yesButtonClicked") {
|
||||
// this will never happen since if noButtonClicked, we will clear current task, aborting this instance
|
||||
throw new Error("API request failed")
|
||||
}
|
||||
|
||||
// Do not retry automatically again if currently unauthenticated
|
||||
if (clineError.isErrorType(ClineErrorType.Auth)) {
|
||||
return
|
||||
}
|
||||
|
||||
await this.say("api_req_retried")
|
||||
}
|
||||
// delegate generator output from the recursive call
|
||||
@@ -1895,10 +1946,10 @@ export class Task {
|
||||
}
|
||||
|
||||
// Used to know what models were used in the task if user wants to export metadata for error reporting purposes
|
||||
const currentProviderId = (await getGlobalState(this.getContext(), "apiProvider")) as string
|
||||
if (currentProviderId && this.api.getModel().id) {
|
||||
const { modelId, providerId } = await this.getCurrentProviderInfo()
|
||||
if (providerId && modelId) {
|
||||
try {
|
||||
await this.modelContextTracker.recordModelUsage(currentProviderId, this.api.getModel().id, this.chatSettings.mode)
|
||||
await this.modelContextTracker.recordModelUsage(providerId, modelId, this.chatSettings.mode)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
@@ -2014,6 +2065,20 @@ export class Task {
|
||||
!this.taskState.checkpointTrackerErrorMessage
|
||||
) {
|
||||
try {
|
||||
// Warning Timer - If checkpoints take a while to to initialize, show a warning message
|
||||
let checkpointsWarningTimer: NodeJS.Timeout | null = null
|
||||
let checkpointsWarningShown = false
|
||||
|
||||
checkpointsWarningTimer = setTimeout(async () => {
|
||||
if (!checkpointsWarningShown) {
|
||||
checkpointsWarningShown = true
|
||||
this.taskState.checkpointTrackerErrorMessage =
|
||||
"Checkpoints are taking longer than expected to initialize. Working in a large repository? Consider re-opening Cline in a project that uses git, or disabling checkpoints."
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
}, 7_000)
|
||||
|
||||
// Timeout - If checkpoints take too long to initialize, warn user and disable checkpoints for the task
|
||||
this.checkpointTracker = await pTimeout(
|
||||
CheckpointTracker.create(this.taskId, this.context.globalStorageUri.fsPath, this.enableCheckpoints),
|
||||
{
|
||||
@@ -2022,10 +2087,22 @@ export class Task {
|
||||
"Checkpoints taking too long to initialize. Consider re-opening Cline in a project that uses git, or disabling checkpoints.",
|
||||
},
|
||||
)
|
||||
if (checkpointsWarningTimer) {
|
||||
clearTimeout(checkpointsWarningTimer)
|
||||
checkpointsWarningTimer = null
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error"
|
||||
console.error("Failed to initialize checkpoint tracker:", errorMessage)
|
||||
this.taskState.checkpointTrackerErrorMessage = errorMessage // will be displayed right away since we saveClineMessages next which posts state to webview
|
||||
|
||||
// If the error was a timeout, we disabled all checkpoint operations for the rest of the task
|
||||
if (errorMessage.includes("Checkpoints taking too long to initialize")) {
|
||||
this.taskState.checkpointTrackerErrorMessage =
|
||||
"Checkpoints initialization timed out. Consider re-opening Cline in a project that uses git, or disabling checkpoints."
|
||||
await this.postStateToWebview()
|
||||
} else {
|
||||
this.taskState.checkpointTrackerErrorMessage = errorMessage // will be displayed right away since we saveClineMessages next which posts state to webview
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2076,7 +2153,7 @@ export class Task {
|
||||
content: userContent,
|
||||
})
|
||||
|
||||
telemetryService.captureConversationTurnEvent(this.taskId, currentProviderId, this.api.getModel().id, "user")
|
||||
telemetryService.captureConversationTurnEvent(this.taskId, providerId, modelId, "user")
|
||||
|
||||
// since we sent off a placeholder api_req_started message to update the webview while waiting to actually start the API request (to load potential details for example), we need to update the text of that message
|
||||
const lastApiReqIndex = findLastIndex(this.messageStateHandler.getClineMessages(), (m) => m.say === "api_req_started")
|
||||
@@ -2141,19 +2218,13 @@ export class Task {
|
||||
})
|
||||
await this.messageStateHandler.saveClineMessagesAndUpdateHistory()
|
||||
|
||||
telemetryService.captureConversationTurnEvent(
|
||||
this.taskId,
|
||||
currentProviderId,
|
||||
this.api.getModel().id,
|
||||
"assistant",
|
||||
{
|
||||
tokensIn: inputTokens,
|
||||
tokensOut: outputTokens,
|
||||
cacheWriteTokens,
|
||||
cacheReadTokens,
|
||||
totalCost,
|
||||
},
|
||||
)
|
||||
telemetryService.captureConversationTurnEvent(this.taskId, providerId, this.api.getModel().id, "assistant", {
|
||||
tokensIn: inputTokens,
|
||||
tokensOut: outputTokens,
|
||||
cacheWriteTokens,
|
||||
cacheReadTokens,
|
||||
totalCost,
|
||||
})
|
||||
|
||||
// signals to provider that it can retrieve the saved messages from disk, as abortTask can not be awaited on in nature
|
||||
this.taskState.didFinishAbortingStream = true
|
||||
@@ -2250,7 +2321,8 @@ export class Task {
|
||||
// abandoned happens when extension is no longer waiting for the cline instance to finish aborting (error is thrown here when any function in the for loop throws due to this.abort)
|
||||
if (!this.taskState.abandoned) {
|
||||
this.abortTask() // if the stream failed, there's various states the task could be in (i.e. could have streamed some tools the user may have executed), so we just resort to replicating a cancel task
|
||||
const errorMessage = formatErrorWithStatusCode(error)
|
||||
const clineError = ErrorService.toClineError(error, this.api.getModel().id)
|
||||
const errorMessage = clineError.serialize()
|
||||
|
||||
await abortStream("streaming_failed", errorMessage)
|
||||
await this.reinitExistingTaskFromId(this.taskId)
|
||||
@@ -2320,19 +2392,13 @@ export class Task {
|
||||
// need to save assistant responses to file before proceeding to tool use since user can exit at any moment and we wouldn't be able to save the assistant's response
|
||||
let didEndLoop = false
|
||||
if (assistantMessage.length > 0) {
|
||||
telemetryService.captureConversationTurnEvent(
|
||||
this.taskId,
|
||||
currentProviderId,
|
||||
this.api.getModel().id,
|
||||
"assistant",
|
||||
{
|
||||
tokensIn: inputTokens,
|
||||
tokensOut: outputTokens,
|
||||
cacheWriteTokens,
|
||||
cacheReadTokens,
|
||||
totalCost,
|
||||
},
|
||||
)
|
||||
telemetryService.captureConversationTurnEvent(this.taskId, providerId, modelId, "assistant", {
|
||||
tokensIn: inputTokens,
|
||||
tokensOut: outputTokens,
|
||||
cacheWriteTokens,
|
||||
cacheReadTokens,
|
||||
totalCost,
|
||||
})
|
||||
|
||||
await this.messageStateHandler.addToApiConversationHistory({
|
||||
role: "assistant",
|
||||
@@ -2378,6 +2444,8 @@ export class Task {
|
||||
},
|
||||
],
|
||||
})
|
||||
// Returns early to avoid retry since no assistant message was received
|
||||
return true
|
||||
}
|
||||
|
||||
return didEndLoop // will always be false for now
|
||||
|
||||
@@ -20,6 +20,7 @@ interface MessageStateHandlerParams {
|
||||
taskIsFavorited?: boolean
|
||||
updateTaskHistory: (historyItem: HistoryItem) => Promise<HistoryItem[]>
|
||||
taskState: TaskState
|
||||
checkpointTrackerErrorMessage?: string
|
||||
}
|
||||
|
||||
export class MessageStateHandler {
|
||||
@@ -27,6 +28,7 @@ export class MessageStateHandler {
|
||||
private clineMessages: ClineMessage[] = []
|
||||
private taskIsFavorited: boolean
|
||||
private checkpointTracker: CheckpointTracker | undefined
|
||||
private checkpointTrackerErrorMessage: string | undefined
|
||||
private updateTaskHistory: (historyItem: HistoryItem) => Promise<HistoryItem[]>
|
||||
private context: vscode.ExtensionContext
|
||||
private taskId: string
|
||||
@@ -38,6 +40,7 @@ export class MessageStateHandler {
|
||||
this.taskState = params.taskState
|
||||
this.taskIsFavorited = params.taskIsFavorited ?? false
|
||||
this.updateTaskHistory = params.updateTaskHistory
|
||||
this.checkpointTrackerErrorMessage = this.taskState.checkpointTrackerErrorMessage
|
||||
}
|
||||
|
||||
setCheckpointTracker(tracker: CheckpointTracker | undefined) {
|
||||
@@ -98,6 +101,7 @@ export class MessageStateHandler {
|
||||
cwdOnTaskInitialization: cwd,
|
||||
conversationHistoryDeletedRange: this.taskState.conversationHistoryDeletedRange,
|
||||
isFavorited: this.taskIsFavorited,
|
||||
checkpointTrackerErrorMessage: this.taskState.checkpointTrackerErrorMessage,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to save cline messages:", error)
|
||||
|
||||
@@ -1,25 +1,9 @@
|
||||
import { showSystemNotification } from "@/integrations/notifications"
|
||||
import { ClineApiReqCancelReason, ClineApiReqInfo } from "@/shared/ExtensionMessage"
|
||||
import { serializeError } from "serialize-error"
|
||||
import { MessageStateHandler } from "./message-state"
|
||||
import { calculateApiCostAnthropic } from "@/utils/cost"
|
||||
import { ApiHandler } from "@/api"
|
||||
|
||||
export function formatErrorWithStatusCode(error: any): string {
|
||||
const { statusCode, message } = extractErrorDetails(error)
|
||||
|
||||
// Only prepend the statusCode if it's not already part of the message
|
||||
return statusCode && !message.includes(statusCode.toString()) ? `${statusCode} - ${message}` : message
|
||||
}
|
||||
|
||||
export function extractErrorDetails(error: any): { message: string; statusCode?: number; requestId?: string } {
|
||||
const statusCode = error.status || error.statusCode || (error.response && error.response?.status)
|
||||
const message = error.message ?? JSON.stringify(serializeError(error), null, 2)
|
||||
const requestId = error.request_id || error.response?.request_id || undefined
|
||||
|
||||
return { message, statusCode, requestId }
|
||||
}
|
||||
|
||||
export const showNotificationForApprovalIfAutoApprovalEnabled = (
|
||||
message: string,
|
||||
autoApprovalSettingsEnabled: boolean,
|
||||
|
||||
@@ -10,7 +10,7 @@ import path from "node:path"
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
import { Uri } from "vscode"
|
||||
import { ExtensionMessage } from "@/shared/ExtensionMessage"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
|
||||
export abstract class WebviewProvider {
|
||||
@@ -264,13 +264,11 @@ export abstract class WebviewProvider {
|
||||
} catch (error) {
|
||||
// Only show the error message if not in development mode.
|
||||
if (!process.env.IS_DEV) {
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message:
|
||||
"Cline: Local webview dev server is not running, HMR will not work. Please run 'npm run dev:webview' before launching the extension to enable HMR. Using bundled assets.",
|
||||
}),
|
||||
)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message:
|
||||
"Cline: Local webview dev server is not running, HMR will not work. Please run 'npm run dev:webview' before launching the extension to enable HMR. Using bundled assets.",
|
||||
})
|
||||
}
|
||||
|
||||
return this.getHtmlContent()
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Controller } from "@core/controller"
|
||||
import { HistoryItem } from "@shared/HistoryItem"
|
||||
import { ClineMessage } from "@shared/ExtensionMessage"
|
||||
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
|
||||
/**
|
||||
* Registers development-only commands for task manipulation.
|
||||
@@ -99,12 +99,10 @@ export function registerTaskCommands(context: vscode.ExtensionContext, controlle
|
||||
await controller.postStateToWebview()
|
||||
|
||||
const message = `Created ${tasksCount} test tasks`
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message,
|
||||
})
|
||||
},
|
||||
)
|
||||
}),
|
||||
|
||||
+54
-65
@@ -6,11 +6,10 @@ 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 "./integrations/editor/DiffViewProvider"
|
||||
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 { Controller } from "./core/controller"
|
||||
import { sendMcpButtonClickedEvent } from "./core/controller/ui/subscribeToMcpButtonClicked"
|
||||
import { sendChatButtonClickedEvent } from "./core/controller/ui/subscribeToChatButtonClicked"
|
||||
import { ErrorService } from "./services/error/ErrorService"
|
||||
@@ -27,19 +26,20 @@ import {
|
||||
migrateCustomInstructionsToGlobalRules,
|
||||
migrateModeFromWorkspaceStorageToControllerState,
|
||||
migrateWelcomeViewCompleted,
|
||||
migrateLegacyApiConfigurationToModeSpecific,
|
||||
} from "./core/storage/state-migrations"
|
||||
|
||||
import { sendFocusChatInputEvent } from "./core/controller/ui/subscribeToFocusChatInput"
|
||||
import { FileContextTracker } from "./core/context/context-tracking/FileContextTracker"
|
||||
import * as hostProviders from "@hosts/host-providers"
|
||||
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 { getHostBridgeProvider } from "@hosts/host-providers"
|
||||
import { ShowMessageRequest, ShowMessageType } from "./shared/proto/host/window"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { ShowMessageType } from "./shared/proto/host/window"
|
||||
import { GitCommitGenerator } from "./integrations/git/commit-message-generator"
|
||||
/*
|
||||
Built using https://github.com/microsoft/vscode-webview-ui-toolkit
|
||||
|
||||
@@ -75,13 +75,16 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
// 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)
|
||||
|
||||
// Version checking for autoupdate notification
|
||||
const currentVersion = context.extension.packageJSON.version
|
||||
const previousVersion = context.globalState.get<string>("clineVersion")
|
||||
const sidebarWebview = hostProviders.createWebviewProvider(WebviewProviderType.SIDEBAR)
|
||||
const sidebarWebview = HostProvider.get().createWebviewProvider(WebviewProviderType.SIDEBAR)
|
||||
|
||||
const testModeWatchers = await initializeTestMode(sidebarWebview)
|
||||
// Initialize test mode and add disposables to context
|
||||
@@ -106,12 +109,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
const message = `Cline has been updated to v${currentVersion}`
|
||||
await vscode.commands.executeCommand("claude-dev.SidebarProvider.focus")
|
||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message,
|
||||
}),
|
||||
)
|
||||
HostProvider.window.showMessage({ type: ShowMessageType.INFORMATION, message })
|
||||
// Record that we've shown the popup for this version.
|
||||
await context.globalState.update("clineLastPopupNotificationVersion", currentVersion)
|
||||
}
|
||||
@@ -190,7 +188,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
Logger.log("Opening Cline in new tab")
|
||||
// (this example uses webviewProvider activation event which is necessary to deserialize cached webview, but since we use retainContextWhenHidden, we don't need to use that event)
|
||||
// https://github.com/microsoft/vscode-extension-samples/blob/main/webview-sample/src/extension.ts
|
||||
const tabWebview = hostProviders.createWebviewProvider(WebviewProviderType.TAB)
|
||||
const tabWebview = HostProvider.get().createWebviewProvider(WebviewProviderType.TAB)
|
||||
//const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined
|
||||
const lastCol = Math.max(...vscode.window.visibleTextEditors.map((editor) => editor.viewColumn || 0))
|
||||
|
||||
@@ -413,12 +411,10 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
// Ensure clipboard is restored even if an error occurs
|
||||
await writeTextToClipboard(tempCopyBuffer)
|
||||
console.error("Error getting terminal contents:", error)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Failed to get terminal contents",
|
||||
}),
|
||||
)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Failed to get terminal contents",
|
||||
})
|
||||
}
|
||||
}),
|
||||
)
|
||||
@@ -554,12 +550,10 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
}
|
||||
const selectedText = editor.document.getText(range)
|
||||
if (!selectedText.trim()) {
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Please select some code to explain.",
|
||||
}),
|
||||
)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Please select some code to explain.",
|
||||
})
|
||||
return
|
||||
}
|
||||
const filePath = editor.document.uri.fsPath
|
||||
@@ -581,12 +575,10 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
}
|
||||
const selectedText = editor.document.getText(range)
|
||||
if (!selectedText.trim()) {
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Please select some code to improve.",
|
||||
}),
|
||||
)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Please select some code to improve.",
|
||||
})
|
||||
return
|
||||
}
|
||||
const filePath = editor.document.uri.fsPath
|
||||
@@ -651,12 +643,10 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
sendFocusChatInputEvent(clientId)
|
||||
} else {
|
||||
console.error("FocusChatInput: Could not find or activate a Cline webview to focus.")
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Could not activate Cline view. Please try opening it manually from the Activity Bar.",
|
||||
}),
|
||||
)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Could not activate Cline view. Please try opening it manually from the Activity Bar.",
|
||||
})
|
||||
}
|
||||
telemetryService.captureButtonClick("command_focusChatInput", activeWebviewProvider?.controller.task?.taskId)
|
||||
}),
|
||||
@@ -672,28 +662,27 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
|
||||
// Register the generateGitCommitMessage command handler
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("cline.generateGitCommitMessage", async () => {
|
||||
// Get the controller from any instance, without activating the view
|
||||
const controller = WebviewProvider.getAllInstances()[0]?.controller
|
||||
|
||||
if (controller) {
|
||||
// Call the controller method to generate commit message
|
||||
await controller.generateGitCommitMessage()
|
||||
} else {
|
||||
// Create a temporary controller just for this operation
|
||||
const outputChannel = vscode.window.createOutputChannel("Cline Commit Generator")
|
||||
const tempController = new Controller(context, outputChannel, () => Promise.resolve(true), uuidv4())
|
||||
|
||||
await tempController.generateGitCommitMessage()
|
||||
outputChannel.dispose()
|
||||
}
|
||||
vscode.commands.registerCommand("cline.generateGitCommitMessage", async (scm) => {
|
||||
await GitCommitGenerator?.generate?.(context, scm)
|
||||
}),
|
||||
vscode.commands.registerCommand("cline.abortGitCommitMessage", () => {
|
||||
GitCommitGenerator?.abort?.()
|
||||
}),
|
||||
)
|
||||
|
||||
context.subscriptions.push(
|
||||
context.secrets.onDidChange((event) => {
|
||||
context.secrets.onDidChange(async (event) => {
|
||||
if (event.key === "clineAccountId") {
|
||||
AuthService.getInstance(context)?.restoreRefreshTokenAndRetrieveAuthInfo()
|
||||
// Check if the secret was removed (logout) or added/updated (login)
|
||||
const secretValue = await context.secrets.get("clineAccountId")
|
||||
const authService = AuthService.getInstance(context)
|
||||
if (secretValue) {
|
||||
// Secret was added or updated - restore auth info (login from another window)
|
||||
authService?.restoreRefreshTokenAndRetrieveAuthInfo()
|
||||
} else {
|
||||
// Secret was removed - handle logout for all windows
|
||||
authService?.handleDeauth()
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
@@ -702,7 +691,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
}
|
||||
|
||||
function maybeSetupHostProviders(context: ExtensionContext) {
|
||||
if (!hostProviders.isSetup) {
|
||||
if (!HostProvider.isInitialized()) {
|
||||
console.log("Setting up vscode host providers...")
|
||||
const createWebview = function (type: WebviewProviderType) {
|
||||
return new VscodeWebviewProvider(context, outputChannel, type)
|
||||
@@ -710,19 +699,10 @@ function maybeSetupHostProviders(context: ExtensionContext) {
|
||||
const createDiffView = function () {
|
||||
return new VscodeDiffViewProvider()
|
||||
}
|
||||
hostProviders.initializeHostProviders(createWebview, createDiffView, vscodeHostBridgeClient)
|
||||
HostProvider.initialize(createWebview, createDiffView, vscodeHostBridgeClient)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Find a solution for automatically removing DEV related content from production builds.
|
||||
// This type of code is fine in production to keep. We just will want to remove it from production builds
|
||||
// to bring down built asset sizes.
|
||||
//
|
||||
// This is a workaround to reload the extension when the source code changes
|
||||
// since vscode doesn't support hot reload for extensions
|
||||
const IS_DEV = process.env.IS_DEV
|
||||
const DEV_WORKSPACE_FOLDER = process.env.DEV_WORKSPACE_FOLDER
|
||||
|
||||
// This method is called when your extension is deactivated
|
||||
export async function deactivate() {
|
||||
// Dispose all webview instances
|
||||
@@ -735,6 +715,15 @@ export async function deactivate() {
|
||||
Logger.log("Cline extension deactivated")
|
||||
}
|
||||
|
||||
// TODO: Find a solution for automatically removing DEV related content from production builds.
|
||||
// This type of code is fine in production to keep. We just will want to remove it from production builds
|
||||
// to bring down built asset sizes.
|
||||
//
|
||||
// This is a workaround to reload the extension when the source code changes
|
||||
// since vscode doesn't support hot reload for extensions
|
||||
const IS_DEV = process.env.IS_DEV
|
||||
const DEV_WORKSPACE_FOLDER = process.env.DEV_WORKSPACE_FOLDER
|
||||
|
||||
// Set up development mode file watcher
|
||||
if (IS_DEV && IS_DEV !== "false") {
|
||||
assert(DEV_WORKSPACE_FOLDER, "DEV_WORKSPACE_FOLDER must be set in development")
|
||||
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { DiffViewProvider } from "@/integrations/editor/DiffViewProvider"
|
||||
import { status } from "@grpc/grpc-js"
|
||||
|
||||
export class ExternalDiffViewProvider extends DiffViewProvider {
|
||||
private activeDiffEditorId: string | undefined
|
||||
|
||||
override async openDiffEditor(): Promise<void> {
|
||||
if (!this.absolutePath) {
|
||||
return
|
||||
}
|
||||
const response = await HostProvider.diff.openDiff({
|
||||
path: this.absolutePath,
|
||||
content: this.originalContent ?? "",
|
||||
})
|
||||
this.activeDiffEditorId = response.diffId
|
||||
}
|
||||
|
||||
override async replaceText(
|
||||
content: string,
|
||||
rangeToReplace: { startLine: number; endLine: number },
|
||||
_currentLine: number | undefined,
|
||||
): Promise<void> {
|
||||
if (!this.activeDiffEditorId) {
|
||||
return
|
||||
}
|
||||
await HostProvider.diff.replaceText({
|
||||
diffId: this.activeDiffEditorId,
|
||||
content: content,
|
||||
startLine: rangeToReplace.startLine,
|
||||
endLine: rangeToReplace.endLine,
|
||||
})
|
||||
}
|
||||
|
||||
protected override async truncateDocument(lineNumber: number): Promise<void> {
|
||||
if (!this.activeDiffEditorId) {
|
||||
return
|
||||
}
|
||||
await HostProvider.diff.truncateDocument({
|
||||
diffId: this.activeDiffEditorId,
|
||||
endLine: lineNumber,
|
||||
})
|
||||
}
|
||||
|
||||
protected async saveDocument(): Promise<Boolean> {
|
||||
if (!this.activeDiffEditorId) {
|
||||
return false
|
||||
}
|
||||
try {
|
||||
await HostProvider.diff.saveDocument({ diffId: this.activeDiffEditorId })
|
||||
return true
|
||||
} catch (err: any) {
|
||||
if (err.code === status.NOT_FOUND) {
|
||||
// This can happen when the task is reloaded or the diff editor is closed. So, don't
|
||||
// consider it a real error.
|
||||
console.log("Diff not found:", this.activeDiffEditorId)
|
||||
return false
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override async scrollEditorToLine(_line: number): Promise<void> {}
|
||||
|
||||
override async scrollAnimation(_startLine: number, _endLine: number): Promise<void> {}
|
||||
|
||||
protected override async getDocumentText(): Promise<string | undefined> {
|
||||
if (!this.activeDiffEditorId) {
|
||||
return undefined
|
||||
}
|
||||
return (await HostProvider.diff.getDocumentText({ diffId: this.activeDiffEditorId })).content
|
||||
}
|
||||
|
||||
protected override async getNewDiagnosticProblems(): Promise<string> {
|
||||
console.log(`Called ExternalDiffViewProvider.getNewDiagnosticProblems() stub`)
|
||||
return ""
|
||||
}
|
||||
|
||||
protected override async closeDiffView(): Promise<void> {
|
||||
if (!this.activeDiffEditorId) {
|
||||
return
|
||||
}
|
||||
await HostProvider.diff.closeDiff({ diffId: this.activeDiffEditorId })
|
||||
this.activeDiffEditorId = undefined
|
||||
}
|
||||
|
||||
protected override async resetDiffView(): Promise<void> {
|
||||
this.activeDiffEditorId = undefined
|
||||
}
|
||||
}
|
||||
-5
@@ -4,11 +4,6 @@ import * as vscode from "vscode"
|
||||
import { URI } from "vscode-uri"
|
||||
import { WebviewProvider } from "@core/webview"
|
||||
|
||||
/*
|
||||
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
||||
https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/customSidebarViewProvider.ts
|
||||
*/
|
||||
|
||||
export class ExternalWebviewProvider extends WebviewProvider {
|
||||
// This hostname cannot be changed without updating the external webview handler.
|
||||
private RESOURCE_HOSTNAME: string = "internal.resources"
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as grpc from "@grpc/grpc-js"
|
||||
import { Controller } from "../core/controller"
|
||||
import { Controller } from "@core/controller"
|
||||
import { Channel, createChannel } from "nice-grpc"
|
||||
|
||||
/**
|
||||
* Type definition for a gRPC handler function.
|
||||
@@ -36,3 +37,51 @@ export type GrpcStreamingResponseHandlerWrapper = <TRequest, TResponse>(
|
||||
) => grpc.handleServerStreamingCall<TRequest, TResponse>
|
||||
|
||||
export type StreamingResponseWriter<TResponse> = (response: TResponse, isLast?: boolean, sequenceNumber?: number) => Promise<void>
|
||||
|
||||
/**
|
||||
* Abstract base class for type-safe gRPC client implementations.
|
||||
*
|
||||
* Provides automatic connection management with lazy initialization and
|
||||
* transparent reconnection on network failures. Ensures type safety through
|
||||
* generic client typing and consistent error handling patterns.
|
||||
*
|
||||
* @template TClient - The specific gRPC client type (e.g., niceGrpc.host.DiffServiceClient)
|
||||
*/
|
||||
export abstract class BaseGrpcClient<TClient> {
|
||||
private client: TClient | null = null
|
||||
private channel: Channel | null = null
|
||||
protected address: string
|
||||
|
||||
constructor(address: string) {
|
||||
this.address = address
|
||||
}
|
||||
|
||||
protected abstract createClient(channel: Channel): TClient
|
||||
|
||||
protected getClient(): TClient {
|
||||
if (!this.client || !this.channel) {
|
||||
this.channel = createChannel(this.address)
|
||||
this.client = this.createClient(this.channel)
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
|
||||
protected destroyClient(): void {
|
||||
this.channel?.close()
|
||||
this.client = null
|
||||
this.channel = null
|
||||
}
|
||||
|
||||
protected async makeRequest<T>(requestFn: (client: TClient) => Promise<T>): Promise<T> {
|
||||
const client = this.getClient()
|
||||
|
||||
try {
|
||||
return await requestFn(client)
|
||||
} catch (error: any) {
|
||||
if (error?.code === "UNAVAILABLE") {
|
||||
this.destroyClient()
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+6
-12
@@ -14,14 +14,13 @@ import {
|
||||
DiffServiceClientInterface,
|
||||
} from "@generated/hosts/host-bridge-client-types"
|
||||
import { HostBridgeClientProvider } from "@/hosts/host-provider-types"
|
||||
import { HOSTBRIDGE_PORT } from "./cline-core"
|
||||
import { HOSTBRIDGE_PORT } from "@/standalone/protobus-service"
|
||||
|
||||
/**
|
||||
* Manager to hold the gRPC clients for the host bridge. The clients should be re-used to avoid
|
||||
* creating a new TCP connection every time a rpc is made.
|
||||
*/
|
||||
export class ExternalHostBridgeClientManager implements HostBridgeClientProvider {
|
||||
private channel: Channel
|
||||
watchServiceClient: WatchServiceClientInterface
|
||||
workspaceClient: WorkspaceServiceClientInterface
|
||||
envClient: EnvServiceClientInterface
|
||||
@@ -30,16 +29,11 @@ export class ExternalHostBridgeClientManager implements HostBridgeClientProvider
|
||||
|
||||
constructor() {
|
||||
const address = process.env.HOST_BRIDGE_ADDRESS || `localhost:${HOSTBRIDGE_PORT}`
|
||||
this.channel = createChannel(address)
|
||||
|
||||
this.watchServiceClient = new WatchServiceClientImpl(this.channel)
|
||||
this.workspaceClient = new WorkspaceServiceClientImpl(this.channel)
|
||||
this.envClient = new EnvServiceClientImpl(this.channel)
|
||||
this.windowClient = new WindowServiceClientImpl(this.channel)
|
||||
this.diffClient = new DiffServiceClientImpl(this.channel)
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
this.channel.close()
|
||||
this.watchServiceClient = new WatchServiceClientImpl(address)
|
||||
this.workspaceClient = new WorkspaceServiceClientImpl(address)
|
||||
this.envClient = new EnvServiceClientImpl(address)
|
||||
this.windowClient = new WindowServiceClientImpl(address)
|
||||
this.diffClient = new DiffServiceClientImpl(address)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { WebviewProvider } from "@core/webview"
|
||||
import { HostBridgeClientProvider } from "./host-provider-types"
|
||||
import { WebviewProviderType } from "@/shared/webview/types"
|
||||
import { DiffViewProvider } from "@/integrations/editor/DiffViewProvider"
|
||||
|
||||
/**
|
||||
* Singleton class that manages host-specific providers for dependency injection.
|
||||
*
|
||||
* This system runs on two different platforms (VSCode extension and cline-core),
|
||||
* so all the host-specific classes and properties are contained in here. The
|
||||
* rest of the codebase can use the host provider interface to access platform-specific
|
||||
* implementations in a platform-agnostic way.
|
||||
*
|
||||
* Usage:
|
||||
* - Initialize once: HostProvider.initialize(webviewCreator, diffCreator, hostBridge)
|
||||
* - Access HostBridge services: HostProvider.window.showMessage()
|
||||
* - Access Host Provider factories: HostProvider.get().createDiffViewProvider()
|
||||
*/
|
||||
export class HostProvider {
|
||||
private static instance: HostProvider | null = null
|
||||
|
||||
createWebviewProvider: WebviewProviderCreator
|
||||
createDiffViewProvider: DiffViewProviderCreator
|
||||
hostBridge: HostBridgeClientProvider
|
||||
|
||||
// Private constructor to enforce singleton pattern
|
||||
private constructor(
|
||||
createWebviewProvider: WebviewProviderCreator,
|
||||
createDiffViewProvider: DiffViewProviderCreator,
|
||||
hostBridge: HostBridgeClientProvider,
|
||||
) {
|
||||
this.createWebviewProvider = createWebviewProvider
|
||||
this.createDiffViewProvider = createDiffViewProvider
|
||||
this.hostBridge = hostBridge
|
||||
}
|
||||
|
||||
public static initialize(
|
||||
webviewProviderCreator: WebviewProviderCreator,
|
||||
diffViewProviderCreator: DiffViewProviderCreator,
|
||||
hostBridgeProvider: HostBridgeClientProvider,
|
||||
): HostProvider {
|
||||
if (HostProvider.instance) {
|
||||
throw new Error("Host providers have already been initialized.")
|
||||
}
|
||||
HostProvider.instance = new HostProvider(webviewProviderCreator, diffViewProviderCreator, hostBridgeProvider)
|
||||
return HostProvider.instance
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the singleton instance
|
||||
*/
|
||||
public static get(): HostProvider {
|
||||
if (!HostProvider.instance) {
|
||||
throw new Error("HostProvider not initialized. Call HostProvider.initialize() first.")
|
||||
}
|
||||
return HostProvider.instance
|
||||
}
|
||||
|
||||
public static isInitialized(): boolean {
|
||||
return !!HostProvider.instance
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the HostProvider instance (primarily for testing)
|
||||
* This allows tests to reinitialize the HostProvider with different configurations
|
||||
*/
|
||||
public static reset(): void {
|
||||
HostProvider.instance = null
|
||||
}
|
||||
|
||||
// Static service accessors for more concise access for callers.
|
||||
public static get watch() {
|
||||
return HostProvider.get().hostBridge.watchServiceClient
|
||||
}
|
||||
|
||||
public static get workspace() {
|
||||
return HostProvider.get().hostBridge.workspaceClient
|
||||
}
|
||||
|
||||
public static get env() {
|
||||
return HostProvider.get().hostBridge.envClient
|
||||
}
|
||||
|
||||
public static get window() {
|
||||
return HostProvider.get().hostBridge.windowClient
|
||||
}
|
||||
|
||||
public static get diff() {
|
||||
return HostProvider.get().hostBridge.diffClient
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A function that creates WebviewProvider instances
|
||||
*/
|
||||
export type WebviewProviderCreator = (providerType: WebviewProviderType) => WebviewProvider
|
||||
|
||||
/**
|
||||
* A function that creates DiffViewProvider instances
|
||||
*/
|
||||
export type DiffViewProviderCreator = () => DiffViewProvider
|
||||
@@ -1,49 +0,0 @@
|
||||
import { WebviewProvider } from "@core/webview"
|
||||
import { HostBridgeClientProvider } from "./host-provider-types"
|
||||
import { WebviewProviderType } from "@/shared/webview/types"
|
||||
import { DiffViewProvider } from "@/integrations/editor/DiffViewProvider"
|
||||
|
||||
/**
|
||||
* A function that creates WebviewProvider instances
|
||||
*/
|
||||
export type WebviewProviderCreator = (providerType: WebviewProviderType) => WebviewProvider
|
||||
|
||||
export type DiffViewProviderCreator = () => DiffViewProvider
|
||||
|
||||
let _webviewProviderCreator: WebviewProviderCreator | undefined
|
||||
let _diffViewProviderCreator: DiffViewProviderCreator | undefined
|
||||
let _hostBridgeProvider: HostBridgeClientProvider | undefined
|
||||
|
||||
export var isSetup: boolean = false
|
||||
|
||||
export function initializeHostProviders(
|
||||
webviewProviderCreator: WebviewProviderCreator,
|
||||
diffViewProviderCreator: DiffViewProviderCreator,
|
||||
hostBridgeProvider: HostBridgeClientProvider,
|
||||
) {
|
||||
_webviewProviderCreator = webviewProviderCreator
|
||||
_diffViewProviderCreator = diffViewProviderCreator
|
||||
_hostBridgeProvider = hostBridgeProvider
|
||||
isSetup = true
|
||||
}
|
||||
|
||||
export function createWebviewProvider(providerType: WebviewProviderType): WebviewProvider {
|
||||
if (!_webviewProviderCreator) {
|
||||
throw Error("Host providers not initialized")
|
||||
}
|
||||
return _webviewProviderCreator(providerType)
|
||||
}
|
||||
|
||||
export function createDiffViewProvider(): DiffViewProvider {
|
||||
if (!_diffViewProviderCreator) {
|
||||
throw Error("Host providers not initialized")
|
||||
}
|
||||
return _diffViewProviderCreator()
|
||||
}
|
||||
|
||||
export function getHostBridgeProvider(): HostBridgeClientProvider {
|
||||
if (!_hostBridgeProvider) {
|
||||
throw Error("Host providers not initialized")
|
||||
}
|
||||
return _hostBridgeProvider
|
||||
}
|
||||
@@ -1,10 +1,19 @@
|
||||
import { arePathsEqual } from "@/utils/path"
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { DecorationController } from "@integrations/editor/DecorationController"
|
||||
import { DIFF_VIEW_URI_SCHEME, DiffViewProvider } from "@integrations/editor/DiffViewProvider"
|
||||
import { DecorationController } from "@/hosts/vscode/DecorationController"
|
||||
import { DiffViewProvider } from "@integrations/editor/DiffViewProvider"
|
||||
import { diagnosticsToProblemsString, getNewDiagnostics } from "@/integrations/diagnostics"
|
||||
|
||||
export const DIFF_VIEW_URI_SCHEME = "cline-diff"
|
||||
|
||||
export class VscodeDiffViewProvider extends DiffViewProvider {
|
||||
private activeDiffEditor?: vscode.TextEditor
|
||||
private preDiagnostics: [vscode.Uri, vscode.Diagnostic[]][] = []
|
||||
|
||||
private fadedOverlayController?: DecorationController
|
||||
private activeLineController?: DecorationController
|
||||
|
||||
override async openDiffEditor(): Promise<void> {
|
||||
if (!this.absolutePath) {
|
||||
throw new Error("No file path set")
|
||||
@@ -81,20 +90,112 @@ export class VscodeDiffViewProvider extends DiffViewProvider {
|
||||
override async replaceText(
|
||||
content: string,
|
||||
rangeToReplace: { startLine: number; endLine: number },
|
||||
currentLine: number,
|
||||
currentLine: number | undefined,
|
||||
): Promise<void> {
|
||||
const document = this.activeDiffEditor?.document
|
||||
if (!document) {
|
||||
if (!this.activeDiffEditor || !this.activeDiffEditor.document) {
|
||||
throw new Error("User closed text editor, unable to edit file...")
|
||||
}
|
||||
// Place cursor at the beginning of the diff editor to keep it out of the way of the stream animation
|
||||
const beginningOfDocument = new vscode.Position(0, 0)
|
||||
this.activeDiffEditor.selection = new vscode.Selection(beginningOfDocument, beginningOfDocument)
|
||||
|
||||
// Replace the text in the diff editor document.
|
||||
const document = this.activeDiffEditor?.document
|
||||
const edit = new vscode.WorkspaceEdit()
|
||||
const range = new vscode.Range(rangeToReplace.startLine, 0, rangeToReplace.endLine, 0)
|
||||
edit.replace(document.uri, range, content)
|
||||
await vscode.workspace.applyEdit(edit)
|
||||
|
||||
// Update decorations for the entire changed section
|
||||
this.activeLineController?.setActiveLine(currentLine)
|
||||
this.fadedOverlayController?.updateOverlayAfterLine(currentLine, document.lineCount)
|
||||
if (currentLine !== undefined) {
|
||||
// Update decorations for the entire changed section
|
||||
this.activeLineController?.setActiveLine(currentLine)
|
||||
this.fadedOverlayController?.updateOverlayAfterLine(currentLine, document.lineCount)
|
||||
}
|
||||
}
|
||||
|
||||
override async scrollEditorToLine(line: number): Promise<void> {
|
||||
if (!this.activeDiffEditor) {
|
||||
return
|
||||
}
|
||||
const scrollLine = line + 4
|
||||
this.activeDiffEditor.revealRange(new vscode.Range(scrollLine, 0, scrollLine, 0), vscode.TextEditorRevealType.InCenter)
|
||||
}
|
||||
|
||||
override async scrollAnimation(startLine: number, endLine: number): Promise<void> {
|
||||
if (!this.activeDiffEditor) {
|
||||
return
|
||||
}
|
||||
const totalLines = endLine - startLine
|
||||
const numSteps = 10 // Adjust this number to control animation speed
|
||||
const stepSize = Math.max(1, Math.floor(totalLines / numSteps))
|
||||
|
||||
// Create and await the smooth scrolling animation
|
||||
for (let line = startLine; line <= endLine; line += stepSize) {
|
||||
this.activeDiffEditor.revealRange(new vscode.Range(line, 0, line, 0), vscode.TextEditorRevealType.InCenter)
|
||||
await new Promise((resolve) => setTimeout(resolve, 16)) // ~60fps
|
||||
}
|
||||
}
|
||||
|
||||
override async truncateDocument(lineNumber: number): Promise<void> {
|
||||
if (!this.activeDiffEditor) {
|
||||
return
|
||||
}
|
||||
const document = this.activeDiffEditor.document
|
||||
if (lineNumber < document.lineCount) {
|
||||
const edit = new vscode.WorkspaceEdit()
|
||||
edit.delete(document.uri, new vscode.Range(lineNumber, 0, document.lineCount, 0))
|
||||
await vscode.workspace.applyEdit(edit)
|
||||
}
|
||||
// Clear all decorations at the end (before applying final edit)
|
||||
this.fadedOverlayController?.clear()
|
||||
this.activeLineController?.clear()
|
||||
}
|
||||
|
||||
protected override async getDocumentText(): Promise<string | undefined> {
|
||||
if (!this.activeDiffEditor || !this.activeDiffEditor.document) {
|
||||
return undefined
|
||||
}
|
||||
return this.activeDiffEditor.document.getText()
|
||||
}
|
||||
|
||||
protected override async getNewDiagnosticProblems(): Promise<string> {
|
||||
// Get the diagnostics after changing the document.
|
||||
const postDiagnostics = vscode.languages.getDiagnostics()
|
||||
const newProblems = getNewDiagnostics(this.preDiagnostics, postDiagnostics)
|
||||
// Only including errors since warnings can be distracting (if user wants to fix warnings they can use the @problems mention)
|
||||
// will be empty string if no errors
|
||||
const problems = await diagnosticsToProblemsString(newProblems, [vscode.DiagnosticSeverity.Error])
|
||||
return problems
|
||||
}
|
||||
|
||||
protected override async saveDocument(): Promise<Boolean> {
|
||||
if (!this.activeDiffEditor) {
|
||||
return false
|
||||
}
|
||||
if (!this.activeDiffEditor.document.isDirty) {
|
||||
return false
|
||||
}
|
||||
await this.activeDiffEditor.document.save()
|
||||
return true
|
||||
}
|
||||
|
||||
protected async closeDiffView(): Promise<void> {
|
||||
// Close all the cline diff views.
|
||||
const tabs = vscode.window.tabGroups.all
|
||||
.flatMap((tg) => tg.tabs)
|
||||
.filter((tab) => tab.input instanceof vscode.TabInputTextDiff && tab.input?.original?.scheme === DIFF_VIEW_URI_SCHEME)
|
||||
for (const tab of tabs) {
|
||||
// trying to close dirty views results in save popup
|
||||
if (!tab.isDirty) {
|
||||
await vscode.window.tabGroups.close(tab)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override async resetDiffView(): Promise<void> {
|
||||
this.activeDiffEditor = undefined
|
||||
this.fadedOverlayController = undefined
|
||||
this.activeLineController = undefined
|
||||
this.preDiagnostics = []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { CloseDiffRequest, CloseDiffResponse } from "@/shared/proto/index.host"
|
||||
|
||||
export async function closeDiff(_request: CloseDiffRequest): Promise<CloseDiffResponse> {
|
||||
throw new Error("diffService is not supported. Use the VscodeDiffViewProvider.")
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { GetDocumentTextRequest, GetDocumentTextResponse } from "@/shared/proto/index.host"
|
||||
|
||||
export async function getDocumentText(_request: GetDocumentTextRequest): Promise<GetDocumentTextResponse> {
|
||||
throw new Error("diffService is not supported. Use the VscodeDiffViewProvider.")
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { OpenDiffRequest, OpenDiffResponse } from "@/shared/proto/index.host"
|
||||
|
||||
export async function openDiff(_request: OpenDiffRequest): Promise<OpenDiffResponse> {
|
||||
throw new Error("diffService.openDiff is not supported. Use the VscodeDiffViewProvider.")
|
||||
throw new Error("diffService is not supported. Use the VscodeDiffViewProvider.")
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ReplaceTextRequest, ReplaceTextResponse } from "@/shared/proto/index.host"
|
||||
|
||||
export async function replaceText(_request: ReplaceTextRequest): Promise<ReplaceTextResponse> {
|
||||
throw new Error("diffService.replaceText is not supported. Use the VscodeDiffViewProvider.")
|
||||
throw new Error("diffService is not supported. Use the VscodeDiffViewProvider.")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { SaveDocumentRequest, SaveDocumentResponse } from "@/shared/proto/index.host"
|
||||
|
||||
export async function saveDocument(_request: SaveDocumentRequest): Promise<SaveDocumentResponse> {
|
||||
throw new Error("diffService is not supported. Use the VscodeDiffViewProvider.")
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { TruncateDocumentRequest, TruncateDocumentResponse } from "@/shared/proto/index.host"
|
||||
|
||||
export async function truncateDocument(_request: TruncateDocumentRequest): Promise<TruncateDocumentResponse> {
|
||||
throw new Error("diffService is not supported. Use the VscodeDiffViewProvider.")
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { SelectedResponse, ShowMessageRequest, ShowMessageType } from "@/shared/
|
||||
|
||||
const DEFAULT_OPTIONS = { modal: false, items: [] } as const
|
||||
|
||||
export async function showMessage(request: ShowMessageRequest): Promise<SelectedResponse | undefined> {
|
||||
export async function showMessage(request: ShowMessageRequest): Promise<SelectedResponse> {
|
||||
const { message, type, options } = request
|
||||
const { modal, detail, items } = { ...DEFAULT_OPTIONS, ...options }
|
||||
const option = { modal, detail }
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import simpleGit from "simple-git"
|
||||
import * as vscode from "vscode"
|
||||
import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
|
||||
import { GitOperations } from "./CheckpointGitOperations"
|
||||
import { getShadowGitPath, getWorkingDirectory, hashWorkingDir } from "./CheckpointUtils"
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
import { expect } from "chai"
|
||||
import proxyquire from "proxyquire"
|
||||
import path from "path"
|
||||
import sinon from "sinon"
|
||||
|
||||
const createMockProcess = () => {
|
||||
const mockProcess = {
|
||||
stdin: {
|
||||
write: sinon.fake(),
|
||||
end: sinon.fake(),
|
||||
},
|
||||
stdout: {
|
||||
on: sinon.fake(),
|
||||
resume: sinon.fake(),
|
||||
},
|
||||
stderr: {
|
||||
on: sinon.fake(() => {}),
|
||||
},
|
||||
on: sinon.fake((event, callback) => {
|
||||
if (event === "close") {
|
||||
setImmediate(() => callback(0))
|
||||
}
|
||||
if (event === "error") {
|
||||
}
|
||||
}),
|
||||
killed: false,
|
||||
kill: sinon.fake(),
|
||||
exitCode: 0,
|
||||
then: (onResolve: (value: any) => void) => {
|
||||
setImmediate(() => onResolve({ exitCode: 0 }))
|
||||
return Promise.resolve({ exitCode: 0 })
|
||||
},
|
||||
catch: () => Promise.resolve({ exitCode: 0 }),
|
||||
finally: (callback: () => void) => {
|
||||
setImmediate(callback)
|
||||
return Promise.resolve({ exitCode: 0 })
|
||||
},
|
||||
}
|
||||
return mockProcess
|
||||
}
|
||||
|
||||
const createMockReadlineInterface = () => {
|
||||
const mockInterface = {
|
||||
async *[Symbol.asyncIterator]() {
|
||||
// Simulate Claude CLI JSON output - yield a few chunks then end
|
||||
yield '{"type":"text","text":"Hello"}'
|
||||
yield '{"type":"text","text":" world"}'
|
||||
// Iterator ends naturally when function returns
|
||||
return
|
||||
},
|
||||
close: sinon.fake(),
|
||||
}
|
||||
return mockInterface
|
||||
}
|
||||
|
||||
const mockExeca = sinon.fake((...args) => {
|
||||
return createMockProcess()
|
||||
})
|
||||
|
||||
let os = "darwin"
|
||||
|
||||
const { MAX_SYSTEM_PROMPT_LENGTH, runClaudeCode } = proxyquire("./run", {
|
||||
"@/utils/path": {
|
||||
getCwd: () => Promise.resolve(path.resolve("./")),
|
||||
},
|
||||
"node:os": {
|
||||
platform: () => os,
|
||||
},
|
||||
execa: {
|
||||
execa: mockExeca,
|
||||
},
|
||||
readline: {
|
||||
createInterface: createMockReadlineInterface,
|
||||
},
|
||||
})
|
||||
|
||||
describe("Claude Code Integration", () => {
|
||||
const scriptPath = "echo"
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
const itCallsTheScriptWithAFile = (systemPrompt: string) => {
|
||||
it("calls the script using with a file", async () => {
|
||||
const cProcess = runClaudeCode({
|
||||
systemPrompt,
|
||||
messages: [],
|
||||
modelId: "test",
|
||||
path: scriptPath,
|
||||
})
|
||||
|
||||
const chunks: string[] = []
|
||||
for await (const chunk of cProcess) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
|
||||
expect(chunks).to.have.length(2)
|
||||
|
||||
const lastExecaCall = mockExeca.lastCall
|
||||
const params = lastExecaCall.args[1]
|
||||
expect(params).to.not.be.null
|
||||
expect(params.includes("--system-prompt-file")).to.be.true
|
||||
expect(params.includes("--system-prompt")).to.be.false
|
||||
})
|
||||
}
|
||||
|
||||
describe("when it's running on Windows", () => {
|
||||
beforeEach(() => {
|
||||
os = "win32"
|
||||
})
|
||||
|
||||
describe("when the system prompt is longer than the MAX_SYSTEM_PROMPT_LENGTH", () => {
|
||||
const SYSTEM_PROMPT = "a".repeat(MAX_SYSTEM_PROMPT_LENGTH * 1.2)
|
||||
|
||||
itCallsTheScriptWithAFile(SYSTEM_PROMPT)
|
||||
})
|
||||
|
||||
describe("when the system prompt is shorter than the MAX_SYSTEM_PROMPT_LENGTH", () => {
|
||||
const SYSTEM_PROMPT = "a".repeat(MAX_SYSTEM_PROMPT_LENGTH / 2)
|
||||
|
||||
itCallsTheScriptWithAFile(SYSTEM_PROMPT)
|
||||
})
|
||||
})
|
||||
|
||||
describe("when it's not running on Windows", () => {
|
||||
beforeEach(() => {
|
||||
os = "darwin"
|
||||
})
|
||||
|
||||
describe("when the system prompt is longer than the MAX_SYSTEM_PROMPT_LENGTH", () => {
|
||||
const SYSTEM_PROMPT = "a".repeat(MAX_SYSTEM_PROMPT_LENGTH * 1.2)
|
||||
|
||||
itCallsTheScriptWithAFile(SYSTEM_PROMPT)
|
||||
})
|
||||
|
||||
describe("when the system prompt is shorter than the MAX_SYSTEM_PROMPT_LENGTH", () => {
|
||||
const SYSTEM_PROMPT = "a".repeat(MAX_SYSTEM_PROMPT_LENGTH / 2)
|
||||
|
||||
it("calls the script without a file", async () => {
|
||||
const cProcess = runClaudeCode({
|
||||
systemPrompt: SYSTEM_PROMPT,
|
||||
messages: [],
|
||||
modelId: "test",
|
||||
path: scriptPath,
|
||||
})
|
||||
|
||||
const chunks: string[] = []
|
||||
for await (const chunk of cProcess) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
|
||||
expect(chunks).to.have.length(2)
|
||||
|
||||
const lastExecaCall = mockExeca.lastCall
|
||||
const params = lastExecaCall.args[1]
|
||||
expect(params).to.not.be.null
|
||||
expect(params.includes("--system-prompt-file")).to.be.false
|
||||
expect(params.includes("--system-prompt")).to.be.true
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,15 +1,20 @@
|
||||
import { getCwd } from "@/utils/path"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
import fs from "node:fs/promises"
|
||||
import type Anthropic from "@anthropic-ai/sdk"
|
||||
import { execa } from "execa"
|
||||
import readline from "readline"
|
||||
import { ClaudeCodeMessage } from "./types"
|
||||
import crypto from "node:crypto"
|
||||
|
||||
type ClaudeCodeOptions = {
|
||||
systemPrompt: string
|
||||
messages: Anthropic.Messages.MessageParam[]
|
||||
path?: string
|
||||
modelId?: string
|
||||
modelId: string
|
||||
thinkingBudgetTokens?: number
|
||||
shouldUseFile?: boolean
|
||||
}
|
||||
|
||||
type ProcessState = {
|
||||
@@ -19,30 +24,46 @@ type ProcessState = {
|
||||
exitCode: number | null
|
||||
}
|
||||
|
||||
// The maximum argument length is longer than this,
|
||||
// but environment variables and other factors can reduce it.
|
||||
// We use a conservative limit to avoid issues while supporting older Claude Code versions that don't support file input.
|
||||
export const MAX_SYSTEM_PROMPT_LENGTH = 65536
|
||||
|
||||
export async function* runClaudeCode(options: ClaudeCodeOptions): AsyncGenerator<ClaudeCodeMessage | string> {
|
||||
const process = runProcess(options, await getCwd())
|
||||
const isSystemPromptTooLong = options.systemPrompt.length > MAX_SYSTEM_PROMPT_LENGTH
|
||||
const uniqueId = crypto.randomUUID()
|
||||
const tempFilePath = path.join(os.tmpdir(), `cline-system-prompt-${uniqueId}.txt`)
|
||||
if (os.platform() === "win32" || isSystemPromptTooLong) {
|
||||
// Use a temporary file to prevent ENAMETOOLONG and E2BIG errors
|
||||
// https://github.com/anthropics/claude-code/issues/3411#issuecomment-3082068547
|
||||
await fs.writeFile(tempFilePath, options.systemPrompt, "utf8")
|
||||
options.systemPrompt = tempFilePath
|
||||
options.shouldUseFile = true
|
||||
}
|
||||
|
||||
const cProcess = runProcess(options, await getCwd())
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdout,
|
||||
input: cProcess.stdout,
|
||||
})
|
||||
|
||||
try {
|
||||
const processState: ProcessState = {
|
||||
error: null,
|
||||
stderrLogs: "",
|
||||
exitCode: null,
|
||||
partialData: null,
|
||||
}
|
||||
const processState: ProcessState = {
|
||||
error: null,
|
||||
stderrLogs: "",
|
||||
exitCode: null,
|
||||
partialData: null,
|
||||
}
|
||||
|
||||
process.stderr.on("data", (data) => {
|
||||
try {
|
||||
cProcess.stderr.on("data", (data) => {
|
||||
processState.stderrLogs += data.toString()
|
||||
})
|
||||
|
||||
process.on("close", (code) => {
|
||||
cProcess.on("close", (code) => {
|
||||
processState.exitCode = code
|
||||
})
|
||||
|
||||
process.on("error", (err) => {
|
||||
cProcess.on("error", (err) => {
|
||||
processState.error = err
|
||||
})
|
||||
|
||||
@@ -68,7 +89,7 @@ export async function* runClaudeCode(options: ClaudeCodeOptions): AsyncGenerator
|
||||
yield processState.partialData
|
||||
}
|
||||
|
||||
const { exitCode } = await process
|
||||
const { exitCode } = await cProcess
|
||||
if (exitCode !== null && exitCode !== 0) {
|
||||
const errorOutput = processState.error?.message || processState.stderrLogs?.trim()
|
||||
throw new Error(
|
||||
@@ -78,6 +99,12 @@ export async function* runClaudeCode(options: ClaudeCodeOptions): AsyncGenerator
|
||||
} catch (err) {
|
||||
console.error(`Error during Claude Code execution:`, err)
|
||||
|
||||
if (processState.stderrLogs.includes("unknown option '--system-prompt-file'")) {
|
||||
throw new Error(`The Claude Code executable is outdated. Please update it to the latest version.`, {
|
||||
cause: err,
|
||||
})
|
||||
}
|
||||
|
||||
if (err instanceof Error) {
|
||||
if (err.message.includes("ENOENT")) {
|
||||
throw new Error(
|
||||
@@ -113,15 +140,19 @@ Anthropic is aware of this issue and is considering a fix: https://github.com/an
|
||||
if (startOfCommand !== -1) {
|
||||
const messageWithoutCommand = err.message.slice(0, startOfCommand).trim()
|
||||
|
||||
throw new Error(messageWithoutCommand, { cause: err })
|
||||
throw new Error(`${messageWithoutCommand}\n${processState.stderrLogs?.trim()}`, { cause: err })
|
||||
}
|
||||
}
|
||||
|
||||
throw err
|
||||
} finally {
|
||||
rl.close()
|
||||
if (!process.killed) {
|
||||
process.kill()
|
||||
if (!cProcess.killed) {
|
||||
cProcess.kill()
|
||||
}
|
||||
|
||||
if (options.shouldUseFile) {
|
||||
fs.unlink(tempFilePath).catch(console.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -151,12 +182,14 @@ const CLAUDE_CODE_TIMEOUT = 600000 // 10 minutes
|
||||
// https://github.com/sindresorhus/execa/blob/main/docs/api.md#optionsmaxbuffer
|
||||
const BUFFER_SIZE = 20_000_000 // 20 MB
|
||||
|
||||
function runProcess({ systemPrompt, messages, path, modelId, thinkingBudgetTokens }: ClaudeCodeOptions, cwd: string) {
|
||||
function runProcess(
|
||||
{ systemPrompt, messages, path, modelId, thinkingBudgetTokens, shouldUseFile }: ClaudeCodeOptions,
|
||||
cwd: string,
|
||||
) {
|
||||
const claudePath = path?.trim() || "claude"
|
||||
|
||||
const args = [
|
||||
"-p",
|
||||
"--system-prompt",
|
||||
shouldUseFile ? "--system-prompt-file" : "--system-prompt",
|
||||
systemPrompt,
|
||||
"--verbose",
|
||||
"--output-format",
|
||||
@@ -166,12 +199,11 @@ function runProcess({ systemPrompt, messages, path, modelId, thinkingBudgetToken
|
||||
// Cline will handle recursive calls
|
||||
"--max-turns",
|
||||
"1",
|
||||
"--model",
|
||||
modelId,
|
||||
"-p",
|
||||
]
|
||||
|
||||
if (modelId) {
|
||||
args.push("--model", modelId)
|
||||
}
|
||||
|
||||
/**
|
||||
* @see {@link https://docs.anthropic.com/en/docs/claude-code/settings#environment-variables}
|
||||
*/
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import * as vscode from "vscode"
|
||||
|
||||
interface DebugSession {
|
||||
id: string
|
||||
name: string
|
||||
output: string[]
|
||||
lastRetrievedIndex: number
|
||||
}
|
||||
|
||||
export class DebugConsoleManager {
|
||||
private sessions: Map<string, DebugSession> = new Map()
|
||||
private disposables: vscode.Disposable[] = []
|
||||
|
||||
constructor() {
|
||||
// Listen for debug session start events
|
||||
this.disposables.push(
|
||||
vscode.debug.onDidStartDebugSession((session) => {
|
||||
this.sessions.set(session.id, {
|
||||
id: session.id,
|
||||
name: session.name,
|
||||
output: [],
|
||||
lastRetrievedIndex: -1,
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
// Listen for debug session end events
|
||||
this.disposables.push(
|
||||
vscode.debug.onDidTerminateDebugSession((session) => {
|
||||
this.sessions.delete(session.id)
|
||||
}),
|
||||
)
|
||||
|
||||
// Listen for debug console output
|
||||
this.disposables.push(
|
||||
vscode.debug.onDidReceiveDebugSessionCustomEvent((e: vscode.DebugSessionCustomEvent) => {
|
||||
if (e.event === "output" && e.body?.output) {
|
||||
const session = this.sessions.get(e.session.id)
|
||||
if (session) {
|
||||
session.output.push(e.body.output)
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active debug sessions
|
||||
*/
|
||||
getActiveSessions(): { id: string; name: string }[] {
|
||||
return Array.from(this.sessions.values()).map(({ id, name }) => ({ id, name }))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get any new output since the last retrieval for a specific debug session
|
||||
*/
|
||||
getUnretrievedOutput(sessionId: string): string | undefined {
|
||||
const session = this.sessions.get(sessionId)
|
||||
if (!session) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const newOutput = session.output.slice(session.lastRetrievedIndex + 1).join("")
|
||||
session.lastRetrievedIndex = session.output.length - 1
|
||||
return newOutput || undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up resources
|
||||
*/
|
||||
dispose() {
|
||||
this.disposables.forEach((d) => d.dispose())
|
||||
this.sessions.clear()
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
/*
|
||||
import * as vscode from "vscode"
|
||||
import deepEqual from "fast-deep-equal"
|
||||
|
||||
type FileDiagnostics = [vscode.Uri, vscode.Diagnostic[]][]
|
||||
|
||||
|
||||
About Diagnostics:
|
||||
The Problems tab shows diagnostics that have been reported for your project. These diagnostics are categorized into:
|
||||
Errors: Critical issues that usually prevent your code from compiling or running correctly.
|
||||
Warnings: Potential problems in the code that may not prevent it from running but could cause issues (e.g., bad practices, unused variables).
|
||||
Information: Non-critical suggestions or tips (e.g., formatting issues or notes from linters).
|
||||
The Problems tab displays diagnostics from various sources:
|
||||
1. Language Servers:
|
||||
- TypeScript: Type errors, missing imports, syntax issues
|
||||
- Python: Syntax errors, invalid type hints, undefined variables
|
||||
- JavaScript/Node.js: Parsing and execution errors
|
||||
2. Linters:
|
||||
- ESLint: Code style, best practices, potential bugs
|
||||
- Pylint: Unused imports, naming conventions
|
||||
- TSLint: Style and correctness issues in TypeScript
|
||||
3. Build Tools:
|
||||
- Webpack: Module resolution failures, build errors
|
||||
- Gulp: Build errors during task execution
|
||||
4. Custom Validators:
|
||||
- Extensions can generate custom diagnostics for specific languages or tools
|
||||
Each problem typically indicates its source (e.g., language server, linter, build tool).
|
||||
Diagnostics update in real-time as you edit code, helping identify issues quickly. For example, if you introduce a syntax error in a TypeScript file, the Problems tab will immediately display the new error.
|
||||
|
||||
Notes on diagnostics:
|
||||
- linter diagnostics are only captured for open editors
|
||||
- this works great for us since when cline edits/creates files its through vscode's textedit api's and we get those diagnostics for free
|
||||
- some tools might require you to save the file or manually refresh to clear the problem from the list.
|
||||
|
||||
System Prompt
|
||||
- You will automatically receive workspace error diagnostics in environment_details. Be mindful that this may include issues beyond the scope of your task or the user's request. Only address errors relevant to your work, and avoid fixing pre-existing or unrelated issues unless the user specifically instructs you to do so.
|
||||
- If you are unable to resolve errors provided in environment_details after two attempts, consider using ask_followup_question to ask the user for additional information, such as the latest documentation related to a problematic framework, to help you make progress on the task. If the error remains unresolved after this step, proceed with your task while disregarding the error.
|
||||
|
||||
class DiagnosticsMonitor {
|
||||
private diagnosticsChangeEmitter: vscode.EventEmitter<void> = new vscode.EventEmitter<void>()
|
||||
private disposables: vscode.Disposable[] = []
|
||||
private lastDiagnostics: FileDiagnostics = []
|
||||
|
||||
constructor() {
|
||||
this.disposables.push(
|
||||
vscode.languages.onDidChangeDiagnostics(() => {
|
||||
this.diagnosticsChangeEmitter.fire()
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
public async getCurrentDiagnostics(shouldWaitForChanges: boolean): Promise<FileDiagnostics> {
|
||||
const currentDiagnostics = this.getDiagnostics()
|
||||
if (!shouldWaitForChanges) {
|
||||
this.lastDiagnostics = currentDiagnostics
|
||||
return currentDiagnostics
|
||||
}
|
||||
|
||||
if (!deepEqual(this.lastDiagnostics, currentDiagnostics)) {
|
||||
this.lastDiagnostics = currentDiagnostics
|
||||
return currentDiagnostics
|
||||
}
|
||||
|
||||
let timeout = 300 // only way this happens is if there's no errors
|
||||
|
||||
// if diagnostics contain existing errors (since the check above didn't trigger) then it's likely cline just did something that should have fixed the error, so we'll give a longer grace period for diagnostics to catch up
|
||||
const hasErrors = currentDiagnostics.some(([_, diagnostics]) =>
|
||||
diagnostics.some((d) => d.severity === vscode.DiagnosticSeverity.Error)
|
||||
)
|
||||
if (hasErrors) {
|
||||
console.log("Existing errors detected, extending timeout", currentDiagnostics)
|
||||
timeout = 10_000
|
||||
}
|
||||
|
||||
return this.waitForUpdatedDiagnostics(timeout)
|
||||
}
|
||||
|
||||
private async waitForUpdatedDiagnostics(timeout: number): Promise<FileDiagnostics> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
cleanup()
|
||||
const finalDiagnostics = this.getDiagnostics()
|
||||
this.lastDiagnostics = finalDiagnostics
|
||||
resolve(finalDiagnostics)
|
||||
}, timeout)
|
||||
|
||||
const disposable = this.diagnosticsChangeEmitter.event(() => {
|
||||
const updatedDiagnostics = this.getDiagnostics() // I thought this would only trigger when diagnostics changed, but that's not the case.
|
||||
if (deepEqual(this.lastDiagnostics, updatedDiagnostics)) {
|
||||
// diagnostics have not changed, ignoring...
|
||||
return
|
||||
}
|
||||
cleanup()
|
||||
this.lastDiagnostics = updatedDiagnostics
|
||||
resolve(updatedDiagnostics)
|
||||
})
|
||||
|
||||
const cleanup = () => {
|
||||
clearTimeout(timer)
|
||||
disposable.dispose()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private getDiagnostics(): FileDiagnostics {
|
||||
const allDiagnostics = vscode.languages.getDiagnostics()
|
||||
return allDiagnostics
|
||||
.filter(([_, diagnostics]) => diagnostics.some((d) => d.severity === vscode.DiagnosticSeverity.Error))
|
||||
.map(([uri, diagnostics]) => [
|
||||
uri,
|
||||
diagnostics.filter((d) => d.severity === vscode.DiagnosticSeverity.Error),
|
||||
])
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.disposables.forEach((d) => d.dispose())
|
||||
this.disposables = []
|
||||
this.diagnosticsChangeEmitter.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
export default DiagnosticsMonitor
|
||||
*/
|
||||
@@ -4,15 +4,10 @@ import * as fs from "fs/promises"
|
||||
import { createDirectoriesForFile } from "@utils/fs"
|
||||
import { arePathsEqual, getCwd } from "@utils/path"
|
||||
import { formatResponse } from "@core/prompts/responses"
|
||||
import { DecorationController } from "./DecorationController"
|
||||
import * as diff from "diff"
|
||||
import { diagnosticsToProblemsString, getNewDiagnostics } from "../diagnostics"
|
||||
import { detectEncoding } from "../misc/extract-text"
|
||||
import * as iconv from "iconv-lite"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { ShowTextDocumentRequest, ShowTextDocumentOptions } from "@/shared/proto/host/window"
|
||||
|
||||
export const DIFF_VIEW_URI_SCHEME = "cline-diff"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
|
||||
export abstract class DiffViewProvider {
|
||||
editType?: "create" | "modify"
|
||||
@@ -26,11 +21,6 @@ export abstract class DiffViewProvider {
|
||||
private streamedLines: string[] = []
|
||||
private newContent?: string
|
||||
|
||||
protected activeDiffEditor?: vscode.TextEditor
|
||||
protected fadedOverlayController?: DecorationController
|
||||
protected activeLineController?: DecorationController
|
||||
protected preDiagnostics: [vscode.Uri, vscode.Diagnostic[]][] = []
|
||||
|
||||
constructor() {}
|
||||
|
||||
public async open(relPath: string): Promise<void> {
|
||||
@@ -62,17 +52,13 @@ export abstract class DiffViewProvider {
|
||||
await fs.writeFile(this.absolutePath, "")
|
||||
}
|
||||
await this.openDiffEditor()
|
||||
this.scrollEditorToLine(0) // will this crash for new files?
|
||||
await this.scrollEditorToLine(0)
|
||||
this.streamedLines = []
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a diff editor or viewer for the current file.
|
||||
*
|
||||
* This abstract method must be implemented by subclasses to create and display
|
||||
* a diff editor or viewer that shows the difference between the original and
|
||||
* modified content.
|
||||
*
|
||||
* Called automatically by the `open` method after ensuring the file exists and
|
||||
* creating any necessary directories.
|
||||
*
|
||||
@@ -80,13 +66,84 @@ export abstract class DiffViewProvider {
|
||||
*/
|
||||
protected abstract openDiffEditor(): Promise<void>
|
||||
|
||||
/**
|
||||
* Scrolls the diff editor to reveal a specific line.
|
||||
*
|
||||
* It's used during streaming updates to keep the user's view focused on the changing content.
|
||||
*
|
||||
* @param line The 0-based line number to scroll to
|
||||
*/
|
||||
protected abstract scrollEditorToLine(line: number): Promise<void>
|
||||
|
||||
/**
|
||||
* Creates a smooth scrolling animation between two lines in the diff editor.
|
||||
*
|
||||
* It's typically used when updates contain many lines, to help the user visually track the flow
|
||||
* of significant changes in the document.
|
||||
*
|
||||
* @param startLine The 0-based line number to begin the animation from
|
||||
* @param endLine The 0-based line number to animate to
|
||||
*/
|
||||
protected abstract scrollAnimation(startLine: number, endLine: number): Promise<void>
|
||||
|
||||
/**
|
||||
* Removes content from the specified line to the end of the document.
|
||||
* Called after the final update is received.
|
||||
*/
|
||||
protected abstract truncateDocument(lineNumber: number): Promise<void>
|
||||
|
||||
/**
|
||||
* Get the contents of the diff editor document.
|
||||
*
|
||||
* Returns undefined if the diff editor was closed.
|
||||
*/
|
||||
protected abstract getDocumentText(): Promise<string | undefined>
|
||||
|
||||
/**
|
||||
* Get any new diagnostic problems that appeared after applying the diff.
|
||||
*
|
||||
* Getting diagnostics before and after the file edit is a better approach than
|
||||
* automatically tracking problems in real-time. This method ensures we only
|
||||
* report new problems that are a direct result of this specific edit.
|
||||
* Since these are new problems resulting from Cline's edit, we know they're
|
||||
* directly related to the work he's doing. This eliminates the risk of Cline
|
||||
* going off-task or getting distracted by unrelated issues, which was a problem
|
||||
* with the previous auto-debug approach. Some users' machines may be slow to
|
||||
* update diagnostics, so this approach provides a good balance between automation
|
||||
* and avoiding potential issues where Cline might get stuck in loops due to
|
||||
* outdated problem information. If no new problems show up by the time the user
|
||||
* accepts the changes, they can always debug later using the '@problems' mention.
|
||||
* This way, Cline only becomes aware of new problems resulting from his edits
|
||||
* and can address them accordingly. If problems don't change immediately after
|
||||
* applying a fix, Cline won't be notified, which is generally fine since the
|
||||
* initial fix is usually correct and it may just take time for linters to catch up.
|
||||
*/
|
||||
protected abstract getNewDiagnosticProblems(): Promise<string>
|
||||
|
||||
/**
|
||||
* Save the contents of the diff editor UI to the file.
|
||||
*
|
||||
* @returns true if the file was saved.
|
||||
*/
|
||||
protected abstract saveDocument(): Promise<Boolean>
|
||||
|
||||
/**
|
||||
* Closes the diff editor tab or window.
|
||||
*/
|
||||
protected abstract closeDiffView(): Promise<void>
|
||||
|
||||
/**
|
||||
* Cleans up the diff view resources and resets internal state.
|
||||
*/
|
||||
protected abstract resetDiffView(): Promise<void>
|
||||
|
||||
async update(
|
||||
accumulatedContent: string,
|
||||
isFinal: boolean,
|
||||
changeLocation?: { startLine: number; endLine: number; startChar: number; endChar: number },
|
||||
) {
|
||||
if (!this.relPath || !this.activeLineController || !this.fadedOverlayController) {
|
||||
throw new Error("Required values not set")
|
||||
if (!this.isEditing) {
|
||||
throw new Error("Not editing any file")
|
||||
}
|
||||
|
||||
// --- Fix to prevent duplicate BOM ---
|
||||
@@ -104,16 +161,6 @@ export abstract class DiffViewProvider {
|
||||
}
|
||||
const diffLines = accumulatedLines.slice(this.streamedLines.length)
|
||||
|
||||
const diffEditor = this.activeDiffEditor
|
||||
const document = diffEditor?.document
|
||||
if (!diffEditor || !document) {
|
||||
throw new Error("User closed text editor, unable to edit file...")
|
||||
}
|
||||
|
||||
// Place cursor at the beginning of the diff editor to keep it out of the way of the stream animation
|
||||
const beginningOfDocument = new vscode.Position(0, 0)
|
||||
diffEditor.selection = new vscode.Selection(beginningOfDocument, beginningOfDocument)
|
||||
|
||||
// Instead of animating each line, we'll update in larger chunks
|
||||
const currentLine = this.streamedLines.length + diffLines.length - 1
|
||||
if (currentLine >= 0) {
|
||||
@@ -129,30 +176,19 @@ export abstract class DiffViewProvider {
|
||||
if (changeLocation) {
|
||||
// We have the actual location of the change, scroll to it
|
||||
const targetLine = changeLocation.startLine
|
||||
this.scrollEditorToLine(targetLine)
|
||||
await this.scrollEditorToLine(targetLine)
|
||||
} else {
|
||||
// Fallback to the old logic for non-replacement updates
|
||||
if (diffLines.length <= 5) {
|
||||
// For small changes, just jump directly to the line
|
||||
this.scrollEditorToLine(currentLine)
|
||||
await this.scrollEditorToLine(currentLine)
|
||||
} else {
|
||||
// For larger changes, create a quick scrolling animation
|
||||
const startLine = this.streamedLines.length
|
||||
const endLine = currentLine
|
||||
const totalLines = endLine - startLine
|
||||
const numSteps = 10 // Adjust this number to control animation speed
|
||||
const stepSize = Math.max(1, Math.floor(totalLines / numSteps))
|
||||
|
||||
// Create and await the smooth scrolling animation
|
||||
for (let line = startLine; line <= endLine; line += stepSize) {
|
||||
this.activeDiffEditor?.revealRange(
|
||||
new vscode.Range(line, 0, line, 0),
|
||||
vscode.TextEditorRevealType.InCenter,
|
||||
)
|
||||
await new Promise((resolve) => setTimeout(resolve, 16)) // ~60fps
|
||||
}
|
||||
await this.scrollAnimation(startLine, endLine)
|
||||
// Ensure we end at the final line
|
||||
this.scrollEditorToLine(currentLine)
|
||||
await this.scrollEditorToLine(currentLine)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,11 +197,8 @@ export abstract class DiffViewProvider {
|
||||
this.streamedLines = accumulatedLines
|
||||
if (isFinal) {
|
||||
// Handle any remaining lines if the new content is shorter than the original
|
||||
if (this.streamedLines.length < document.lineCount) {
|
||||
const edit = new vscode.WorkspaceEdit()
|
||||
edit.delete(document.uri, new vscode.Range(this.streamedLines.length, 0, document.lineCount, 0))
|
||||
await vscode.workspace.applyEdit(edit)
|
||||
}
|
||||
await this.truncateDocument(this.streamedLines.length)
|
||||
|
||||
// Add empty last line if original content had one
|
||||
const hasEmptyLastLine = this.originalContent?.endsWith("\n")
|
||||
if (hasEmptyLastLine) {
|
||||
@@ -174,9 +207,6 @@ export abstract class DiffViewProvider {
|
||||
accumulatedContent += "\n"
|
||||
}
|
||||
}
|
||||
// Clear all decorations at the end (before applying final edit)
|
||||
this.fadedOverlayController.clear()
|
||||
this.activeLineController.clear()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,7 +225,7 @@ export abstract class DiffViewProvider {
|
||||
abstract replaceText(
|
||||
content: string,
|
||||
rangeToReplace: { startLine: number; endLine: number },
|
||||
currentLine: number,
|
||||
currentLine: number | undefined,
|
||||
): Promise<void>
|
||||
|
||||
async saveChanges(): Promise<{
|
||||
@@ -204,7 +234,10 @@ export abstract class DiffViewProvider {
|
||||
autoFormattingEdits: string | undefined
|
||||
finalContent: string | undefined
|
||||
}> {
|
||||
if (!this.relPath || !this.newContent || !this.activeDiffEditor) {
|
||||
// get the contents before save operation which may do auto-formatting
|
||||
const preSaveContent = await this.getDocumentText()
|
||||
|
||||
if (!this.relPath || !this.absolutePath || !this.newContent || preSaveContent === undefined) {
|
||||
return {
|
||||
newProblemsMessage: undefined,
|
||||
userEdits: undefined,
|
||||
@@ -212,50 +245,21 @@ export abstract class DiffViewProvider {
|
||||
finalContent: undefined,
|
||||
}
|
||||
}
|
||||
const updatedDocument = this.activeDiffEditor.document
|
||||
|
||||
// get the contents before save operation which may do auto-formatting
|
||||
const preSaveContent = updatedDocument.getText()
|
||||
|
||||
if (updatedDocument.isDirty) {
|
||||
await updatedDocument.save()
|
||||
}
|
||||
|
||||
await this.saveDocument()
|
||||
// get text after save in case there is any auto-formatting done by the editor
|
||||
const postSaveContent = updatedDocument.getText()
|
||||
const postSaveContent = (await this.getDocumentText()) || ""
|
||||
|
||||
await getHostBridgeProvider().windowClient.showTextDocument(
|
||||
ShowTextDocumentRequest.create({
|
||||
path: this.absolutePath,
|
||||
options: ShowTextDocumentOptions.create({
|
||||
preview: false,
|
||||
preserveFocus: true,
|
||||
}),
|
||||
}),
|
||||
)
|
||||
await this.closeAllDiffViews()
|
||||
await HostProvider.window.showTextDocument({
|
||||
path: this.absolutePath,
|
||||
options: {
|
||||
preview: false,
|
||||
preserveFocus: true,
|
||||
},
|
||||
})
|
||||
await this.closeDiffView()
|
||||
|
||||
/*
|
||||
Getting diagnostics before and after the file edit is a better approach than
|
||||
automatically tracking problems in real-time. This method ensures we only
|
||||
report new problems that are a direct result of this specific edit.
|
||||
Since these are new problems resulting from Cline's edit, we know they're
|
||||
directly related to the work he's doing. This eliminates the risk of Cline
|
||||
going off-task or getting distracted by unrelated issues, which was a problem
|
||||
with the previous auto-debug approach. Some users' machines may be slow to
|
||||
update diagnostics, so this approach provides a good balance between automation
|
||||
and avoiding potential issues where Cline might get stuck in loops due to
|
||||
outdated problem information. If no new problems show up by the time the user
|
||||
accepts the changes, they can always debug later using the '@problems' mention.
|
||||
This way, Cline only becomes aware of new problems resulting from his edits
|
||||
and can address them accordingly. If problems don't change immediately after
|
||||
applying a fix, Cline won't be notified, which is generally fine since the
|
||||
initial fix is usually correct and it may just take time for linters to catch up.
|
||||
*/
|
||||
const postDiagnostics = vscode.languages.getDiagnostics()
|
||||
const newProblems = await diagnosticsToProblemsString(getNewDiagnostics(this.preDiagnostics, postDiagnostics), [
|
||||
vscode.DiagnosticSeverity.Error, // only including errors since warnings can be distracting (if user wants to fix warnings they can use the @problems mention)
|
||||
]) // will be empty string if no errors
|
||||
const newProblems = await this.getNewDiagnosticProblems()
|
||||
const newProblemsMessage =
|
||||
newProblems.length > 0 ? `\n\nNew problems detected after saving the file:\n${newProblems}` : ""
|
||||
|
||||
@@ -295,17 +299,17 @@ export abstract class DiffViewProvider {
|
||||
}
|
||||
|
||||
async revertChanges(): Promise<void> {
|
||||
if (!this.absolutePath || !this.activeDiffEditor) {
|
||||
if (!this.absolutePath) {
|
||||
return
|
||||
}
|
||||
const fileExists = this.editType === "modify"
|
||||
const updatedDocument = this.activeDiffEditor.document
|
||||
|
||||
if (!fileExists) {
|
||||
if (updatedDocument.isDirty) {
|
||||
await updatedDocument.save()
|
||||
}
|
||||
await this.closeAllDiffViews()
|
||||
await fs.unlink(this.absolutePath)
|
||||
// This is a load-bearing save statement- even though the file is saved and then immediately deleted.
|
||||
// In vscode, it will not close the diff editor correctly if the file is not saved.
|
||||
await this.saveDocument()
|
||||
await this.closeDiffView()
|
||||
await fs.rm(this.absolutePath, { force: true })
|
||||
// Remove only the directories we created, in reverse order
|
||||
for (let i = this.createdDirs.length - 1; i >= 0; i--) {
|
||||
await fs.rmdir(this.createdDirs[i])
|
||||
@@ -314,70 +318,41 @@ export abstract class DiffViewProvider {
|
||||
console.log(`File ${this.absolutePath} has been deleted.`)
|
||||
} else {
|
||||
// revert document
|
||||
const edit = new vscode.WorkspaceEdit()
|
||||
const fullRange = new vscode.Range(
|
||||
updatedDocument.positionAt(0),
|
||||
updatedDocument.positionAt(updatedDocument.getText().length),
|
||||
)
|
||||
edit.replace(updatedDocument.uri, fullRange, this.originalContent ?? "")
|
||||
// Apply the edit and save, since contents shouldn't have changed this won't show in local history unless of course the user made changes and saved during the edit
|
||||
await vscode.workspace.applyEdit(edit)
|
||||
await updatedDocument.save()
|
||||
// Apply the edit and save, since contents shouldn't have changed this won't show in local history unless of
|
||||
// course the user made changes and saved during the edit.
|
||||
const contents = (await this.getDocumentText()) || ""
|
||||
const lineCount = (contents.match(/\n/g) || []).length + 1
|
||||
await this.replaceText(this.originalContent ?? "", { startLine: 0, endLine: lineCount }, undefined)
|
||||
|
||||
await this.saveDocument()
|
||||
console.log(`File ${this.absolutePath} has been reverted to its original content.`)
|
||||
if (this.documentWasOpen) {
|
||||
await getHostBridgeProvider().windowClient.showTextDocument(
|
||||
ShowTextDocumentRequest.create({
|
||||
path: this.absolutePath,
|
||||
options: ShowTextDocumentOptions.create({
|
||||
preview: false,
|
||||
preserveFocus: true,
|
||||
}),
|
||||
}),
|
||||
)
|
||||
await HostProvider.window.showTextDocument({
|
||||
path: this.absolutePath,
|
||||
options: {
|
||||
preview: false,
|
||||
preserveFocus: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
await this.closeAllDiffViews()
|
||||
await this.closeDiffView()
|
||||
}
|
||||
|
||||
// edit is done
|
||||
await this.reset()
|
||||
}
|
||||
|
||||
private async closeAllDiffViews() {
|
||||
const tabs = vscode.window.tabGroups.all
|
||||
.flatMap((tg) => tg.tabs)
|
||||
.filter((tab) => tab.input instanceof vscode.TabInputTextDiff && tab.input?.original?.scheme === DIFF_VIEW_URI_SCHEME)
|
||||
for (const tab of tabs) {
|
||||
// trying to close dirty views results in save popup
|
||||
if (!tab.isDirty) {
|
||||
await vscode.window.tabGroups.close(tab)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private scrollEditorToLine(line: number) {
|
||||
if (this.activeDiffEditor) {
|
||||
const scrollLine = line + 4
|
||||
this.activeDiffEditor.revealRange(
|
||||
new vscode.Range(scrollLine, 0, scrollLine, 0),
|
||||
vscode.TextEditorRevealType.InCenter,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
scrollToFirstDiff() {
|
||||
if (!this.activeDiffEditor) {
|
||||
async scrollToFirstDiff() {
|
||||
if (!this.isEditing) {
|
||||
return
|
||||
}
|
||||
const currentContent = this.activeDiffEditor.document.getText()
|
||||
const currentContent = (await this.getDocumentText()) || ""
|
||||
const diffs = diff.diffLines(this.originalContent || "", currentContent)
|
||||
let lineCount = 0
|
||||
for (const part of diffs) {
|
||||
if (part.added || part.removed) {
|
||||
// Found the first diff, scroll to it
|
||||
this.activeDiffEditor.revealRange(
|
||||
new vscode.Range(lineCount, 0, lineCount, 0),
|
||||
vscode.TextEditorRevealType.InCenter,
|
||||
)
|
||||
this.scrollEditorToLine(lineCount)
|
||||
return
|
||||
}
|
||||
if (!part.removed) {
|
||||
@@ -393,10 +368,8 @@ export abstract class DiffViewProvider {
|
||||
this.originalContent = undefined
|
||||
this.createdDirs = []
|
||||
this.documentWasOpen = false
|
||||
this.activeDiffEditor = undefined
|
||||
this.fadedOverlayController = undefined
|
||||
this.activeLineController = undefined
|
||||
this.streamedLines = []
|
||||
this.preDiagnostics = []
|
||||
|
||||
await this.resetDiffView()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,116 @@
|
||||
import * as vscode from "vscode"
|
||||
import { writeTextToClipboard } from "@utils/env"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { ShowMessageType, ShowTextDocumentRequest, ShowMessageRequest } from "@/shared/proto/host/window"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { ShowMessageType, ShowTextDocumentRequest } from "@/shared/proto/host/window"
|
||||
import { buildApiHandler } from "@/api"
|
||||
import { getAllExtensionState } from "@/core/storage/state"
|
||||
import { getWorkingState } from "@/utils/git"
|
||||
import { getCwd } from "@/utils/path"
|
||||
|
||||
/**
|
||||
* Git commit message generator module
|
||||
*/
|
||||
export const GitCommitGenerator = {
|
||||
generate,
|
||||
abort,
|
||||
}
|
||||
|
||||
let commitGenerationAbortController: AbortController | undefined = undefined
|
||||
|
||||
async function generate(context: vscode.ExtensionContext, scm?: vscode.SourceControl) {
|
||||
const cwd = await getCwd()
|
||||
if (!context || !cwd) {
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "No workspace folder open",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const gitDiff = await getWorkingState(cwd)
|
||||
if (gitDiff === "No changes in working directory") {
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "No changes in workspace for commit message",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const inputBox = scm?.inputBox
|
||||
if (!inputBox) {
|
||||
vscode.window.showErrorMessage("Git extension not found or no repositories available")
|
||||
return
|
||||
}
|
||||
|
||||
await vscode.window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.SourceControl,
|
||||
title: "Generating commit message...",
|
||||
cancellable: true,
|
||||
},
|
||||
() => performCommitGeneration(context, gitDiff, inputBox),
|
||||
)
|
||||
}
|
||||
|
||||
async function performCommitGeneration(context: vscode.ExtensionContext, gitDiff: string, inputBox: any) {
|
||||
try {
|
||||
vscode.commands.executeCommand("setContext", "cline.isGeneratingCommit", true)
|
||||
|
||||
const truncatedDiff = gitDiff.length > 5000 ? gitDiff.substring(0, 5000) + "\n\n[Diff truncated due to size]" : gitDiff
|
||||
|
||||
const prompt = `Based on the following git diff, generate a concise and descriptive commit message:
|
||||
${truncatedDiff}
|
||||
The commit message should:
|
||||
1. Start with a short summary (50-72 characters)
|
||||
2. Use the imperative mood (e.g., "Add feature" not "Added feature")
|
||||
3. Describe what was changed and why
|
||||
4. Be clear and descriptive
|
||||
Commit message:`
|
||||
|
||||
// Get the current API configuration
|
||||
const { apiConfiguration } = await getAllExtensionState(context)
|
||||
// Set to use Act mode for now by default
|
||||
// TODO: A new mode for commit generation
|
||||
const currentMode = "act"
|
||||
|
||||
// Build the API handler
|
||||
const apiHandler = buildApiHandler(apiConfiguration, currentMode)
|
||||
|
||||
// Create a system prompt
|
||||
const systemPrompt =
|
||||
"You are a helpful assistant that generates concise and descriptive git commit messages based on git diffs."
|
||||
|
||||
// Create a message for the API
|
||||
const messages = [{ role: "user" as const, content: prompt }]
|
||||
|
||||
commitGenerationAbortController = new AbortController()
|
||||
const stream = apiHandler.createMessage(systemPrompt, messages)
|
||||
|
||||
let response = ""
|
||||
for await (const chunk of stream) {
|
||||
commitGenerationAbortController.signal.throwIfAborted()
|
||||
if (chunk.type === "text") {
|
||||
response += chunk.text
|
||||
inputBox.value = extractCommitMessage(response)
|
||||
}
|
||||
}
|
||||
|
||||
if (!inputBox.value) {
|
||||
throw new Error("empty API response")
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
vscode.window.showErrorMessage(`Failed to generate commit message: ${errorMessage}`)
|
||||
} finally {
|
||||
vscode.commands.executeCommand("setContext", "cline.isGeneratingCommit", false)
|
||||
}
|
||||
}
|
||||
|
||||
function abort() {
|
||||
commitGenerationAbortController?.abort()
|
||||
vscode.commands.executeCommand("setContext", "cline.isGeneratingCommit", false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the git diff into a prompt for the AI
|
||||
* @param gitDiff The git diff to format
|
||||
@@ -31,26 +140,15 @@ Commit message:`
|
||||
|
||||
/**
|
||||
* Extracts the commit message from the AI response
|
||||
* @param aiResponse The response from the AI
|
||||
* @param str String containing the AI response
|
||||
* @returns The extracted commit message
|
||||
*/
|
||||
export function extractCommitMessage(aiResponse: string): string {
|
||||
export function extractCommitMessage(str: string): string {
|
||||
// Remove any markdown formatting or extra text
|
||||
let message = aiResponse.trim()
|
||||
|
||||
// Remove markdown code blocks if present
|
||||
if (message.startsWith("```") && message.endsWith("```")) {
|
||||
message = message.substring(3, message.length - 3).trim()
|
||||
|
||||
// Remove language identifier if present (e.g., ```git)
|
||||
const firstLineBreak = message.indexOf("\n")
|
||||
if (firstLineBreak > 0 && firstLineBreak < 20) {
|
||||
// Reasonable length for a language identifier
|
||||
message = message.substring(firstLineBreak).trim()
|
||||
}
|
||||
}
|
||||
|
||||
return message
|
||||
return str
|
||||
.trim()
|
||||
.replace(/^```[^\n]*\n?|```$/g, "")
|
||||
.trim()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,12 +157,10 @@ export function extractCommitMessage(aiResponse: string): string {
|
||||
*/
|
||||
export async function copyCommitMessageToClipboard(message: string): Promise<void> {
|
||||
await writeTextToClipboard(message)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Commit message copied to clipboard",
|
||||
}),
|
||||
)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Commit message copied to clipboard",
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,18 +173,16 @@ export async function showCommitMessageOptions(message: string): Promise<void> {
|
||||
const editAction = "Edit Message"
|
||||
|
||||
const selectedAction = (
|
||||
await getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Commit message generated",
|
||||
options: {
|
||||
modal: false,
|
||||
detail: message,
|
||||
items: [copyAction, applyAction, editAction],
|
||||
},
|
||||
}),
|
||||
)
|
||||
)?.selectedOption
|
||||
await HostProvider.window.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Commit message generated",
|
||||
options: {
|
||||
modal: false,
|
||||
detail: message,
|
||||
items: [copyAction, applyAction, editAction],
|
||||
},
|
||||
})
|
||||
).selectedOption
|
||||
|
||||
// Handle user dismissing the dialog (selectedAction is undefined)
|
||||
if (!selectedAction) {
|
||||
@@ -120,28 +214,22 @@ async function applyCommitMessageToGitInput(message: string): Promise<void> {
|
||||
if (api && api.repositories.length > 0) {
|
||||
const repo = api.repositories[0]
|
||||
repo.inputBox.value = message
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Commit message applied to Git input",
|
||||
}),
|
||||
)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Commit message applied to Git input",
|
||||
})
|
||||
} else {
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "No Git repositories found",
|
||||
}),
|
||||
)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "No Git repositories found",
|
||||
})
|
||||
await copyCommitMessageToClipboard(message)
|
||||
}
|
||||
} else {
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Git extension not found",
|
||||
}),
|
||||
)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Git extension not found",
|
||||
})
|
||||
await copyCommitMessageToClipboard(message)
|
||||
}
|
||||
}
|
||||
@@ -156,15 +244,13 @@ async function editCommitMessage(message: string): Promise<void> {
|
||||
language: "markdown",
|
||||
})
|
||||
|
||||
await getHostBridgeProvider().windowClient.showTextDocument(
|
||||
await HostProvider.window.showTextDocument(
|
||||
ShowTextDocumentRequest.create({
|
||||
path: document.uri.fsPath,
|
||||
}),
|
||||
)
|
||||
getHostBridgeProvider().windowClient.showMessage(
|
||||
ShowMessageRequest.create({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Edit the commit message and copy when ready",
|
||||
}),
|
||||
)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Edit the commit message and copy when ready",
|
||||
})
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user