Compare commits

...

41 Commits

Author SHA1 Message Date
0xtoshii 815b839be5 launch buttons 2025-07-21 16:01:57 -07:00
Ara 63f3b40ef1 Change available Cerebras models + modify context window (#5076)
* Change context length for Cerebras Qwen 3 32b to 64k

* changeset

* changeset

* Create changeset

* Add Cerebras Qwen 3 235B a22b

* Change available Cerebras models

llama-3.3-70b
qwen-3-32b
qwen-3-235b-a22b

* Add changeset

---------

Co-authored-by: Kevin Taylor <kevin.taylor@cerebras.net>
Co-authored-by: Cline Evaluation <cline@example.com>
2025-07-21 14:03:45 -07:00
Bee 827d002ea6 Fix CLINE_ENVIRONMENT configuration not being passed to webview (#5029)
* Fix CLINE_ENVIRONMENT configuration not being passed to webview

## Problem

The CLINE_ENVIRONMENT configuration set in launch.json was not being properly passed to the webview, causing the webview to break when trying to access environment-specific configurations.

This resulted in:

Webview using incorrect API URLs (always defaulting to production)
Broken authentication flows in development/staging environments
Inconsistent behavior between the main extension and webview components

## Root Cause

The issue occurred because:

Duplicate Configuration Logic: The webview had its own separate config.ts file that was trying to read process.env.CLINE_ENVIRONMENT directly
Environment Variable Propagation: While Vite was configured to pass CLINE_ENVIRONMENT to the build process, the webview's runtime code couldn't access this environment variable properly
Configuration Mismatch: The main extension and webview were using different configuration sources, leading to inconsistent environment settings

* fix credit uri

* replace apiBaseUrl with appBaseUrl

* local option

* fallback changed to app

* change to app url

* merge fixes

---------

Co-authored-by: 0xtoshii <94262432+0xToshii@users.noreply.github.com>
2025-07-21 13:58:04 -07:00
celestial-vault c040db6d71 Fix options to buildApiHandler (#5064)
* add missing taskId to buildApiHandler

* remove console log
2025-07-21 13:56:32 -07:00
Sarah Fortune 8d5985c8fd refactor(hosts): Simplify the host provider interface for callers (#5057)
* refactor(hosts): Simplify the host provider interface for callers

- Streamline API to reduce complexity for consuming code
- Update all references across core, controller, and integration modules
- Consolidate provider patterns for easier usage
- Rename host-providers.ts to host-provider.ts

# Conflicts:
#	src/core/controller/index.ts
#	src/extension.ts
#	src/integrations/git/commit-message-generator.ts

* Add comment

* In the tests, reset the HostProvider to prevent it complaining that it is being reinitialized

* Update comment

* Fix imports
2025-07-21 12:20:50 -07:00
Bee c970b8030e Refactor: Git commit message generation as a module (#5031)
* Refactor: Git commit message generation as a module

Implement Git commit message generation as a module

Refactors the Git commit message generation functionality into a separate module for better organization and maintainability.

The changes include:

- Moving the commit message generation logic from `src/core/controller/index.ts` to a new module `src/integrations/git/commit-message-generator.ts`.
- Adding a command to abort commit message generation.
- Updating the `cline.generateGitCommitMessage` command to use the new module.
- Adding a context key to disable the generate commit message button when a commit is generating.

This refactoring improves the codebase by:

- Separating concerns: The commit message generation logic is now isolated in its own module, making it easier to understand and maintain.
- Improving testability: The new module can be tested independently of the controller.
- Promoting code reuse: The commit message generation logic can be reused in other parts of the application if needed.

* clean up

* changeset added
2025-07-21 10:43:14 -07:00
Sarah Fortune e7ce38bd85 Generate typed clients for the ProtoBus API (#5063) 2025-07-21 10:09:02 -07:00
Sarah Fortune 600a6e33ed Move duplicated code for loading the protobuf descriptor set into a shared util file. (#5059)
* Move duplicated code for laoding the protobuf descriptor set into a shared util file.

* Formatting

* Update comment
2025-07-20 17:57:22 -07:00
Sarah Fortune caf0d8aee2 Add better error handling the DiffViewProvider.saveDocument() (#5041) 2025-07-19 23:58:46 -07:00
celestial-vault ee33e84c48 Speed up E2E tests (#5045)
* cache vscode and playwright downloads

* remove extra logs

* change cache path to check for vscode

* empty commit

* use optimized e2e script
2025-07-19 21:57:36 -07:00
Sarah Fortune 280138b259 Improve HostBridge error handling and logging robustness (#5037) 2025-07-19 20:54:42 -07:00
Sarah Fortune 924f235c31 Change the script scripts/runclinecore.sh to only install and run cline-core (#5049)
-and not build the zip file.
2025-07-19 17:03:06 -07:00
Tomás Barreiro 06d5bc56bc Use --system-prompt-file to pass the system prompt to Claude Code (#5024)
* Pass the system prompt through a file when using Claude Code

* Support older versions and return better errors

* Add tests

* Add changeset

* Remove outdated docs

* Use a unique file name and clean the file

* Address comments
2025-07-19 12:41:21 -07:00
canvrno b1d82e163c Robust Checkpoints timeout, error handling (#5015)
* Diable checkpoints on tasks where the init timed out, fix settings link

* early warning message

* Added checkpointTrackerErrorMessage to HistoryItem

* Updated some comments related to checkpoints timeouts

* changeset, timer cleanup

* timer cleanup
2025-07-19 12:14:41 -07:00
celestial-vault 9f73cde5e9 fix stale state in model picker searchTerm (#5044) 2025-07-19 11:48:18 -07:00
Ara be2d416359 Updating Mintlify version (#5030)
* Updating Mintlify version

* clean up and ignore docs workspace for lint (#5032)

---------

Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>
2025-07-18 19:29:28 -07:00
celestial-vault 42ffc30324 Separate plan act model settings (#4827)
* add provider handler options types and only pass those fields when building

* use separate fields for plan and act mode for ephemeral model settings

* post merge issues fixed: bedrock and cline api build handlers; current mode reading in controller/index.ts; adding a couple fields to welcomeView migration

* add in moonshotApiKey to settings-conversion after main merge

* fix type errors

* fix frontend types

* fix code after merge conflicts; switch groq provider to plan/act paradigm

* consolidate to Mode type

* remove promise resolve

* move huggingface provider into the new schema

* add new providers to migration function

* use normalizeApiConfiguration for settings menu provider value to avoid undefined state error
2025-07-18 14:14:31 -07:00
Sarah Fortune b0c67e9f83 Fix clean up part of the build-proto script (#5027) 2025-07-18 13:30:04 -07:00
Sarah Fortune fd366208a1 Fix the structure for the hosts package (#5020)
* Fix the structure for the hosts package.

Right now we support two host platforms: vscode using the internal host bridge service (inside the same process), and other platforms using the external host bridge (using gRPC over local sockets).

The structure of the two packages should mirror each other

```
hosts/
  vscode/
    VscodeDiffViewProvider
    VscodeWebViewProvider
    etc ...
  external/
    ExternalDiffViewProvider
    ExternalWebViewProvider
    etc ...
```

* Fix imports

* Fix imports

* Fix imports
2025-07-18 13:22:23 -07:00
Sarah Fortune 6c7bc58215 Move remaining platform specific code out of the diff view provider. (#5009)
* .

* Move the DecorationController into the hosts/vscode package.
2025-07-18 13:16:19 -07:00
Sarah Fortune f69a378ff4 Add eslint checks for more vscode API calls (#5023)
* Add eslint check for more vscode API calls

Add eslint checks to prevent vscode API calls from being reintroduced after they were switched to the host bridge.

The ones that are not enabled are not completely migrated, so they are not turned on because it would cause too many warnings, and make developers used to ignoring them.

* Update eslint-rules/no-direct-vscode-api.js

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Update warning message text

* Move the check for if the file is being included in the linter check or not into its own function

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-07-18 13:08:13 -07:00
Sarah Fortune 309e3bd85c Fix import in McpHub (#5021) 2025-07-18 11:59:07 -07:00
Daniel Campos Olivares eb6eb371e4 fix: Add documentation to nav menu (#5014) 2025-07-18 11:25:40 -07:00
Mohan Raj Rajamanickam 419e3e4677 fix: mcp servers are not started when disabled (#4501)
* fix: disabled servers should not be started

* refactor to dedupe

* add changeset

* Update moody-crabs-relate.md

* minor comment tweaks

* use unknown instead of any
2025-07-18 10:35:19 -07:00
Sarah Fortune e95eecd65f Just shut up already (#5011)
Don't display a notification when the MCP servers have updated.
2025-07-17 23:32:10 -07:00
Sarah Fortune e83e71cc6d Remove class that is unused, and other class that is totally commented out. (#5010) 2025-07-17 23:00:29 -07:00
Sarah Fortune a9e526e99b Move save() and getDocument() to the platform specific diff classes (#5008)
* Move logic to save the diff document and get the diff content to the platform specific diff view providers.

Add getDocumentText to the diff service.

The ExternalDiffViewProvider should be using its activeDiffEditorId (not the activeDiffEditor, which is the vscode Editor object)

* Check activeDiffEditorId

* Update src/integrations/editor/DiffViewProvider.ts

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Use await with this.saveDocument()

* Remove test file

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-07-17 18:06:54 -10:00
Sarah Fortune 72f16a8c30 Don't need to use Message.create({...}) with the host bridge (#4999)
* Use await when calling async function showMessage.

Use await when calling `windowClient.showMessage`, otherwise the caller doesn't wait for the request to complete and any exceptions are lost.

For the host bridge RPCs, you don't need to do `ShowMessageRequest.create({...})`, you can just pass the request directly like: `{...}. The Message.create() was needed for the ProtoBus because of the way it was implemented, it couldn't be type-checked by the compiled. It's not needed elsewhere.

* Dont use await, because it blocks until the message is dismissed

* Dont need `?.selectedOption`, the response cannot be null

* Dont use await
2025-07-17 20:15:42 -07:00
Toshii 1a3ec9f024 move environment setting to launch & publish (#5002)
* move env setting to launch & publish

* change env name

* Update webview-ui/vite.config.ts

Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>

* update env location

---------

Co-authored-by: Bee <68532117+abeatrix@users.noreply.github.com>
2025-07-17 18:49:36 -07:00
Sarah Fortune b3fa3ad0d3 Add new RPCs to the host bridge diff service (#4997)
* Add truncate document to the hostbridge diff service.

# Conflicts:
#	src/standalone/ExternalDiffviewProvider.ts

* Fix typo

* Add host bridge rpcs for the diff service

* Add saveDocument and closeDiff to the host bridge.

* Formatting

* Use the host bridge in ExternalDiffviewProvider

* Make comments less verbose
2025-07-17 18:36:43 -07:00
github-actions[bot] 45241fcccf v3.19.7 Release Notes (#4983)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md for version 3.19.7

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: pashpashpash <nik@cline.bot>
2025-07-17 18:19:36 -07:00
pashpashpash 2a3f0e9418 Adding huggingface provider (#4952)
Co-authored-by: arafatkatze <arafat.da.khan@gmail.com>
2025-07-17 18:00:28 -07:00
Toshii 4334764903 remove warning (#5003) 2025-07-17 17:53:25 -07:00
Bee b3a10243b8 Unify error handling and display logic (#4984)
* Unify error handling and display

Several improvements to error handling and reporting within the Cline extension, focusing on providing more informative error messages to the user and improving telemetry data.

- **Error Handling:**
  - Introduces a `ClineError` class to encapsulate Cline-specific errors, providing structured data about the error (status code, request ID, etc.).
  - The `ErrorService` now creates and logs `ClineError` instances, improving error reporting to Sentry and telemetry.
  - Removes redundant error formatting logic from `src/core/task/utils.ts`, relying on the `ClineError` class for consistent error representation.
  - The Cline API handler now throws the raw error, allowing the `ClineError` class to handle the error formatting.

- **UI Improvements:**
  - Introduces new UI components (`ErrorRow`, `ErrorBlockTitle`) to display error messages in a more user-friendly format within the chat interface.
  - Displays credit limit errors with detailed information and a link to purchase credits.
  - Improves the display of rate limit errors and authentication errors.
  - Adds specific handling for PowerShell-related errors, providing a link to a troubleshooting guide.

- **Telemetry:**
  - Captures provider API errors using `ClineError` data, providing more detailed information about the error in telemetry reports.

- **Other Changes:**
  - Fixes an import path in `src/services/posthog/PostHogClientProvider.ts`.
  - Adds tests for the new UI components.
  - Adds `error` field to `ClineMessage` to transport `ClineError` instances to the webview.

These changes aim to provide a better user experience by displaying more informative error messages and improving the overall reliability of the Cline extension.

* include provider id

* includes model and provider id

* fix test

* return gracefully for empty response
2025-07-17 16:21:18 -07:00
Bee 8e95c136a6 Handle auth state changes in all extension windows (#4987)
* Handle auth state changes in all extension windows

Addresses an issue where authentication state changes (login/logout) were not being properly propagated across all extension windows.

The `onDidChange` event handler for `clineAccountId` secret now checks if the secret was added/updated (login) or removed (logout).

- If the secret exists, `restoreRefreshTokenAndRetrieveAuthInfo` is called to restore auth info (login from another window).
- If the secret is removed, `handleDeauth` is called to handle logout for all windows.

This ensures that all extension windows are kept in sync with the current authentication state.

* changeset
2025-07-17 15:40:30 -07:00
Daniel Campos Olivares 0b66faa1dd chore: Introduce SAP AI Core documentation (#4961)
* feat: Introduce SAP AI Core documentation

* fix: Markdown lint

Signed-off-by: Daniel Campos Olivares <dacamposol@gmail.com>

* fix: Typo

Signed-off-by: Daniel Campos Olivares <dacamposol@gmail.com>

---------

Signed-off-by: Daniel Campos Olivares <dacamposol@gmail.com>
2025-07-17 14:10:30 -07:00
Sarah Fortune 3d2dc1c5c4 Move platform specific code out of the DiffViewProvider and into the VscodeDiffViewProvider (#4980)
* Add a way to reset state for the platform specific diff view providers.

Add the abstract method `resetDiffView()` to the DiffViewProvider.

On Vscode it reset the active diff editor and decorations.

On External diff view providers, it reset the diff editor ID.

* Add a way to truncate the document to the platform specific diff view providers.

This is used when the last update is recieved to remove any content from the bottom of the document.

* Move `closeDiffView` to the platform specific diff view classes.

* Update comment

* Use await for async function

* Move setting the cursor position into replaceText
2025-07-17 12:35:48 -07:00
Andrei Eternal 028412579b Standalone Terminal Manager & remove other vscode-impls (now covered by host bridge) (#4993)
* Revert "experimental vscode impls & build-proto cleanup (#4493)"

This reverts commit f00c5f4ecc.

* leave terminal impls

* terminal manager switch for standalone

* Export standaloneTerminalManager to global

* clean up standalone terminal switch

---------

Co-authored-by: Andrei Edell <andrei@nugbase.com>
2025-07-17 12:11:45 -07:00
Sarah Fortune f04788c2ec Move the logic to scroll the diff editor window into the platform specific classes (#4977)
* Move the logic to scroll the diff editor window into the platform specific classes.

Move the current logic into the VscodeDiffViewProvider.

The ExternalDiffViewProvider will use the host bridge to scroll the editor tab. But for now it doesn't do anything.

* Add comment

* Remove log messages
2025-07-17 11:26:40 -07:00
Toshii 5fba39f886 move to simple config for setting env details of backend (#4978)
* backend env

* frontend env config

* rename config

* preview update

* separate mcp link

* mcp url updated

* config change to local

* webview config to local
2025-07-16 22:53:08 -07:00
Toshii 9a4e3655b3 remove import (#4981) 2025-07-16 19:40:43 -07:00
189 changed files with 7311 additions and 27212 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Improve cerebras Qwen model performance by removing thinking tokens from the model input
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Change available Cerebras models - limit to Qwen and llama 3.3 70b
+8
View File
@@ -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
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
fix: mcp servers are not started when disabled
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Refactor Git commit message generation to support output streaming.
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Introduce Claude Code support on Windows and fix E2BIG issues
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Improve the Claude Code error messages
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Change Cerebras Qwen 3 32b context window from 16k to 64k
-1
View File
@@ -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",
+26 -6
View File
@@ -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() }}
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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/
+31 -3
View File
@@ -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"
}
},
{
+6
View File
@@ -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
View File
@@ -159,7 +159,8 @@
"provider-config/openai",
"provider-config/openai-compatible",
"provider-config/openrouter",
"provider-config/requesty"
"provider-config/requesty",
"provider-config/sap-aicore"
]
},
{
-10118
View File
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -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"
}
}
+2 -20
View File
@@ -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
+39
View File
@@ -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" }],
},
],
})
-3
View File
@@ -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",
},
},
+62 -17
View File
@@ -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)
}
}
}
}
}
}
}
}
}
},
}
},
})
+1019 -12545
View File
File diff suppressed because it is too large Load Diff
+23 -8
View File
@@ -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",
+1
View File
@@ -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
View File
@@ -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 {}
+4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+36 -52
View File
@@ -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}
}`
+73 -36
View File
@@ -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()
+50
View File
@@ -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 }
}
+7 -7
View File
@@ -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
View File
@@ -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)
}
+13 -13
View File
@@ -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)
+1 -1
View File
@@ -26,7 +26,7 @@ describe("OllamaHandler", () => {
beforeEach(() => {
options = {
ollamaModelId: "llama2",
actModeOllamaModelId: "llama2",
ollamaBaseUrl: "http://localhost:11434",
}
handler = new OllamaHandler(options)
+1 -1
View File
@@ -13,7 +13,7 @@ interface AnthropicHandlerOptions {
}
export class AnthropicHandler implements ApiHandler {
private options: ApiHandlerOptions
private options: AnthropicHandlerOptions
private client: Anthropic | undefined
constructor(options: AnthropicHandlerOptions) {
+1 -1
View File
@@ -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) {
+3 -15
View File
@@ -12,6 +12,7 @@ import OpenAI from "openai"
import { version as extensionVersion } from "../../../package.json"
import { shouldSkipReasoningForModel } from "@utils/model-utils"
import { CLINE_ACCOUNT_AUTH_ERROR_MESSAGE } from "@/shared/ClineAccount"
import { clineEnvConfig } from "@/config"
interface ClineHandlerOptions {
taskId?: string
@@ -28,10 +29,7 @@ export class ClineHandler implements ApiHandler {
private clineAccountService = ClineAccountService.getInstance()
private _authService: AuthService
private client: OpenAI | undefined
// TODO: replace this with a global API Host
private readonly _baseUrl = "https://api.cline.bot"
// private readonly _baseUrl = "https://core-api.staging.int.cline.bot"
// private readonly _baseUrl = "http://localhost:7777"
private readonly _baseUrl = clineEnvConfig.apiBaseUrl
lastGenerationId?: string
private counter = 0
@@ -179,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
}
}
+1 -1
View File
@@ -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) {
+142
View File
@@ -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
}
}
+9
View File
@@ -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
+6
View File
@@ -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
+80
View File
@@ -0,0 +1,80 @@
export enum Environment {
production = "production",
staging = "staging",
local = "local",
}
interface EnvironmentConfig {
appBaseUrl: string
apiBaseUrl: string
mcpBaseUrl: string
firebase: {
apiKey: string
authDomain: string
projectId: string
storageBucket?: string
messagingSenderId?: string
appId?: string
}
}
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
}
// 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
+2 -3
View File
@@ -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)
+9 -13
View File
@@ -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({
+5 -7
View File
@@ -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,
+75 -343
View File
@@ -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,12 +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"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -49,6 +48,7 @@ export class Controller {
private disposables: vscode.Disposable[] = []
task?: Task
workspaceTracker: WorkspaceTracker
mcpHub: McpHub
accountService: ClineAccountService
@@ -82,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"
}
/*
@@ -111,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",
})
}
}
@@ -256,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
@@ -466,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
}
@@ -498,7 +371,7 @@ export class Controller {
// MCP Marketplace
private async fetchMcpMarketplaceFromApi(silent: boolean = false): Promise<McpMarketplaceCatalog | undefined> {
try {
const response = await axios.get("https://api.cline.bot/v1/mcp/marketplace", {
const response = await axios.get(`${clineEnvConfig.mcpBaseUrl}/marketplace`, {
headers: {
"Content-Type": "application/json",
},
@@ -524,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
}
@@ -537,7 +408,7 @@ export class Controller {
private async fetchMcpMarketplaceFromApiRPC(silent: boolean = false): Promise<McpMarketplaceCatalog | undefined> {
try {
const response = await axios.get("https://api.cline.bot/v1/mcp/marketplace", {
const response = await axios.get(`${clineEnvConfig.mcpBaseUrl}/marketplace`, {
headers: {
"Content-Type": "application/json",
"User-Agent": "cline-vscode-extension",
@@ -613,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,
})
}
}
@@ -639,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
}
@@ -974,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
}
+2 -2
View File
@@ -3,8 +3,8 @@ import { StringRequest } from "../../../shared/proto/common"
import { McpDownloadResponse } from "../../../shared/proto/mcp"
import { McpServer } from "@shared/mcp"
import axios from "axios"
import * as vscode from "vscode"
import { sendChatButtonClickedEvent } from "../ui/subscribeToChatButtonClicked"
import { clineEnvConfig } from "@/config"
/**
* Download an MCP server from the marketplace
@@ -31,7 +31,7 @@ export async function downloadMcp(controller: Controller, request: StringRequest
// Fetch server details from marketplace
const response = await axios.post<McpDownloadResponse>(
"https://api.cline.bot/v1/mcp/download",
`${clineEnvConfig.mcpBaseUrl}/download`,
{ mcpId },
{
headers: { "Content-Type": "application/json" },
@@ -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
+17 -25
View File
@@ -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,
})
}
}
+2 -1
View File
@@ -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
+7 -12
View File
@@ -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
+3 -3
View File
@@ -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() }))
}
+65 -13
View File
@@ -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()
}
}
}
})
+22
View File
@@ -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
}
}
+9 -13
View File
@@ -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 -1
View File
@@ -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,
+51 -36
View File
@@ -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"
+381 -5
View File
@@ -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
View File
@@ -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)
+9 -2
View File
@@ -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
View File
@@ -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
+4
View File
@@ -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)
-16
View File
@@ -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,
+6 -8
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
}
@@ -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
}
}
}
@@ -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)
}
}
+101
View File
@@ -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
-49
View File
@@ -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
}
+109 -8
View File
@@ -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 -1
View File
@@ -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"
+163
View File
@@ -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
})
})
})
})
+56 -24
View File
@@ -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
*/
+125 -152
View File
@@ -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()
}
}

Some files were not shown because too many files have changed in this diff Show More