Compare commits

...
Author SHA1 Message Date
celestial-vault 05fedee11e update the current task's consecutiveAutoApprovedRequestsCount on max request change 2025-07-15 22:44:20 -07:00
github-actions[bot] 80fbcda03b v3.19.5 Release Notes (#4941)
-   Add Groq as a new API provider with support for all Groq models including Kimi-K2
-   Add user role display in organization UI for Cline account users
-   Fix message dialogs not showing option buttons properly
-   Fix authentication issues when using multiple VSCode windows
2025-07-15 21:49:06 -07:00
pashpashpash cef79e06db not showing request id when insufficient balance (#4953)
* not showing request id when insufficient balance

* whoops

* betterrr
2025-07-15 21:38:11 -07:00
Ara 5dddbef65c Adding Groq provider (#4943) 2025-07-15 19:47:56 -07:00
b6f6358d4a Set up E2E tests with Playwright (#4721)
* Add Playwright E2E tests

Adding end-to-end (E2E) testing capabilities using Playwright. It also updates the `@vscode/test-electron` dependency.

The changes include:

- Adding Playwright as a dev dependency.
- Adding `e2e` and `e2e:build` scripts to `package.json` for running E2E tests.
- Adding `@playwright/test` to the list of dependencies.
- Updating `@vscode/test-electron` from `2.4.1` to `2.5.2`.
- Adding `test-results` to `.gitignore` to exclude test result files.

* wip: github workflow

Adding a new GitHub Actions workflow for running end-to-end (E2E) tests using Playwright. The workflow is triggered on push to the main branch, pull requests, and manual workflow dispatch.

The workflow defines a matrix strategy to run tests on different runners (Ubuntu and Windows) and shards. It also uploads Playwright recordings as artifacts if the tests fail.

* add @vscode/vsce as dev dep

* update workflow

* apply feedback

* fix test workflows

* add command palette helper

This commit improves the reliability and efficiency of the end-to-end tests by:

- Adding a delay to the "Let's go!" button click in the auth test to ensure the action is properly registered.
- Adding an expectation to ensure the "Get Started for Free" button is no longer visible after API key submission.
- Caching the "Use your own API key" button to avoid redundant lookups.
- Introducing a `runCommandPalette` helper function to streamline command execution within the VS Code environment.
- Disabling notifications before running the tests to prevent interference.

* state change

* set TEMP_PROFILE

* v3.18.7 Release Notes

* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md for version 3.18.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>

* Remove optimistic loading from organization dropdown (#4746)

* update build script to javascript

* fix match

* Add mode switching to chat test

* expected

---------

Co-authored-by: abeatrix <beatrix@cline.bot>
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>
Co-authored-by: canvrno <46584286+canvrno@users.noreply.github.com>
2025-07-16 07:29:58 +05:30
Bee e607d02ab2 Fix: pass items to showMessage in VS Code host bridge (#4949) 2025-07-15 18:14:30 -07:00
Nick Baumann b5e2916bd6 docs: Update Claude Code documentation to include Pro plans alongside Max plans (#4948) 2025-07-15 17:00:45 -07:00
Sarah Fortune 371db77007 Move the vscode specific classes into the hosts/vscode package. (#4947)
* Move the vscode specific classes into the `hosts/vscode` package.

Move the vscode specific classes VscodeDiffViewProvider and VscodeWebviewProvider in the `hosts/vscode` package.

I am doing this so that the vscode-specific code is contained in one package instead of being mixed with the code that is meant to be platform-agnostic.

This also makes it easier for us to see which parts of the codebase are still using the vscode APIs and need to be migrated, and for the linter rules
that check that vscode API calls are not reintroduced after they are migrated to the host bridge.

* Use absolute imports instead of relative
2025-07-15 16:46:58 -07:00
Sarah Fortune 80f2e9f6ea Fix bad merge (#4946) 2025-07-15 16:26:12 -07:00
celestial-vault b46d396de2 add webview type checking to check-types (#4944) 2025-07-15 16:20:07 -07:00
Sarah Fortune 8683980c90 Move the vscode hostbridge handlers into their own package (#4945)
* Move the generated files for the host bridge into the 'src/generated' directory

Move the generated index.ts and methods.ts files for the host bridge into the 'src/generated' directory

I'm doing this because when all the generated files are one in location its a) easier to see from the import statement that the code is generated, b) it's easier to change the package(s) of the generated files, and c) easier to reset/clean the build state.

* Update import path

* Move the hostbridge handlers in the a hostbridge packge.

Move the hostbridge handlers out of the top level of the vscode package into their own subpackage.
I have to move all the vscode specific code into the `hosts/vscode` package, and I want the hostbridge handlers to be grouped together, not mixed in with things like the VscodeDiffViewProvider, VscodeWebviewProvider etc.
2025-07-15 16:12:48 -07:00
Beeandabeatrix b916e495e6 Remove credit validation from request (#4903)
Removes the credit validation check from the `createMessage` function in `src/api/providers/cline.ts`. The `validateRequest` function, which checks if the user has sufficient credits, has also been removed from `src/services/account/ClineAccountService.ts`.

The credit validation is no longer performed before sending a message to the Cline API.

Co-authored-by: abeatrix <beatrix@cline.bot>
2025-07-15 16:08:08 -07:00
Sarah Fortune d45077c4c5 Move the generated files for the host bridge into the 'src/generated' (#4942)
* Move the generated files for the host bridge into the 'src/generated' directory

Move the generated index.ts and methods.ts files for the host bridge into the 'src/generated' directory

I'm doing this because when all the generated files are one in location its a) easier to see from the import statement that the code is generated, b) it's easier to change the package(s) of the generated files, and c) easier to reset/clean the build state.

* Update import path
2025-07-15 16:00:02 -07:00
Beeandabeatrix 6ced4472d3 Capture provider API errors (#4936)
* Capture provider API errors

Introduces a new telemetry event to capture errors returned by API providers. This will allow us to better monitor the reliability and performance of different providers and identify potential issues.

The following changes were made:

- Added a `captureProviderApiError` method to the `TelemetryService` to record provider API errors.
- Added a new `PROVIDER_API_ERROR` event to the `TelemetryService.EVENTS.TASK` enum.
- Modified the `Task` class to capture and report provider API errors, including the error message, status code, and request ID.
- Added `extractErrorDetails` to extract the status code, message, and request ID from an error object.
- Updated `formatErrorWithStatusCode` to use `extractErrorDetails`.

* clean up

---------

Co-authored-by: abeatrix <beatrix@cline.bot>
2025-07-15 15:37:25 -07:00
Beeandabeatrix dcb39a77f2 Display user role in organization (#4937)
* Display user role in organization

Adds a new feature to the Account View that displays the user's role within the currently selected organization.

- Added a `getMainRole` function to determine the user's primary role (Owner, Admin, or Member) based on the roles array.
- Display a VSCodeTag component showing the user's role next to the organization dropdown.
- Updated the organization dropdown to use className instead of style for width.

* changeset

---------

Co-authored-by: abeatrix <beatrix@cline.bot>
2025-07-15 15:36:47 -07:00
Sarah Fortune 3301577934 Add diff.replaceText to the host bridge. (#4879)
Update the diff service to use a unique id to track open diff editor in external platforms.
Store the diff Id in the ExternalDiffViewEditor when the diff is opened. It will serve the same purpose as the activeDiffEditor property on vscode, it can be used to manipulate the diff editor tab.
Add the implementation of replaceText in the ExternalDiffViewEditor.
2025-07-15 14:50:23 -07:00
a36c11eb97 Remove state parameter from auth callback (#4845)
* Remove state parameter from auth callback

Removes the state parameter that contains auth nonce and the associated logic.

The state parameter which contains the auth nonce in the auth callback doesn't work with multi-windows as each window contains its own nonce. As the provider parameter is sufficient to identify the auth provider we could remove the auth nonce to avoid complications.

* remove authNonce

* changeset

---------

Co-authored-by: abeatrix <beatrix@cline.bot>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-07-16 01:20:51 +05:30
Sarah Fortune 7d1f199883 Move the generated file hosts/vscode/host-grpc-service-config.ts in the src/generated directory. (#4938)
Rename some of the methods in the build-protos script to be more readable.
2025-07-15 12:39:21 -07:00
Sarah Fortune 6a1e0e518b Move the build-proto script into the scripts directory (#4935)
* Move the build-protos script into the scripts directory.

* Reorder imports
2025-07-15 12:04:04 -07:00
pashpashpash 004b313d20 Update diff edit evals README.md (#4920)
* Update README.md

* Update README.md
2025-07-16 00:17:14 +05:30
Saoud Rizwan bf37bfa7a3 Add vision capability to moonshot v1 (#4926) 2025-07-15 04:15:39 -07:00
e6dbde70a9 v3.19.4 Release Notes (#4925)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md

---------

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: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-07-15 03:57:26 -07:00
Saoud Rizwan cb9a339442 Add ability to choose chinese endpoint for Moonshot provider (#4924)
* Add ability to choose chinese endpoint for Moonshot provider

* Create fluffy-planes-prove.md
2025-07-15 03:33:17 -07:00
47b5df14d7 v3.19.3 Release Notes (#4917)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md

---------

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: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-07-15 01:32:06 -07:00
Saoud Rizwan 4ecbecb1e2 Add Moonshot AI provider (#4913)
* Add Moonshot AI provider

* Create little-pens-switch.md
2025-07-15 01:16:45 -07:00
fadaf00835 v3.19.2 Release Notes (#4910)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md

---------

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: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-07-14 22:19:20 -07:00
575cfd48cc Includes request ID in error returned by Cline API (#4909)
* Includes request ID in error returned by Cline API

Adding the request ID to error messages to aid in debugging for users.

* Use constant for auth error message; revert change from previous PR

* Fix webview auth status if null/undefined user is passed in auth status message

* Create nasty-parents-play.md

---------

Co-authored-by: abeatrix <beatrix@cline.bot>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-07-14 22:16:50 -07:00
Saoud Rizwan a0787e3d36 Release Version v3.19.1 (#4908) 2025-07-14 21:51:24 -07:00
Nidelson GimenezandSaoud Rizwan c9b922009f docs: improve documentation (#4863)
* docs: fix typo

* improved clarity in the local development instructions

* Update CONTRIBUTING.md

* Create wise-hairs-grow.md

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-07-14 21:42:06 -07:00
2d6ff38e69 v3.18.15 Release Notes (#4890)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md

---------

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: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-07-14 21:01:12 -07:00
Saoud Rizwan 3069e27413 Add groq to kimi providers (#4906)
* Add groq to kimi providers

* Create fair-glasses-lick.md
2025-07-14 20:54:34 -07:00
Saoud Rizwan 57c8b8120d Revert "pass linter errors with read_file (#4159)" (#4905)
This reverts commit 5e2b199377.
2025-07-14 20:18:19 -07:00
Saoud Rizwan 5243f0b9b1 Set default kimi provider to together (#4904) 2025-07-14 19:49:45 -07:00
celestial-vault c014060275 remove unused ExtensionStateContext setters (#4865) 2025-07-14 18:35:54 -07:00
celestial-vault d790ce86a0 Add markdown parsing to MCP responses (#4862)
* refactor out useEffect logic

* add markdown parsing to mcp response

* add display mode to global state; simplify state flow

* fix imports after merge conflicts
2025-07-14 18:17:14 -07:00
celestial-vault 5e2b199377 pass linter errors with read_file (#4159)
* pass linter errors with read_file

* changeset

* add code back after merge with main
2025-07-14 16:49:37 -07:00
celestial-vault 9234d0cdc4 [McpResponseDisplay] Refactor out useEffect logic to helper function (#4852)
* refactor out useEffect logic

* refactor: memoize renderSegment with useCallback
2025-07-14 15:29:50 -07:00
Sarah Fortune db1db8c95d Change the name of the cline core script from standalone.js to cline-core.js (#4894)
Update the name of the script so that you can tell from the process name what it is,
standalone.js is too vague to be able to associate it with cline.
2025-07-14 15:22:11 -07:00
Sarah Fortune f53af72643 Update the vscode usages script to separate out vscode.commands.executeCommand calls. (#4891)
vscode.commands.executeCommand runs other vscode commands, so we need the know which commands are being run.
2025-07-14 15:21:27 -07:00
Massimiliano Angelino 260e0d5f8e feat: adding Bedrock Api Keys support (#4728) 2025-07-15 00:19:58 +05:30
celestial-vault 5b68ee5523 Add kimi-k2 as trending model (#4889)
* add kimi k2 as trending model

* changeset

* adjust wording
2025-07-14 11:21:26 -07:00
Ara 3e5abd5e72 Removing reasoning UX for Grok 4 models and correct pricing (#4849)
* Removing reasoning UX for Grok 4 models and correct pricing

* More resiliency
2025-07-13 17:36:41 -07:00
Sarah Fortuneandellipsis-dev[bot] 1ba5873454 DiffViewProvider refactoring for the host bridge (#4877)
* DiffViewProvider refactoring.

Move the platform specific logic for updating the diff out of DiffViewProvider and into VscodeDiffViewProvider.replaceText().
Add a stub handler for replaceText in the ExternalDiffviewProvider.

* Add comment to replaceText()

* Update src/integrations/editor/DiffViewProvider.ts

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

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-07-13 16:28:09 -07:00
Sarah Fortune 1bdaf8ef6f Implement openDiffEditor in the ExternalDiffViewProvider (#4875)
Use the host bridge to open the diff view.
2025-07-13 14:43:33 -07:00
pashpashpash 7f6038c74e better balance display (#4867) 2025-07-12 20:36:19 -07:00
Sarah Fortune 2fd9635b97 Add a vscode specific DiffViewProvider, and one for external platforms. (#4866)
* Add a vscode specific DiffViewProvider, and one for external platforms.

Make the DiffViewProvider class abstract, with two implementations the VscodeDiffViewProvider and the ExternalDiffViewProvider.
Move the vscode specific code to open the diff view into VscodeDiffViewProvider.openDiffEditor. The ExternalDiffViewProvider will use the host bridge to open the diff view.
Add a way to get the correct DiffViewProvider for the platform to the host provider.

Right now, there is only platform specific logic for `open`, the other functions like `scroll` and `replaceText` will be added in a follow up PR. The end-state will look like [this](https://github.com/cline/cline/compare/main...sjf-gg), but it's easier to test and review each part separately.

* In the VscodeDiffViewProvider, use the previous way of getting the open diff editor, before it was switched to the host bridge.

Using the host bridge doesn't help here, because we really need a reference to the actual vscode text editor document, which the host bridge can't return. So, it was doing openDocument, and then immediately using the vscode SDK to get the editor reference. When, really the diff editor was already open, and we only need the editor reference.
2025-07-12 12:45:57 -07:00
Sarah Fortune 568b834338 Add DiffService to the host bridge. (#4841)
* Add DiffService to the host bridge.

Add a new diff service with a method to open the diff view for a file.
On vscode, we will not use the hostbridge for opening the diff editor, so the vscode handler just throws an error.

* Update diff.proto

* Fix proto import
2025-07-12 12:43:55 -07:00
381e9b9d1f v3.18.14 Release Notes (#4857)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md

---------

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: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-07-12 02:47:50 -07:00
Saoud Rizwan d86861629d Fix re-sign in flow (#4856)
* Fix re-sign in flow

* Fix AuthState user representation to webview to fix issue where invalid auth was still showing as logged in

* Fix comments

* Create stale-peas-give.md
2025-07-12 02:45:18 -07:00
7fb10ba053 v3.18.13 Release Notes (#4846)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md

---------

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: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-07-12 02:03:43 -07:00
Saoud RizwanandCopilot b7ca95ed57 Replace firebase user object session logic with google endpoint to get id token and custom jwt validation (#4853)
* Replace firebase user object session logic with google endpoint to get id token and custom jwt validation

* Add comments

* Update src/services/account/ClineAccountService.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Create green-drinks-rest.md

* Remove signOut

* Ensure that refresh token is passed properly in params

* Name function better and add comment; fix org switch logic

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-12 02:00:52 -07:00
Saoud Rizwan 6bd8726dd6 Saoudrizwan/show resignin button (#4855)
* Show sign in button when cline account shows auth error

* Add comment
2025-07-12 01:00:42 -07:00
Saoud Rizwan 347d4f48da Remove cancelReason when retrying request to show the proper animation in ChatRow (#4854) 2025-07-12 00:59:44 -07:00
canvrno baa5aaa0a7 git branch analysis workflow (#4717) 2025-07-11 17:46:46 -07:00
16f066dcbf Host bridge migration: showErrorMessage, showInformationMessage, showWarningMessage (#4745)
* host bridge migration: showErrorMessage & showInformationMessage & showWarningMessage

Introduces the `showMessage` host API to the VS Code extension, allowing the host to display informational, warning, and error messages to the user.

The changes include:

- A new `ShowMessageRequest` and `SelectedResponse` message definition in `proto/host/window.proto` to define the request and response structure for showing messages.
- A new file `src/hosts/vscode/window/showMessage.ts` that implements the `showErrorMessage`, `showInformationMessage`, and `showWarningMessage` functions. These functions use the VS Code API to display messages based on the `ShowMessageRequest`.
- Replace all current call sites with the new implementations

* define the request structure

This commit introduces the `showErrorMessage`, `showInformationMessage`, and `showWarningMessage` host APIs to the VS Code extension. These APIs allow the host to display informational, warning, and error messages to the user.

The changes include:

- Added `ShowErrorMessageRequest`, `ShowInformationMessageRequest`, and `ShowWarningMessageRequest` message definitions in `proto/host/window.proto` to define the request structure for showing messages.
- Added new files `src/hosts/vscode/window/showErrorMessage.ts`, `src/hosts/vscode/window/showInformationMessage.ts`, and `src/hosts/vscode/window/showWarningMessage.ts` that implement the corresponding functions. These functions use the VS Code API to display messages based on the provided message and options.

* Simplify showMessage functions and use options array

The `showErrorMessage`, `showInformationMessage`, and `showWarningMessage` functions in `src/hosts/vscode/window/` have been refactored for simplification.

- The functions now directly destructure the `modal`, `detail`, and `items` properties from the input object.
- The VS Code API calls now directly pass the `modal` and `detail` options, and use the spread operator to pass the `items.options` array as additional arguments. This removes the need for conditional logic to construct the options object.

* wip: apply feedback

* Update src/integrations/git/commit-message-generator.ts

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

* merge conflict

---------

Co-authored-by: abeatrix <beatrix@cline.bot>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-07-11 17:29:01 -07:00
celestial-vault 59f42c7a81 [AccountView] - Default balance to dashes (#4844)
* default balance to dashes

* changeset

* add balance formatter
2025-07-12 05:55:47 +05:30
celestial-vault 3a86938a56 pass extension version in headers (#4843) 2025-07-11 16:16:23 -07:00
Sarah Fortune 042bf359a9 DiffViewProvider refactoring for the host bridge (#4836)
* DiffViewProvider refactoring

Move the vscode specific code to setup and open the diff view editor into `openDiffEditor`.

* Change openDiffEditor to return void instead of returning a vscode specific editor type
2025-07-11 14:18:26 -07:00
Beeandabeatrix 17200740a8 Trigger auth status update on secret storage change (#4837)
* Trigger auth status update on secret storage change

The auth status should be updated when the clineAccountId secret changes. This commit adds a listener to the secrets.onDidChange event and calls sendAuthStatusUpdate when the clineAccountId secret changes. This ensures that the auth status is always up-to-date.

* restore

* afgain

---------

Co-authored-by: abeatrix <beatrix@cline.bot>
2025-07-11 12:53:24 -07:00
Sarah Fortune c38f443ec4 DiffViewProvider clean-up (#4835)
* In the DiffViewProvider, store the absolutePath instead of the cwd.

Remove unused var `scrollListener`.

* Remove cwd param

* Don't call getCwd in a loop
2025-07-11 10:49:13 -07:00
13f1f0d44b v3.18.12 Release Notes (#4819)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md for version 3.18.12

---------

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-10 19:50:20 -07:00
pashpashpash 30a169e0c3 supporting buy_credits_url response from backend (#4823) 2025-07-10 19:33:25 -07:00
Ara 77ab7f8ef9 Fix the flaky Cline provider switching toggle (#4791)
* Fix Flaky Cline provider toggle

* More resiliency
2025-07-10 19:32:19 -07:00
akfosterandellipsis-dev[bot] 6d5ea98026 fix: insufficient credits display (#4821)
* fix: insufficient credits display

* add changeset

* Update src/api/providers/cline.ts

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

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-07-10 19:21:59 -07:00
Sarah Fortune 1879798b68 Remove unused vars from the DiffViewProvider (#4818)
Remove `lastFirstVisibleLine`, this value is set, but never read.
Remove `shouldAutoScroll`, this is always true.
2025-07-10 16:10:13 -07:00
3ed47fba17 Request validation and remove credit balance for cline team (#4817)
* Fix: Ensure Cline client is initialized with the latest auth token

The Cline client was not being re-initialized with the latest authentication token after the user signs in. This resulted in the client using an outdated or non-existent token, leading to authentication errors when making API requests.

This commit ensures that the Cline client is initialized with the most recent authentication token by setting the `apiKey` property of the `OpenAI` client instance to the current auth token retrieved from `AuthService` before every request. This guarantees that the client always uses the valid and up-to-date token for authentication.

* changeset

* move this._authService.getAuthToken() to ensureClient

* Request validation and remove credit balance for cline team

Add request validation for Cline API requests and fix the user interface for displaying credit-related information in UI.

The changes include:

- **Credit Balance Validation:** Implemented `validateRequest` method in `ClineAccountService` to check user's credit balance before making API requests.  Requests from active organizations are skipped. An error is thrown if the balance is insufficient.
- **Error Handling:** Improved error handling in `ClineHandler` to provide more informative error messages to the user.
- **UI Enhancements:**
    - Updated `CreditLimitError` component to display the current balance that matches the account view balance format (4 decimal places).
    - Modified `ChatRow` to parse error messages and display the `CreditLimitError` component when applicable.
    - Updated `AccountView` to only display credit balance for user accounts, not organization accounts.
    - Removed unused props from `CreditLimitError` component. Context: https://cline-space.slack.com/archives/C08KYBFL9DJ/p1752182278852399?thread_ts=1752164726.247429&cid=C08KYBFL9DJ
- **Dependencies:** Updated dependencies in `webview-ui` to include `tailwindcss` and configured `tailwind.config.js` to support VSCode theme variables.

* changeset

* Update src/api/providers/cline.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: abeatrix <beatrix@cline.bot>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-10 15:58:13 -07:00
Sarah Fortune 439e99d8d1 Fix script that packages the standalone zip. (#4816)
The vscode packager VCE has different logic that the npm module `ignore`, so it was not including the same files as VCE.

Just copy how VCE uses the .vscodeignore file in the packaging script.

Fix the ignore for demo.gif, it was still getting included because it was only matching demo.gif at the top level.
Also ignore .github and .husky.
2025-07-10 15:57:22 -07:00
github-actions[bot] 8de99c90a6 v3.18.11 Release Notes (#4814) 2025-07-10 13:49:09 -07:00
Beeandabeatrix 5b475fe88c Fix: Ensure Cline client is initialized with the latest auth token (#4813)
* Fix: Ensure Cline client is initialized with the latest auth token

The Cline client was not being re-initialized with the latest authentication token after the user signs in. This resulted in the client using an outdated or non-existent token, leading to authentication errors when making API requests.

This commit ensures that the Cline client is initialized with the most recent authentication token by setting the `apiKey` property of the `OpenAI` client instance to the current auth token retrieved from `AuthService` before every request. This guarantees that the client always uses the valid and up-to-date token for authentication.

* changeset

* move this._authService.getAuthToken() to ensureClient

---------

Co-authored-by: abeatrix <beatrix@cline.bot>
2025-07-10 13:13:30 -07:00
Beeandabeatrix 190d3bd2dc Fix: Improve auth flow and state validation (#4786)
Improves the authentication flow and state validation process.  We no longer reset the auth nounce after each sign in as AuthService is a singleton and there is no risk of nonce collision between different users as only one user can be signed in at a time.

Changes:
    - The `authNonce` is now generated once during `AuthService` instantiation and stored as a read-only property. This ensures that the nonce remains consistent throughout the authentication process.
    - The `resetAuthNonce` method has been removed, as the nonce is no longer meant to be reset.
    - The `createAuthRequest` method now uses the URL object for more graceful query construction.
- **Controller:**
    - The `validateAuthState` method has been simplified to directly compare the provided state with the stored `authNonce`.
- **Extension:**
    - The extension now prompts the user for confirmation if the state parameter in the auth callback does not match the stored `authNonce`. This allows sign-ins initiated from outside the extension (e.g., Cline web) to be handled correctly.

Issue: The issue is that the authNonce is being reset in the validateAuthState method in the Controller, but the extension.ts is directly accessing authService.authNonce without going through the validation method. This creates a race condition where:

User initiates auth, nonce is generated
Auth callback comes back with the state
If there are multiple auth attempts or the callback is processed multiple times, the nonce might be reset before the validation in extension.ts happens
User gets "Invalid auth state" error

Co-authored-by: abeatrix <beatrix@cline.bot>
2025-07-10 21:53:36 +05:30
d0069eb7cb Changeset version bump (#4785)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md for version 3.18.9 with improved descriptions

* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md and package.json for version 3.18.10

* ahugosaia'ohs'gasgh

---------

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-10 00:05:06 -07:00
pashpashpash 268bbec7f1 recommend grok 4 (#4798)
* recommend grok 4

* changeset
2025-07-09 23:42:04 -07:00
pashpashpash 192cd2602e Pashpashpash/grok 4 (#4793)
* grok 4

* numbers
2025-07-09 23:36:06 -07:00
Andrei EternalandAndrei Edell c724edd118 Replace use of vscode.fs.writeFile with node equivalent (#4790)
* Replace use of vscode.fs.writeFile with node equivalent

* format fix

---------

Co-authored-by: Andrei Edell <andrei@nugbase.com>
2025-07-09 18:11:47 -07:00
celestial-vault 0eaf350d87 Provider handler param types (#4729)
* add provider handler options types and only pass those fields when building

* field was removed in merge, remove it
2025-07-09 16:33:27 -07:00
Ara 398bc87a64 Adding Thinking Token Config to Gemini 2.5 pro model and adding gemini 2.5 flash preview (#4668)
* Adding Thinking Token Config to Gemini 2.5 pro model

* Adding Thinking Token Config to Gemini 2.5 pro model

* Adding console logs

* Adding console logs

* Adding console logs
2025-07-10 03:42:07 +05:30
0ec447c992 v3.18.9 Release Notes (#4783)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md for version 3.18.9 with improved descriptions

---------

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-09 14:44:25 -07:00
pashpashpash ba41131b36 Pashpashpash/enabling streaming cline (#4784)
* grok 4

* fixing streaming in cline provider

* removing grok promo from cline provider

* changeset

* oopsie

* oopsie
2025-07-09 14:33:55 -07:00
Beeandabeatrix 0042230acd Handle authentication errors for Cline provider (#4781)
* Fix: Cline authentication errors

This commit improves error handling for Cline authentication. It adds checks for the presence of a Cline account authentication token before making API requests. If the token is missing, it throws an "Unauthorized" error, prompting the user to sign in. Additionally, it catches `ERR_BAD_REQUEST` or 401 errors from the Cline API and throws the same "Unauthorized" error, providing a more user-friendly experience when authentication fails.

* add changeset

---------

Co-authored-by: abeatrix <beatrix@cline.bot>
2025-07-10 02:15:27 +05:30
ncrypted | Oliver 9ff705ffc0 Remove: Pricing for SAP AI Core Provider (#4778)
* remove: pricing for sapaicore

* add: model description hint regarding capacity unit pricing
2025-07-09 10:38:16 -07:00
Dennise Bartlett dea016408c Update Changelog and Versions (#4751) 2025-07-08 20:13:37 -07:00
Frostbourne bf82444aec grok 3 cache pricing (#4750) 2025-07-08 19:59:17 -07:00
988b65f1ad v3.18.7 Release Notes
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md for version 3.18.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-08 18:41:16 -07:00
Dennise Bartlettandfrostbournesb d838bcdc34 Fix account buttons (#4742)
* Update links for account buttons

* add dashboard url

* setup links for future changes

---------

Co-authored-by: frostbournesb <frostbournesb@protonmail.com>
2025-07-08 17:56:38 -07:00
pashpashpash ff1e3297a8 ending grok promo in UI (#4740)
* ending grok promo in UI

* changeset
2025-07-08 17:30:06 -07:00
github-actions[bot] 2428389620 v3.18.6 Release Notes
v3.18.6 Release Notes
2025-07-08 16:16:52 -07:00
6f8627bb5f Organization Accounts added to Extension. Refactor Auth components for Accounts
* Working organization and personal inference, account switching, and usage/credit reporting.

* Organization dropdown tweaks (#4710)

* organization dropdown tweaks

* reverted AuthService

* Update Firebase Provider and Auth Service to support re-hydration of the user credentials

* Add Github Auth Flow

* Fix merge conflicts

* Fix some dumb typing

* recreate dropdown value when initialized (#4716)

* added changeset

* Update urls to production and handle some PR concerns

* Get user credits when signing in (#4736)

* get user balance when signing in instead of when there is no active org

* swapped order of getUserCredits, made calls async

* async changes continued, moved setIsLoading to finally

---------

Co-authored-by: canvrno <46584286+canvrno@users.noreply.github.com>
Co-authored-by: pashpashpash <nik@cline.bot>
2025-07-08 15:59:08 -07:00
Beeandabeatrix 1e81d98abf Fix fresh install mode launch config (#4732)
* Fix fresh install mode launch config

Updates the launch configuration in `.vscode/launch.json` to include a temporary profile and user data directory. This fixes the issue where the launch config does not start in fresh install mode for extension development. This change prevents  interference from existing settings and extensions. The `--user-data-dir=/tmp/cline/user` argument specifies a temporary directory for user data, while `--profile-temp` ensures a clean profile is used for each launch. Also, `--sync=off` is added to disable settings sync.

* update name

* tmp dir

* Implement in-memory storage for temporary profiles

Adds in-memory storage for global state, workspace state, and secrets when running in a temporary profile. This is determined by the `TEMP_PROFILE` environment variable being set to "true". When active, the `updateGlobalState`, `getGlobalState`, `updateGlobalStateBatch`, `updateSecretsBatch`, `storeSecret`, `getSecret`, `updateWorkspaceState`, and `getWorkspaceState` functions will use `Map` objects to store and retrieve data instead of VS Code's `globalState`, `secrets`, and `workspaceState` APIs. This ensures that no data is persisted to disk when using a temporary profile, providing a clean environment for testing and development.

* Refactor tmp user directory for dev launch config

This commit refactors the temporary user directory used in the development launch configuration.

- Updates `.vscode/launch.json` to use `${workspaceFolder}/dist/tmp/user` for the `--user-data-dir` argument, ensuring the temporary profile is located within the workspace.
- Adds `TEMP_PROFILE: "true"` to the environment variables in `.vscode/launch.json` to enable in-memory storage for temporary profiles.
- Renames the `clean-sandbox` task in `.vscode/tasks.json` to `clean-tmp-user` and modifies its command to remove and recreate the `${workspaceFolder}/dist/tmp/user` directory. This ensures a clean environment for each launch.

---------

Co-authored-by: abeatrix <beatrix@cline.bot>
2025-07-08 15:05:11 -07:00
Sarah Fortune 267170920a Update the packaging script for the standalone app to include the same files as the extension in the zip (#4708)
* Package the same files for the standalone app and the extension

When packaging the standalone app use the same .vscodeignore that the vscode packager uses to decide which files to include.
Exclude extra files from the vscode extension that aren't needed: dist-standalone, old_docs and eslint-rules.

* Ignore the whole directory, not just the contents.

* Don't package .DS_Store files

* Update comment
2025-07-08 11:16:49 -07:00
Sarah Fortune 386c78c114 Update the no-vscode-postmessage eslint rule to check for any of the vscode SDK calls that have been replaced (#4715)
* Update the no-vscode-postmessage eslint rule to check for all the vscode SDK calls that have been replaced.

Expand the rule to check for all of the vscode SDK calls that have been
switched to the host bridge or replaced with native functions.

* Remove redundant messages
2025-07-08 10:50:10 -07:00
Sarah Fortune 265a56391a Replace vscode.workspace.fs.stat with fs.stat (#4714)
Use the util function that already exists isDirectory from utils/fs.ts
Use the same function in getRelativePaths
2025-07-08 10:49:56 -07:00
canvrno ff4bab22fb host bridge migration - openExternal (#4502) 2025-07-08 10:47:12 -07:00
tjandy98 bc468707a6 Add header for SAP AI Core Tracking (#4696)
* Add support for Gemini 2.5 Pro and Flash models

Signed-off-by: tjandy98 <3953059+tjandy98@users.noreply.github.com>

* Create calm-ads-glow.md

* Update request header

Signed-off-by: tjandy98 <3953059+tjandy98@users.noreply.github.com>

Add ai-client-type header for tracking

Signed-off-by: tjandy98 <3953059+tjandy98@users.noreply.github.com>

Create little-lions-joke.md

Signed-off-by: tjandy98 <3953059+tjandy98@users.noreply.github.com>

update changeset

Signed-off-by: tjandy98 <3953059+tjandy98@users.noreply.github.com>

update

Signed-off-by: tjandy98 <3953059+tjandy98@users.noreply.github.com>

---------

Signed-off-by: tjandy98 <3953059+tjandy98@users.noreply.github.com>
2025-07-07 21:09:41 -07:00
Sarah Fortuneandellipsis-dev[bot] 0a6a565d41 Replace vscode.workspace.asRelativePath with the host bridge. (#4712)
* Replace vscode.workspace.asRelativePath with the host bridge.

Add a util function asRelativePath to path.ts that does the same thing as the vcode API (returns the path relative to the workspace directory).
In the getRelativePaths protobus handler, don't allow @mentions for files outside the workspace, they do not work in cline, so just prevent them from being added at all.
If the fs.stat fails for a file, don't @mention it either, if stat() fails it means the file doesn't exist or is unreadable.

* Update src/core/controller/file/getRelativePaths.ts

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

* Update src/core/controller/file/getRelativePaths.ts

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

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-07-07 19:40:14 -07:00
d30e4d0194 v3.18.5 Release Notes (#4704)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md for version 3.18.5

---------

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: Cline Evaluation <cline@example.com>
2025-07-07 19:12:41 -07:00
AraandCline Evaluation d453eed582 feat: Globally persist plan/act mode across sessions and ensure proper workspace level persistance of chatsettings(for language) (#4646)
* feat: Persist plan/act mode across sessions

- Add mode persistence to global state storage
- Load saved mode on controller initialization
- Update state keys to include 'mode' as a valid global state key
- Ensures user's selected mode (plan/act) is maintained between VS Code sessions

* Split chat settings storage between global and workspace state
- Move mode setting to global state for cross-workspace persistence
- Store other chat settings in workspace state for project-specific configuration
- Update state retrieval logic to merge global mode with workspace settings
- Add chatSettings to LocalStateKey type definitions

* Code Cleanup

* Code Cleanup

* Get the chatsettings back to global

* Get the chatsettings back to global

* Get the chatsettings back to global

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-07-07 18:23:41 -07:00
Ara 7e32314c0b Optimize provider switching performance with batched storage operations (#4676)
* Optimize provider switching performance with batched storage operations

* Optimize provider switching performance with batched storage operations
2025-07-07 18:20:55 -07:00
Ara cce8f09ae5 Fixing bug on accepting tool response (#4675) 2025-07-07 18:19:50 -07:00
BeeandBeatrix Woo 0262e13ac4 Capture token usage for conversation turns in telementry (#4703)
Adds capture of token usage data for conversation turns in the telemetry service. This includes tokensIn, tokensOut, cacheWriteTokens, cacheReadTokens, and totalCost.

The changes involve:

- Modifying the `captureConversationTurnEvent` method in `TelemetryService.ts` to accept and include token usage data in the captured event properties.
- Updating the `Task` class in `src/core/task/index.ts` to pass token usage information when capturing conversation turn events. This ensures that token usage is tracked for both regular and cached responses.
- Refactor capture event to use object destructuring for easier readability

Co-authored-by: Beatrix Woo <beatrix@cline.bot>
2025-07-08 04:54:06 +05:30
celestial-vault 6d2cf55fc5 migrate download MCP response to proto (#4682) 2025-07-07 14:58:49 -07:00
Sarah Fortune 3577c2efa9 Remove uri service from the host bridge. (#4660)
It is being replaced with the npm module vscode-uri.
2025-07-07 14:27:19 -07:00
Sarah Fortune f97ef745d9 Replace vscode.workspace.getWorkspaceFolder() with the host bridge (#4659)
* Replace vscode.workspace.getWorkspaceFolder() with the host bridge

Use the hostbridge getWorkspacePaths() and use the result to
check for the workspaceFolder of the current file open in the IDE.

* Organize imports

* Update isLocatedInWorkspace() to check all the workspace directories, not just the first.

Add utility function to check if a path is inside a directory instead of duplicating the logic.

* Remove stubs for workspaceFolders that are not needed anymore.
2025-07-07 14:23:26 -07:00
schardosin 4e27e06670 SAP AI Core small bug fix and reorder models (#4686)
* small fix to avoid exception and removed log of returned data

* reorganized sapaicore models to be grouped in logical groups

* updated changese with changes in SAP AI Core

* removed additional received data log sections
2025-07-07 15:44:27 -05:00
ef02d6b0b2 Changeset version bump (#4685)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update package.json

---------

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: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-07-07 13:26:11 -07:00
226 changed files with 11656 additions and 3880 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Add support for Gemini 2.5 Pro and Flash to SAP AI Core Provider
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Fix logging in with cline not getting past welcome screen
@@ -0,0 +1,75 @@
# Git Diff Analysis Workflow
## Objective
Analyze the current branch's changes against main to provide informed insights and context for development decisions.
## Step 1: Gather Git Information
<important>Do not return any text or conversation other than what is necessary to run these commands</important>
**First, check the expected output size:**
```shell
(git branch --show-current && echo "=== STATUS ===" && git status --porcelain | cat && echo "=== COMMIT MESSAGES ===" && git log main..HEAD --oneline | cat && echo "=== CHANGED FILES ===" && git diff main --name-only | cat && echo "=== FULL DIFF ===" && git diff main | cat) | wc -l
```
**If the expected line count is greater than 500 lines, use the file-based approach:**
```shell
git branch --show-current > cline-git-analysis.temp && echo "=== STATUS ===" >> cline-git-analysis.temp && git status --porcelain >> cline-git-analysis.temp && echo "=== COMMIT MESSAGES ===" >> cline-git-analysis.temp && git log main..HEAD --oneline >> cline-git-analysis.temp && echo "=== CHANGED FILES ===" >> cline-git-analysis.temp && git diff main --name-only >> cline-git-analysis.temp && echo "=== FULL DIFF ===" >> cline-git-analysis.temp && git diff main >> cline-git-analysis.temp
```
Then, read the file using the read_file tool. After you have read the file but before you proceed with subsequent steps, delete it:
```shell
rm cline-git-analysis.temp
```
**If the expected line count is 500 lines or fewer, use the direct approach:**
```shell
git branch --show-current && echo "=== STATUS ===" && git status --porcelain | cat && echo "=== COMMIT MESSAGES ===" && git log main..HEAD --oneline | cat && echo "=== CHANGED FILES ===" && git diff main --name-only | cat && echo "=== FULL DIFF ===" && git diff main | cat
```
<important>If using the direct approach, pipe outputs through `cat` to avoid interactive terminals. If the user's shell is not bash/zsh, adjust the command and chaining
syntax accordingly.</important>
## Step 2: Silent, Structured Analysis Phase
- Analyze all git output without providing commentary or narration
- Read the full diff to understand the scope and nature of changes
- Identify patterns, architectural modifications, or potential impacts
- Use `read_file` to examine any related files providing additional context on the changes you have observed
## Step 3: Context Gathering
- Analyze related code without providing commentary or narration
- Read relevant related source files if needed for complete understanding
- Check dependencies, imports, or cross-references spanning the changes
- Understand the broader codebase context around modifications
- This additional context gathering should include related backend code, as well as related ui/frontend code
- You will typically need to analyze at least several files, potentially many, in order to fully complete this step
- You should not continue reading additional context if you have exhausted more than 60% of your available context window
- If you have exhausted less than 40% of your context window, you should continue reviewing additional context
## Step 4: Ready for User Interaction
**Only after completing the full analysis:**
- Engage with the user based on comprehensive understanding
- Provide insights about specific modifications and their impacts
- If you are certain they exist, note potential breaking changes or compatibility issues
- Answer questions with informed context from the complete change set and context gathering
- If the user has not provided a question, or the question is insufficient to provide a quality response, ask brief (one sentence) clarifying questions.
- Only offer recommendations if they are applicable to the user's request and relevant to the changes that you have observed
## Key Rules
- **No prose or conversation during git research phase**
- **No prose or conversation during context gathering phase**
- **Complete all analysis before any user interaction**
- **Use gathered information for all subsequent questions and insights**
- **Focus on understanding the complete picture before discussing**
## Optional: Additional Analysis Commands
For deeper investigation when needed:
```shell
# Detailed commit history with author info
git log main..HEAD --format="%h %s (%an)" | cat
# Change statistics
git diff main --stat | cat
# Specific file type changes
git diff main --name-only | grep -E '\.(ts|js|tsx|jsx|py|md)$' | cat
+1
View File
@@ -22,6 +22,7 @@
"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",
{
+88
View File
@@ -0,0 +1,88 @@
name: E2E Tests
on:
push:
branches:
- main
pull_request:
types: [opened, reopened, synchronize, ready_for_review]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
matrix_prep:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- id: set-matrix
run: |
echo 'matrix=[{"runner":"ubuntu"},{"runner":"windows"},{"runner":"macos"}]' >> $GITHUB_OUTPUT
e2e:
needs: matrix_prep
strategy:
fail-fast: false
matrix:
include: ${{ fromJson(needs.matrix_prep.outputs.matrix) }}
runs-on: ${{ matrix.runner }}-latest
timeout-minutes: 20
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: 22
# Cache root dependencies - only reuse if package-lock.json exactly matches
- name: Cache root dependencies
uses: actions/cache@v4
id: root-cache
with:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
# Cache webview-ui dependencies - only reuse if package-lock.json exactly matches
- name: Cache webview-ui dependencies
uses: actions/cache@v4
id: webview-cache
with:
path: webview-ui/node_modules
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
- name: Install root dependencies
if: steps.root-cache.outputs.cache-hit != 'true'
run: npm ci
- name: Install webview-ui dependencies
if: steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci
- name: Install xvfb on Linux
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
- name: Run E2E tests - Linux
if: matrix.runner == 'ubuntu'
run: xvfb-run -a npm run test:e2e
- name: Run E2E tests - Non-Linux
if: matrix.runner != 'ubuntu'
run: npm run test:e2e
- uses: actions/upload-artifact@v4
if: ${{ failure() }}
with:
name: playwright-recordings-${{ matrix.runner }}
path: |
test-results/playwright/
+4
View File
@@ -68,6 +68,10 @@ jobs:
if: steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci
- name: Install xvfb on Linux
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y xvfb
- name: Install local modules on windows
if: runner.os == 'Windows' && steps.root-cache.outputs.cache-hit == 'true'
run: |
+3 -3
View File
@@ -34,8 +34,8 @@ src/shared/proto/host/*.ts
# Webview
webview-ui/src/services/grpc-client.ts
# Host bridge
src/hosts/vscode/*/methods.ts
src/hosts/vscode/*/index.ts
src/hosts/vscode/client/host-grpc-client.ts
src/hosts/vscode/host-grpc-service-config.ts
src/standalone/server-setup.ts
# E2E Tests
test-results
+1 -1
View File
@@ -2,7 +2,7 @@ import { defineConfig } from "@vscode/test-cli"
import path from "path"
export default defineConfig({
files: "{out/**/*.test.js,src/**/*.test.js}",
files: "{out/**/*.test.js,src/**/*.test.js,!src/test/e2e/**/*.test.js,!out/src/test/e2e/**/*.test.js}",
mocha: {
ui: "bdd",
timeout: 20000, // Maximum time (in ms) that a test can run before failing
+4 -3
View File
@@ -23,19 +23,20 @@
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--user-data-dir=${workspaceFolder}/dist/tmp/user",
"--profile-temp",
"--sync",
"off",
"--sync=off",
"--disable-extensions",
"--extensionDevelopmentPath=${workspaceFolder}",
"${workspaceFolder}"
],
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
"preLaunchTask": "clean-sandbox",
"preLaunchTask": "clean-tmp-user",
"internalConsoleOptions": "openOnSessionStart",
"postDebugTask": "stop",
"env": {
"IS_DEV": "true",
"TEMP_PROFILE": "true",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}"
}
},
+2 -2
View File
@@ -233,10 +233,10 @@
"type": "shell"
},
{
"label": "clean-sandbox",
"label": "clean-tmp-user",
"type": "shell",
"dependsOn": ["watch"],
"command": "rm -rf .vscode-dev"
"command": "rm -rf ${workspaceFolder}/dist/tmp/user && mkdir -p ${workspaceFolder}/dist/tmp/user"
}
],
"inputs": [
+26 -4
View File
@@ -1,24 +1,40 @@
# Default
.vscode/**
.vscode-test/**
out/**
node_modules/**
out/
dist-standalone/
node_modules/
src/**
standalone/**
.gitignore
.yarnrc
esbuild.js
vsc-extension-quickstart.md
**/tsconfig.json
tsconfig*.json
**/.eslintrc.json
**/*.map
**/*.ts
**/.vscode-test.*
eslint-rules/**
.github/**
.husky/**
# Custom
demo.gif
**/demo.gif
.nvmrc
.gitattributes
.prettierignore
.husky/
.github/
eslint-rules/
old_docs/
evals/
.changie.yaml
.codespellrc
.mocharc.json
buf.yaml
.changeset/
.clinerules/
# Ignore all webview-ui files except the build directory (https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/frameworks/hello-world-react-cra/.vscodeignore)
webview-ui/src/**
@@ -32,6 +48,7 @@ webview-ui/node_modules/**
# Ignore docs
docs/**
old_docs/**
# Fix issue where codicons don't get packaged (https://github.com/microsoft/vscode-extension-samples/issues/692)
!node_modules/@vscode/codicons/dist/codicon.css
@@ -42,3 +59,8 @@ docs/**
# Include icons
!assets/icons/**
# Ignore E2E build files
e2e-build.js
e2e.vsix
test-results/
+81
View File
@@ -1,5 +1,86 @@
# Changelog
## [3.19.5]
- Add Groq as a new API provider with support for all Groq models including Kimi-K2
- Add user role display in organization UI for Cline account users
- Fix message dialogs not showing option buttons properly
- Fix authentication issues when using multiple VSCode windows
## [3.19.4]
- Add ability to choose Chinese endpoint for Moonshot provider
## [3.19.3]
- Add Moonshot AI provider
## [3.19.2]
- Show request ID in error messages returned by Cline Accounts API to help debug user reported issues
## [3.19.1]
- Fix documentation
## [3.19.0]
- Add Kimi-K2 as a recommended model in the Cline Provider, and route to Together/Groq for 131k context window and high throughput
- Added API Key support for Bedrock integration
## [3.18.14]
- Fix bug where Cline account users logged in with invalid token would not be shown as logged out in webview presentation layer
## [3.18.13]
- Fix authentication issue where Cline accounts users would keep getting logged out or seeing 'Unexpected API response' errors
## [3.18.12]
- Fix flaky organization switching behavior in Cline provider that caused UI inconsistencies and double loading
- Fix insufficient credits error display to properly show error messages when account balance is too low
- Improve credit balance validation and error handling for Cline provider requests
## [3.18.11]
- Fix authentication issues with Cline provider by ensuring the client always uses the latest auth token
## [3.18.10]
- Update recommended fast & cheap model to Grok 4 in OpenRouter model picker
- Fix Gemini 2.5 Pro thinking budget slider and add support for Gemini 2.5 Flash Lite Preview model (Thanks @arafatkatze!)
## [3.18.9]
- Fix streaming reliability issues with Cline provider that could cause connection problems during long conversations
- Fix authentication error handling for Cline provider to show clearer error messages when not signed in and prevent recursive failed requests
- Remove incorrect pricing display for SAP AI Core provider since it uses non-USD "Capacity Units" that cannot be directly converted (Thanks @ncryptedV1!)
## [3.18.8]
- Update pricing for Grok 3 model because the promotion ended
## [3.18.7]
- Remove promotional "free" messaging for Grok 3 model in UI
## [3.18.6]
- Update request header to include `"ai-client-type": "Cline"` to SAP Api Provider
- Add organization accounts
## [3.18.5]
- Fix Plan/Act mode persistence across sessions and multi-workspace conflicts
- Improve provider switching performance by 18x (from 550ms to 30ms) with batched storage operations
- Improve SAP AI Core provider model organization and fix exception handling (Thanks @schardosin!)
## [3.18.4]
- Add support for Gemini 2.5 Pro and Flash to SAP AI Core Provider
- Fix logging in with Cline account not getting past welcome screen
## [3.18.3]
- Improve Cerebras Qwen model performance by removing thinking tokens from model input (Thanks @kevint-cerebras!)
+1 -1
View File
@@ -51,7 +51,7 @@ Thanks to [Claude 3.7 Sonnet's agentic coding capabilities](https://www.anthrop
### Use any API and Model
Cline supports API providers like OpenRouter, Anthropic, OpenAI, Google Gemini, AWS Bedrock, Azure, GCP Vertex, and Cerebras. You can also configure any OpenAI compatible API, or use a local model through LM Studio/Ollama. If you're using OpenRouter, the extension fetches their latest model list, allowing you to use the newest models as soon as they're available.
Cline supports API providers like OpenRouter, Anthropic, OpenAI, Google Gemini, AWS Bedrock, Azure, GCP Vertex, Cerebras and Groq. You can also configure any OpenAI compatible API, or use a local model through LM Studio/Ollama. If you're using OpenRouter, the extension fetches their latest model list, allowing you to use the newest models as soon as they're available.
The extension also keeps track of total tokens and API usage cost for the entire task loop and individual requests, keeping you informed of spend every step of the way.
+1
View File
@@ -147,6 +147,7 @@
"pages": [
"provider-config/anthropic",
"provider-config/claude-code",
"provider-config/aws-bedrock-with-apikey-authentication",
"provider-config/aws-bedrock-with-credentials-authentication",
"provider-config/aws-bedrock-with-profile-authentication",
"provider-config/gcp-vertex-ai",
@@ -14,6 +14,8 @@ Certain scenarios may warrant using local models, including handling highly sens
#### [IAM Security Best Practices](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html) (For administrators)
#### [AWS Bedrock setup for API Keys](/provider-config/aws-bedrock-with-apikey-authentication)
#### [AWS Bedrock setup for Legacy IAM (AWS Credentials)](/provider-config/aws-bedrock-with-credentials-authentication)
#### [AWS Bedrock setup for SSO token (AWS Profile)](/provider-config/aws-bedrock-with-profile-authentication)
@@ -0,0 +1,135 @@
---
title: "AWS Bedrock"
description: "Learn how to set up AWS Bedrock with Cline using credentials authentication. This guide covers AWS environment setup, regional access verification, and secure integration with the Cline VS Code extension."
---
### Overview
- **AWS Bedrock:** A fully managed service that offers access to leading generative AI models (e.g., Anthropic Claude, Amazon Nova) through AWS.\
[Learn more about AWS Bedrock](https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html).
- **Cline:** A VS Code extension that acts as a coding assistant by integrating with AI models—empowering developers to generate code, debug, and analyze data.
- **Developer Focus:** This guide is tailored for individual developers that want to enable access to frontier models via AWS Bedrock with a simplified setup using API Keys.
---
### Step 1: Prepare Your AWS Environment
#### 1.1 Individual user setup - Create a Bedrock API Key
For more detailed instructions check the [documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/api-keys.html).
1. **Sign in to the AWS Management Console:**\
[AWS Console](https://aws.amazon.com/console/)
2. **Access Bedrock Console:**
- [Bedrock Console](https://console.aws.amazon.com/bedrock)
- Create a new Long Lived API Key. This API Key will have by default the `AmazonBedrockLimitedAccess` IAM policy
[View AmazonBedrockLimitedAccess Policy Details](https://docs.aws.amazon.com/bedrock/latest/userguide/security-iam.html)
#### 1.2 Create or Modify the Policy
To ensure Cline can interact with AWS Bedrock, your IAM user or role needs specific permissions. While the `AmazonBedrockLimitedAccess` managed policy provides comprehensive access, for a more restricted and secure setup adhering to the principle of least privilege, the following minimal permissions are sufficient for Cline's core model invocation functionality:
- `bedrock:InvokeModel`
- `bedrock:InvokeModelWithResponseStream`
- `bedrock:CallWithBearerToken`
You can create a custom IAM policy with these permissions and attach it to your IAM user or role.
1. In the AWS IAM console, create a new policy.
2. Use the JSON editor to add the following policy document:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream", "bedrock:CallWithBearerToken"],
"Resource": "*" // For enhanced security, scope this to specific model ARNs if possible.
}
]
}
```
3. Name the policy (e.g., `ClineBedrockInvokeAccess`) and attach it to the IAM user associated with the key you created. The IAM user and the API key have the same prefix.
**Important Considerations:**
- **Model Listing in Cline:** The minimal permissions (`bedrock:InvokeModel`, `bedrock:InvokeModelWithResponseStream`) are sufficient for Cline to _use_ a model if you specify the model ID directly in Cline's settings. If you rely on Cline to dynamically list available Bedrock models, you might need additional permissions like `bedrock:ListFoundationModels`.
- **AWS Marketplace Subscriptions:** For third-party models (e.g., Anthropic Claude), the **`AmazonBedrockLimitedAccess`** policy grants you the necessary permissions to subscribe via the AWS Marketplace. There is no explicit access to be enabled. For Anthropic models you are still required to submit a First Time Use (FTU) form via the Console. If you get the following message in the Cline chat `[ERROR] Failed to process response: Model use case details have not been submitted for this account. Fill out the Anthropic use case details form before using the model.` then open the [Playground in the AWS Bedrock Console](https://console.aws.amazon.com/bedrock/home?#/text-generation-playground), select any Anthropic model and fill in the form (you might need to send a prompt first)
---
### Step 2: Verify Regional and Model Access
#### 2.1 Choose and Confirm a Region
1. **Select a Region:**\
AWS Bedrock is available in multiple regions (e.g., US East, Europe, Asia Pacific). Choose the region that meets your latency and compliance needs.\
[AWS Global Infrastructure](https://aws.amazon.com/about-aws/global-infrastructure/regions_az/)
2. **Verify Model Access:**
- **Note:** Some models are only accessible via an [Inference Profile](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html). In such case check the box "Cross Region Inference".
---
### Step 3: Configure the Cline VS Code Extension
#### 3.1 Install and Open Cline
1. **Install VS Code:**\
Download from the [VS Code website](https://code.visualstudio.com/).
2. **Install the Cline Extension:**
- Open VS Code.
- Go to the Extensions Marketplace (`Ctrl+Shift+X` or `Cmd+Shift+X`).
- Search for **Cline** and install it.
#### 3.2 Configure Cline Settings
1. **Open Cline Settings:**
- Click on the settings ⚙️ to select your API Provider.
2. **Select AWS Bedrock as the API Provider:**
- From the API Provider dropdown, choose **AWS Bedrock**.
3. **Enter Your AWS API Key:**
- Input your **API Key**
- Specify the correct **AWS Region** (e.g., `us-east-1` or your enterprise-approved region).
4. **Select a Model:**
- Choose an on-demand model (e.g., **anthropic.claude-3-5-sonnet-20241022-v2:0**).
5. **Save and Test:**
- Click **Done/Save** to apply your settings.
- Test the integration by sending a simple prompt (e.g., "Generate a Python function to check if a number is prime.").
---
### Step 4: Security, Monitoring, and Best Practices
1. **Secure Access:**
- Prefer AWS SSO/federated roles over long-lived API Key when possible.
- [AWS IAM Best Practices](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html)
2. **Enhance Network Security:**
- Consider setting up [AWS PrivateLink](https://docs.aws.amazon.com/vpc/latest/userguide/endpoint-services-overview.html) to securely connect to Bedrock.
3. **Monitor and Log Activity:**
- Enable AWS CloudTrail to log Bedrock API calls.
- Use CloudWatch to monitor metrics like invocation count, latency, and token usage.
- Set up alerts for abnormal activity.
4. **Handle Errors and Manage Costs:**
- Implement exponential backoff for throttling errors.
- Use AWS Cost Explorer and set billing alerts to track usage.\
[AWS Cost Management](https://docs.aws.amazon.com/cost-management/latest/userguide/what-is-aws-cost-management.html)
5. **Regular Audits and Compliance:**
- Periodically review IAM roles and CloudTrail logs.
- Follow internal data privacy and governance policies.
---
### Conclusion
By following these steps, your enterprise team can securely integrate AWS Bedrock with the Cline VS Code extension to accelerate development:
1. **Prepare Your AWS Environment:** Create or use a secure IAM role/user, attach the `AmazonBedrockLimitedAccess` policy, and ensure necessary permissions.
2. **Verify Region and Model Access:** Confirm that your selected region supports your required models.
3. **Configure Cline in VS Code:** Install and set up Cline with your AWS credentials and choose an appropriate model.
4. **Implement Security and Monitoring:** Use best practices for IAM, network security, monitoring, and cost management.
For further details, consult the [AWS Bedrock Documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html) and coordinate with your internal cloud team. Happy coding!
---
_This guide will be updated as AWS Bedrock and Cline evolve. Always refer to the latest documentation and internal policies for up-to-date practices._
@@ -5,7 +5,7 @@ description: "Learn how to set up AWS Bedrock with Cline using credentials authe
### Overview
- **AWS Bedrock:** A fully managed service that offers access to leading generative AI models (e.g., Anthropic Claude, Amazon Titan) through AWS.\
- **AWS Bedrock:** A fully managed service that offers access to leading generative AI models (e.g., Anthropic Claude, Amazon Nova) through AWS.\
[Learn more about AWS Bedrock](https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html).
- **Cline:** A VS Code extension that acts as a coding assistant by integrating with AI models—empowering developers to generate code, debug, and analyze data.
- **Enterprise Focus:** This guide is tailored for organizations with established AWS environments (using IAM roles, AWS SSO, AWS Organizations, etc.) to ensure secure and compliant usage.
@@ -25,7 +25,7 @@ description: "Learn how to set up AWS Bedrock with Cline using credentials authe
#### 1.2 Attach the Required Policies
To ensure Cline can interact with AWS Bedrock, your IAM user or role needs specific permissions. While the `AmazonBedrockFullAccess` managed policy provides comprehensive access, for a more restricted and secure setup adhering to the principle of least privilege, the following minimal permissions are sufficient for Cline's core model invocation functionality:
To ensure Cline can interact with AWS Bedrock, your IAM user or role needs specific permissions. While the `AmazonBedrockLimitedAccess` managed policy provides comprehensive access, for a more restricted and secure setup adhering to the principle of least privilege, the following minimal permissions are sufficient for Cline's core model invocation functionality:
- `bedrock:InvokeModel`
- `bedrock:InvokeModelWithResponseStream`
@@ -52,8 +52,8 @@ You can create a custom IAM policy with these permissions and attach it to your
**Option 2: Using a Managed Policy (Simpler Initial Setup)**
- Alternatively, you can attach the AWS managed policy **`AmazonBedrockFullAccess`**. This grants broader permissions, including the ability to list models, manage provisioning, and other Bedrock features. This might be simpler for initial setup or if you require these wider capabilities.
[View AmazonBedrockFullAccess Policy Details](https://docs.aws.amazon.com/bedrock/latest/userguide/security-iam.html)
- Alternatively, you can attach the AWS managed policy **`AmazonBedrockLimitedAccess`**. This grants broader permissions, including the ability to list models, manage provisioning, and other Bedrock features. This might be simpler for initial setup or if you require these wider capabilities.
[View AmazonBedrockLimitedAccess Policy Details](https://docs.aws.amazon.com/bedrock/latest/userguide/security-iam.html)
**Important Considerations:**
@@ -71,8 +71,8 @@ You can create a custom IAM policy with these permissions and attach it to your
AWS Bedrock is available in multiple regions (e.g., US East, Europe, Asia Pacific). Choose the region that meets your latency and compliance needs.\
[AWS Global Infrastructure](https://aws.amazon.com/about-aws/global-infrastructure/regions_az/)
2. **Verify Model Access:**
- In the AWS Bedrock console, confirm that the models your team requires (e.g., Anthropic Claude, Amazon Titan) are marked as "Access granted."
- **Note:** Some advanced models might require an [Inference Profile](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-prereq.html) if not available on-demand.
- In the AWS Bedrock console, confirm that the models your team requires (e.g., Anthropic Claude, Amazon Nova) are marked as "Access granted."
- **Note:** Some advanced models might require an [Inference Profile](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html) if not available on-demand.
#### 2.2 Set Up AWS Marketplace Subscriptions (if needed)
@@ -138,7 +138,7 @@ You can create a custom IAM policy with these permissions and attach it to your
By following these steps, your enterprise team can securely integrate AWS Bedrock with the Cline VS Code extension to accelerate development:
1. **Prepare Your AWS Environment:** Create or use a secure IAM role/user, attach the `AmazonBedrockFullAccess` policy, and ensure necessary permissions.
1. **Prepare Your AWS Environment:** Create or use a secure IAM role/user, attach the `AmazonBedrockLimitedAccess` policy, and ensure necessary permissions.
2. **Verify Region and Model Access:** Confirm that your selected region supports your required models and subscribe via AWS Marketplace if needed.
3. **Configure Cline in VS Code:** Install and set up Cline with your AWS credentials and choose an appropriate model.
4. **Implement Security and Monitoring:** Use best practices for IAM, network security, monitoring, and cost management.
+2 -2
View File
@@ -1,11 +1,11 @@
---
title: "Claude Code"
description: "Use your Claude Max subscription with Cline instead of paying per token. Learn how to set up and configure the Claude Code provider."
description: "Use your Claude Max or Pro subscription with Cline instead of paying per token. Learn how to set up and configure the Claude Code provider."
---
**Website:** [https://docs.anthropic.com/en/docs/claude-code/setup](https://docs.anthropic.com/en/docs/claude-code/setup)
The Claude Code provider lets you use your existing Claude subscription with Cline. If you have Claude Max, this means you can use Claude in Cline without paying extra API costs.
The Claude Code provider lets you use your existing Claude subscription with Cline. If you have Claude Max or Pro, this means you can use Claude in Cline without paying extra API costs.
<Frame>
<img
+14 -3
View File
@@ -5,6 +5,7 @@ const path = require("path")
const production = process.argv.includes("--production")
const watch = process.argv.includes("--watch")
const standalone = process.argv.includes("--standalone")
const e2eBuild = process.argv.includes("--e2e-build")
const destDir = standalone ? "dist-standalone" : "dist"
/**
@@ -153,15 +154,25 @@ const extensionConfig = {
// Standalone-specific configuration
const standaloneConfig = {
...baseConfig,
entryPoints: ["src/standalone/standalone.ts"],
outfile: `${destDir}/standalone.js`,
entryPoints: ["src/standalone/cline-core.ts"],
outfile: `${destDir}/cline-core.js`,
// These gRPC protos need to load files from the module directory at runtime,
// so they cannot be bundled.
external: ["vscode", "@grpc/reflection", "grpc-health-check"],
}
// E2E build script configuration
const e2eBuildConfig = {
...baseConfig,
entryPoints: ["src/test/e2e/utils/build.ts"],
outfile: `${destDir}/e2e-build.js`,
external: ["@vscode/test-electron", "execa"],
sourcemap: false,
plugins: [aliasResolverPlugin, esbuildProblemMatcherPlugin],
}
async function main() {
const config = standalone ? standaloneConfig : extensionConfig
const config = standalone ? standaloneConfig : e2eBuild ? e2eBuildConfig : extensionConfig
const extensionCtx = await esbuild.context(config)
if (watch) {
await extensionCtx.watch()
@@ -0,0 +1,123 @@
const { RuleTester: DirectApiRuleTester } = require("eslint")
const noDirectVscodeApiRule = require("../no-direct-vscode-api")
const directApiRuleTester = new DirectApiRuleTester({
parser: require.resolve("@typescript-eslint/parser"),
parserOptions: {
ecmaVersion: 2020,
sourceType: "module",
ecmaFeatures: {
jsx: true,
},
},
})
directApiRuleTester.run("no-direct-vscode-api", noDirectVscodeApiRule, {
valid: [
// Should allow vscode.postMessage in grpc-client-base.ts
{
code: `vscode.postMessage({ type: "grpc_request", data: {} })`,
filename: "grpc-client-base.ts",
},
{
code: `vscode.postMessage({ type: "grpc_request_cancel" })`,
filename: "/path/to/grpc-client-base.ts",
},
// Should allow in exception directories
{
code: `vscode.workspace.workspaceFolders`,
filename: "/src/hosts/vscode/host-bridge.ts",
},
{
code: `vscode.workspace.fs.stat(uri)`,
filename: "/standalone/runtime-files/helpers.ts",
},
// Should allow other vscode API calls
{
code: `vscode.window.showInformationMessage("Hello")`,
filename: "test.ts",
},
// Should allow postMessage calls on other objects
{
code: `window.postMessage({ type: "test" }, "*")`,
filename: "test.ts",
},
// Should allow variables named vscode but not calling postMessage
{
code: `const vscode = { other: "method" }; vscode.other()`,
filename: "test.ts",
},
],
invalid: [
// Should disallow vscode.postMessage in regular files
{
code: `vscode.postMessage({ type: "test", data: {} })`,
filename: "test.ts",
errors: [
{
messageId: "useGrpcClient",
},
],
},
// Should disallow vscode.postMessage in components
{
code: `vscode.postMessage({ type: "apiConfiguration", apiConfiguration })`,
filename: "ApiOptions.tsx",
errors: [
{
messageId: "useGrpcClient",
},
],
},
// Should disallow vscode.postMessage in test files
{
code: `vscode.postMessage({ type: "newTask", text: message.text })`,
filename: "test.test.ts",
errors: [
{
messageId: "useGrpcClient",
},
],
},
// Should disallow property access for disallowed APIs
{
code: `const folders = vscode.workspace.workspaceFolders;`,
filename: "workspace.ts",
errors: [
{
messageId: "useHostBridge",
},
],
},
// Should disallow method calls for disallowed APIs
{
code: `const relativePath = vscode.workspace.asRelativePath(filePath);`,
filename: "path-utils.ts",
errors: [
{
messageId: "usePathUtils",
},
],
},
// Should disallow nested property access
{
code: `const stats = await vscode.workspace.fs.stat(uri);`,
filename: "file-utils.ts",
errors: [
{
messageId: "useFsUtils",
},
],
},
// Should disallow getting a workspace folder
{
code: `const folder = vscode.workspace.getWorkspaceFolder(uri);`,
filename: "path-helper.ts",
errors: [
{
messageId: "usePathUtils",
},
],
},
],
})
@@ -1,74 +0,0 @@
const { RuleTester: VscodeRuleTester } = require("eslint")
const vscodePostmessageRule = require("../no-vscode-postmessage")
const vscodeRuleTester = new VscodeRuleTester({
parser: require.resolve("@typescript-eslint/parser"),
parserOptions: {
ecmaVersion: 2020,
sourceType: "module",
ecmaFeatures: {
jsx: true,
},
},
})
vscodeRuleTester.run("no-vscode-postmessage", vscodePostmessageRule, {
valid: [
// Should allow vscode.postMessage in grpc-client-base.ts
{
code: `vscode.postMessage({ type: "grpc_request", data: {} })`,
filename: "grpc-client-base.ts",
},
{
code: `vscode.postMessage({ type: "grpc_request_cancel" })`,
filename: "/path/to/grpc-client-base.ts",
},
// Should allow other vscode API calls
{
code: `vscode.window.showInformationMessage("Hello")`,
filename: "test.ts",
},
// Should allow postMessage calls on other objects
{
code: `window.postMessage({ type: "test" }, "*")`,
filename: "test.ts",
},
// Should allow variables named vscode but not calling postMessage
{
code: `const vscode = { other: "method" }; vscode.other()`,
filename: "test.ts",
},
],
invalid: [
// Should ban vscode.postMessage in regular files
{
code: `vscode.postMessage({ type: "test", data: {} })`,
filename: "test.ts",
errors: [
{
messageId: "useGrpcClient",
},
],
},
// Should ban vscode.postMessage in components
{
code: `vscode.postMessage({ type: "apiConfiguration", apiConfiguration })`,
filename: "ApiOptions.tsx",
errors: [
{
messageId: "useGrpcClient",
},
],
},
// Should ban vscode.postMessage in test files
{
code: `vscode.postMessage({ type: "newTask", text: message.text })`,
filename: "test.test.ts",
errors: [
{
messageId: "useGrpcClient",
},
],
},
],
})
+3 -3
View File
@@ -1,13 +1,13 @@
// eslint-rules/index.js
const noProtobufObjectLiterals = require("./no-protobuf-object-literals")
const noGrpcClientObjectLiterals = require("./no-grpc-client-object-literals")
const noVscodePostmessage = require("./no-vscode-postmessage")
const noDirectVscodeApi = require("./no-direct-vscode-api")
module.exports = {
rules: {
"no-protobuf-object-literals": noProtobufObjectLiterals,
"no-grpc-client-object-literals": noGrpcClientObjectLiterals,
"no-vscode-postmessage": noVscodePostmessage,
"no-direct-vscode-api": noDirectVscodeApi,
},
configs: {
recommended: {
@@ -15,7 +15,7 @@ module.exports = {
rules: {
"local/no-protobuf-object-literals": "error",
"local/no-grpc-client-object-literals": "error",
"local/no-vscode-postmessage": "error",
"local/no-direct-vscode-api": "warn",
},
},
},
+164
View File
@@ -0,0 +1,164 @@
const { ESLintUtils } = require("@typescript-eslint/utils")
const path = require("path")
const createRule = ESLintUtils.RuleCreator((name) => `https://cline.bot/eslint-rules/${name}`)
// Configuration of disallowed VSCode APIs and their recommended alternatives
const disallowedApis = {
"vscode.postMessage": {
messageId: "useGrpcClient",
},
"vscode.workspace.fs.stat": {
messageId: "useFsUtils",
},
"vscode.workspace.workspaceFolders": {
messageId: "useHostBridge",
},
"vscode.workspace.asRelativePath": {
messageId: "usePathUtils",
},
"vscode.workspace.getWorkspaceFolder": {
messageId: "usePathUtils",
},
}
module.exports = createRule({
name: "no-direct-vscode-api",
meta: {
type: "problem",
docs: {
description:
"Disallow direct VSCode API usage in favor of Cline's abstraction layers, except in src/hosts/vscode and standalone/runtime-files directories",
recommended: "error",
},
messages: {
useGrpcClient:
"Use gRPC service clients instead of vscode.postMessage().\n" +
"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" +
"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" +
"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" +
"Found: {{code}}",
},
schema: [],
},
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) {
return
}
// For handling nested properties like vscode.workspace.fs.stat
function getFullPropertyPath(node) {
if (node.type !== "MemberExpression") {
return node.name || ""
}
const objectPart = getFullPropertyPath(node.object)
const propertyPart = node.property.name || ""
return objectPart ? `${objectPart}.${propertyPart}` : propertyPart
}
// Check if the expression matches one of our disallowed patterns
if (node.object && node.object.type === "Identifier" && node.object.name === "vscode") {
const fullPath = `vscode.${node.property.name}`
checkDisallowedApi(fullPath, node)
}
// Handle nested expressions like vscode.workspace.fs.stat
else if (node.object && node.object.type === "MemberExpression") {
const fullPath = getFullPropertyPath(node)
// Only proceed if it starts with vscode
if (fullPath.startsWith("vscode.")) {
checkDisallowedApi(fullPath, node)
}
}
}
// Check if an expression matches a disallowed API and report if it does
function checkDisallowedApi(expressionPath, node) {
// Check exact matches
if (disallowedApis[expressionPath]) {
reportViolation(expressionPath, node)
return
}
// Check prefix matches (for nested properties)
for (const disallowedApi in disallowedApis) {
// For direct property access like vscode.workspace.workspaceFolders
if (expressionPath === disallowedApi) {
reportViolation(disallowedApi, node)
return
}
// For method calls like vscode.workspace.asRelativePath(...)
if (expressionPath.startsWith(`${disallowedApi}.`) || expressionPath.startsWith(`${disallowedApi}(`)) {
reportViolation(disallowedApi, node)
return
}
}
}
// Report a violation with the appropriate message
function reportViolation(disallowedApi, node) {
const sourceCode = context.sourceCode
const config = disallowedApis[disallowedApi]
// For method calls, get the whole call expression
let reportNode = node
let parentNode = sourceCode.getAncestors(node).pop()
if (parentNode && parentNode.type === "CallExpression" && parentNode.callee === node) {
reportNode = parentNode
}
const callText = sourceCode.getText(reportNode).trim()
context.report({
node: reportNode,
messageId: config.messageId,
data: {
code: callText,
},
})
}
return {
// Detect basic member expressions (e.g., vscode.postMessage)
MemberExpression(node) {
checkMemberExpression(node)
},
// 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) {
return
}
// Destructuring pattern checks removed as developers don't use the API this way
// They always use direct imports: import * as vscode from "vscode" and direct access: vscode.thing.foo
},
}
},
})
-61
View File
@@ -1,61 +0,0 @@
const { ESLintUtils } = require("@typescript-eslint/utils")
const path = require("path")
const createRule = ESLintUtils.RuleCreator((name) => `https://cline.bot/eslint-rules/${name}`)
module.exports = createRule({
name: "no-vscode-postmessage",
meta: {
type: "problem",
docs: {
description: "Ban vscode.postMessage() calls in favor of gRPC service clients, except in grpc-client-base.ts",
recommended: "error",
},
messages: {
useGrpcClient:
"Use gRPC service clients instead of vscode.postMessage().\n" +
"Example: AccountServiceClient.methodName(RequestType.create({...})) instead of vscode.postMessage({type: '...'}).\n" +
"Found: {{code}}",
},
schema: [],
},
defaultOptions: [],
create(context) {
// Check if current file is grpc-client-base.ts (exception case)
const filename = context.filename
const isGrpcClientBase = path.basename(filename) === "grpc-client-base.ts"
return {
// Detect vscode.postMessage calls
"CallExpression[callee.type='MemberExpression']"(node) {
// Skip if this is grpc-client-base.ts
if (isGrpcClientBase) {
return
}
const callee = node.callee
// Check for vscode.postMessage pattern
if (
callee.object &&
callee.object.type === "Identifier" &&
callee.object.name === "vscode" &&
callee.property &&
callee.property.name === "postMessage"
) {
const sourceCode = context.sourceCode
const callText = sourceCode.getText(node).trim()
context.report({
node,
messageId: "useGrpcClient",
data: {
code: callText,
},
})
}
},
}
},
})
+1 -1
View File
@@ -36,7 +36,7 @@ It starts with our test cases. Each one is a JSON file in `./cases` that has the
Then, for every test run, we set up a specific configuration. This includes which LLM we're testing, which system prompt it gets, which function we use to parse the model's raw output, and which function we use to actually apply the diff. Here's the command I've been using:
```bash
npm run diff-eval -- --model-ids "anthropic/claude-3-5-sonnet-20241022,x-ai/grok-3-beta" --max-cases 4 --valid-attempts-per-case 2 --verbose --parallel
npm run diff-eval -- --model-ids "anthropic/claude-3-5-sonnet,x-ai/grok-3-beta,anthropic/claude-3.7-sonnet,anthropic/claude-sonnet-4,google/gemini-2.5-pro-preview,google/gemini-2.5-flash" --max-cases 5 --valid-attempts-per-case 5 --parallel --diff-edit-function diff-06-26-25 --verbose
```
This will build the eval script, run it, and then open the streamlit dashboard to show the results.
+4657 -1395
View File
File diff suppressed because it is too large Load Diff
+12 -6
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.18.3",
"version": "3.19.5",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -330,12 +330,12 @@
"watch:esbuild": "node esbuild.js --watch",
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
"package": "npm run check-types && npm run build:webview && npm run lint && node esbuild.js --production",
"protos": "node proto/build-proto.js && node scripts/generate-server-setup.mjs && node scripts/generate-host-bridge-client.mjs",
"protos": "node scripts/build-proto.mjs && node scripts/generate-server-setup.mjs && node scripts/generate-host-bridge-client.mjs",
"postprotos": "prettier src/shared/proto src/core/controller src/hosts/ webview-ui/src/services src/generated --write --log-level warn",
"clean": "rimraf dist dist-standalone webview-ui/build src/generated out/",
"compile-tests": "node ./scripts/build-tests.js",
"watch-tests": "tsc -p . -w --outDir out",
"check-types": "npm run protos && tsc --noEmit",
"check-types": "npm run protos && npx tsc --noEmit && cd webview-ui && npx tsc -b --noEmit",
"lint": "eslint src --ext ts && eslint webview-ui/src --ext ts && buf lint && cd webview-ui && npm run lint",
"format": "prettier . --check",
"format:fix": "prettier . --write",
@@ -345,6 +345,8 @@
"test:integration": "vscode-test",
"test:unit": "TS_NODE_PROJECT='./tsconfig.unit-test.json' mocha",
"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",
"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",
@@ -383,7 +385,8 @@
"@typescript-eslint/parser": "^7.18.0",
"@typescript-eslint/utils": "^8.33.0",
"@vscode/test-cli": "^0.0.10",
"@vscode/test-electron": "^2.4.1",
"@vscode/test-electron": "^2.5.2",
"@vscode/vsce": "^3.6.0",
"chai": "^4.3.10",
"chalk": "^5.3.0",
"esbuild": "^0.25.0",
@@ -392,6 +395,7 @@
"grpc-tools": "^1.13.0",
"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",
@@ -408,8 +412,8 @@
"dependencies": {
"@anthropic-ai/sdk": "^0.37.0",
"@anthropic-ai/vertex-sdk": "^0.6.4",
"@aws-sdk/client-bedrock-runtime": "^3.826.0",
"@aws-sdk/credential-providers": "^3.826.0",
"@aws-sdk/client-bedrock-runtime": "^3.840.0",
"@aws-sdk/credential-providers": "^3.840.0",
"@bufbuild/protobuf": "^2.2.5",
"@cerebras/cerebras_cloud_sdk": "^1.35.0",
"@google-cloud/vertexai": "^1.9.3",
@@ -424,6 +428,7 @@
"@opentelemetry/sdk-node": "^0.39.1",
"@opentelemetry/sdk-trace-node": "^1.30.1",
"@opentelemetry/semantic-conventions": "^1.30.0",
"@playwright/test": "^1.53.2",
"@sentry/browser": "^9.12.0",
"@streamparser/json": "^0.0.22",
"@vscode/codicons": "^0.0.36",
@@ -448,6 +453,7 @@
"image-size": "^2.0.2",
"isbinaryfile": "^5.0.2",
"jschardet": "^3.1.4",
"jwt-decode": "^4.0.0",
"mammoth": "^1.8.0",
"monaco-vscode-textmate-theme-converter": "^0.1.7",
"nice-grpc": "^2.1.12",
+17
View File
@@ -0,0 +1,17 @@
import { defineConfig } from "@playwright/test"
const isGitHubAction = !!process.env.CI
export default defineConfig({
workers: 1,
retries: 1,
testDir: "src/test/e2e",
timeout: 20000,
expect: {
timeout: 20000,
},
fullyParallel: true,
reporter: isGitHubAction ? [["github"], ["list"]] : [["list"]],
globalSetup: require.resolve("./src/test/e2e/utils/setup"),
globalTeardown: require.resolve("./src/test/e2e/utils/teardown"),
})
+96 -43
View File
@@ -1,76 +1,129 @@
syntax = "proto3";
package cline;
import "common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
// Service for account-related operations
service AccountService {
// Handles the user clicking the login link in the UI.
// Generates a secure nonce for state validation, stores it in secrets,
// and opens the authentication URL in the external browser.
rpc accountLoginClicked(EmptyRequest) returns (String);
// Handles the user clicking the login link in the UI.
// Generates a secure nonce for state validation, stores it in secrets,
// and opens the authentication URL in the external browser.
rpc accountLoginClicked(EmptyRequest) returns (String);
// Handles the user clicking the logout button in the UI.
// Clears API keys and user state.
rpc accountLogoutClicked(EmptyRequest) returns (Empty);
// Handles the user clicking the logout button in the UI.
// Clears API keys and user state.
rpc accountLogoutClicked(EmptyRequest) returns (Empty);
// Subscribe to auth callback events (when authentication tokens are received)
rpc subscribeToAuthCallback(EmptyRequest) returns (stream String);
// Subscribe to auth status update events (when authentication state changes)
rpc subscribeToAuthStatusUpdate(EmptyRequest)
returns (stream AuthState);
// Handles authentication state changes from the Firebase context.
// Updates the user info in global state and returns the updated value.
rpc authStateChanged(AuthStateChangedRequest) returns (AuthStateChanged);
// Handles authentication state changes from the Firebase context.
// Updates the user info in global state and returns the updated value.
rpc authStateChanged(AuthStateChangedRequest)
returns (AuthState);
// Fetches all user credits data (balance, usage transactions, payment transactions)
rpc fetchUserCreditsData(EmptyRequest) returns (UserCreditsData);
// Fetches all user credits data
// (balance, usage transactions, payment transactions)
rpc getUserCredits(EmptyRequest) returns (UserCreditsData);
rpc getOrganizationCredits(GetOrganizationCreditsRequest) returns (OrganizationCreditsData);
// Fetches all user organizations data
// Returns a list of UserOrganization objects
rpc getUserOrganizations(EmptyRequest) returns (UserOrganizationsResponse);
rpc setUserOrganization(UserOrganizationUpdateRequest) returns (Empty);
}
message AuthStateChangedRequest {
Metadata metadata = 1;
UserInfo user = 2;
Metadata metadata = 1;
UserInfo user = 2;
}
message AuthStateChanged {
optional UserInfo user = 1;
message AuthState {
optional UserInfo user = 1;
}
// User's information
message UserInfo {
optional string display_name = 1;
optional string email = 2;
optional string photo_url = 3;
string uid = 1;
optional string display_name = 2;
optional string email = 3;
optional string photo_url = 4;
}
message UserOrganization {
bool active = 1;
string member_id = 2;
string name = 3;
string organization_id = 4;
repeated string roles = 5; // ["admin", "member", "owner"]
}
message UserOrganizationsResponse {
repeated UserOrganization organizations = 1;
}
message UserOrganizationUpdateRequest {
optional string organization_id = 1;
}
// Response containing all user credits data
message UserCreditsData {
UserCreditsBalance balance = 1;
repeated UsageTransaction usage_transactions = 2;
repeated PaymentTransaction payment_transactions = 3;
UserCreditsBalance balance = 1;
repeated UsageTransaction usage_transactions = 2;
repeated PaymentTransaction payment_transactions = 3;
}
message GetOrganizationCreditsRequest {
string organization_id = 1;
}
message OrganizationCreditsData {
UserCreditsBalance balance = 1;
string organization_id = 2;
repeated OrganizationUsageTransaction usage_transactions = 3;
}
// User's current credit balance
message UserCreditsBalance {
double current_balance = 1;
double current_balance = 1;
}
// Usage transaction record
message UsageTransaction {
string spent_at = 1;
string creator_id = 2;
double credits = 3;
string model_provider = 4;
string model = 5;
int32 prompt_tokens = 6;
int32 completion_tokens = 7;
int32 total_tokens = 8;
string ai_inference_provider_name = 1;
string ai_model_name = 2;
string ai_model_type_name = 3;
int32 completion_tokens = 4;
double cost_usd = 5;
string created_at = 6;
double credits_used = 7;
string generation_id = 8;
string organization_id = 9;
int32 prompt_tokens = 10;
int32 total_tokens = 11;
string user_id = 12;
}
// Payment transaction record
message PaymentTransaction {
string paid_at = 1;
string creator_id = 2;
int32 amount_cents = 3;
double credits = 4;
string paid_at = 1;
string creator_id = 2;
int32 amount_cents = 3;
double credits = 4;
}
message OrganizationUsageTransaction {
string ai_inference_provider_name = 1;
string ai_model_name = 2;
string ai_model_type_name = 3;
int32 completion_tokens = 4;
double cost_usd = 5;
string created_at = 6;
double credits_used = 7;
string generation_id = 8;
string organization_id = 9;
int32 prompt_tokens = 10;
int32 total_tokens = 11;
string user_id = 12;
}
+1 -2
View File
@@ -1,11 +1,10 @@
syntax = "proto3";
package cline;
import "common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
service BrowserService {
rpc getBrowserConnectionInfo(EmptyRequest) returns (BrowserConnectionInfo);
rpc testBrowserConnection(StringRequest) returns (BrowserConnection);
+1 -2
View File
@@ -1,11 +1,10 @@
syntax = "proto3";
package cline;
import "common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
service CheckpointsService {
rpc checkpointDiff(Int64Request) returns (Empty);
rpc checkpointRestore(CheckpointRestoreRequest) returns (Empty);
+1 -2
View File
@@ -1,11 +1,10 @@
syntax = "proto3";
package cline;
import "common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
// Service for file-related operations
service FileService {
// Copies text to clipboard
+39
View File
@@ -0,0 +1,39 @@
syntax = "proto3";
package host;
option java_package = "bot.cline.host.proto";
option java_multiple_files = true;
import "common.proto";
// Provides methods for diff views.
service DiffService {
// Open the diff view/editor.
rpc openDiff(OpenDiffRequest) returns (OpenDiffResponse);
rpc replaceText(ReplaceTextRequest) returns (ReplaceTextResponse);
}
message OpenDiffRequest {
optional cline.Metadata metadata = 1;
// The absolute path of the document being edited.
optional string path = 2;
// The new content for the file.
optional string content = 3;
}
message OpenDiffResponse {
// A unique identifier for the diff view that was opened.
optional string diff_id = 1;
}
message ReplaceTextRequest {
optional cline.Metadata metadata = 1;
optional string diff_id = 2;
optional string content = 3;
optional int32 start_line = 4;
optional int32 end_line = 5;
}
message ReplaceTextResponse {
// TBD
}
+3
View File
@@ -13,4 +13,7 @@ service EnvService {
// Reads text from the system clipboard.
rpc clipboardReadText(cline.EmptyRequest) returns (cline.String);
// Opens a URL in the user's default browser or application.
rpc openExternal(cline.StringRequest) returns (cline.Empty);
}
-36
View File
@@ -1,36 +0,0 @@
syntax = "proto3";
package host;
option java_package = "bot.cline.host.proto";
option java_multiple_files = true;
import "common.proto";
// UriService provides methods for working with URIs in the IDE
service UriService {
// Create a new file URI from a file path
rpc file(cline.StringRequest) returns (Uri);
// Join a URI with additional path segments
rpc joinPath(JoinPathRequest) returns (Uri);
// Parse a string URI into a Uri object
rpc parse(cline.StringRequest) returns (Uri);
}
// Uri represents a URI in the IDE
message Uri {
string scheme = 1;
string authority = 2;
string path = 3;
string query = 4;
string fragment = 5;
string fs_path = 6;
}
// Request for joining path segments to a URI
message JoinPathRequest {
cline.Metadata metadata = 1;
Uri base = 2;
repeated string path_segments = 3;
}
+25
View File
@@ -11,6 +11,7 @@ service WindowService {
// Opens a text document in the editor and returns editor information.
rpc showTextDocument(ShowTextDocumentRequest) returns (TextEditorInfo);
rpc showOpenDialogue(ShowOpenDialogueRequest) returns (SelectedResources);
rpc showMessage(ShowMessageRequest) returns (SelectedResponse);
}
message ShowTextDocumentRequest {
@@ -46,3 +47,27 @@ message ShowOpenDialogueFilterOption {
message SelectedResources {
repeated string paths = 1;
}
enum ShowMessageType {
ERROR = 0;
INFORMATION = 1;
WARNING = 2;
}
message ShowMessageRequest {
cline.Metadata metadata = 1;
ShowMessageType type = 2;
string message = 3;
optional ShowMessageRequestOptions options = 4;
}
message ShowMessageRequestOptions {
repeated string items = 1;
optional bool modal = 2;
optional string detail = 3;
}
message SelectedResponse {
optional string selected_option = 1;
}
+14 -3
View File
@@ -1,16 +1,15 @@
syntax = "proto3";
package cline;
import "common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
service McpService {
rpc toggleMcpServer(ToggleMcpServerRequest) returns (McpServers);
rpc updateMcpTimeout(UpdateMcpTimeoutRequest) returns (McpServers);
rpc addRemoteMcpServer(AddRemoteMcpServerRequest) returns (McpServers);
rpc downloadMcp(StringRequest) returns (Empty);
rpc downloadMcp(StringRequest) returns (McpDownloadResponse);
rpc restartMcpServer(StringRequest) returns (McpServers);
rpc deleteMcpServer(StringRequest) returns (McpServers);
rpc toggleToolAutoApprove(ToggleToolAutoApproveRequest) returns (McpServers);
@@ -119,3 +118,15 @@ message McpMarketplaceItem {
message McpMarketplaceCatalog {
repeated McpMarketplaceItem items = 1;
}
message McpDownloadResponse {
string mcp_id = 1;
string github_url = 2;
string name = 3;
string author = 4;
string description = 5;
string readme_content = 6;
string llms_installation_content = 7;
bool requires_api_key = 8;
optional string error = 9;
}
+16 -6
View File
@@ -1,11 +1,10 @@
syntax = "proto3";
package cline;
import "common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
// Service for model-related operations
service ModelsService {
// Fetches available models from Ollama
@@ -24,6 +23,8 @@ service ModelsService {
rpc subscribeToOpenRouterModels(EmptyRequest) returns (stream OpenRouterCompatibleModelInfo);
// Updates API configuration
rpc updateApiConfigurationProto(UpdateApiConfigurationRequest) returns (Empty);
// Refreshes and returns Groq models
rpc refreshGroqModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
}
// List of VS Code LM models
@@ -121,8 +122,10 @@ enum ApiProvider {
XAI = 21;
SAMBANOVA = 22;
CEREBRAS = 23;
SAPAICORE = 24;
CLAUDE_CODE = 25;
GROQ = 24;
SAPAICORE = 25;
CLAUDE_CODE = 26;
MOONSHOT = 27;
}
// Model info for OpenAI-compatible models
@@ -165,7 +168,7 @@ message ModelsApiConfiguration {
// From ApiHandlerOptions (excluding onRetryAttempt function)
optional string api_model_id = 1;
optional string api_key = 2;
optional string cline_api_key = 3;
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;
@@ -236,4 +239,11 @@ message ModelsApiConfiguration {
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;
}
-3
View File
@@ -1,3 +0,0 @@
{
"type": "module"
}
+1 -2
View File
@@ -1,11 +1,10 @@
syntax = "proto3";
package cline;
import "common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
// SlashService provides methods for managing slash
service SlashService {
// Sends button click message
+11 -4
View File
@@ -1,10 +1,9 @@
syntax = "proto3";
package cline;
import "common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
service StateService {
rpc getLatestState(EmptyRequest) returns (State);
rpc updateTerminalConnectionTimeout(Int64Request) returns (Int64);
@@ -113,7 +112,7 @@ message UpdateSettingsRequest {
optional int64 shell_integration_timeout = 8;
optional bool terminal_reuse_enabled = 9;
optional bool mcp_responses_collapsed = 10;
optional bool mcp_rich_display_enabled = 11;
optional string mcp_display_mode = 11;
optional int64 terminal_output_line_limit = 12;
}
@@ -126,7 +125,7 @@ message ApiConfiguration {
optional string api_base_url = 4;
// Provider-specific API keys
optional string cline_api_key = 5;
optional string cline_account_id = 5;
optional string openrouter_api_key = 6;
optional string anthropic_base_url = 7;
optional string openai_api_key = 8;
@@ -233,4 +232,12 @@ message ApiConfiguration {
// Claude Code specific
optional string claude_code_path = 77;
// Extension fields for Bedrock Api Keys
optional string aws_authentication = 78;
optional string aws_bedrock_api_key = 79;
// Moonshot
optional string moonshot_api_key = 80;
optional string moonshot_api_line = 81;
}
+1 -2
View File
@@ -1,11 +1,10 @@
syntax = "proto3";
package cline;
import "common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
service TaskService {
// Cancels the currently running task
rpc cancelTask(EmptyRequest) returns (Empty);
+1 -2
View File
@@ -1,11 +1,10 @@
syntax = "proto3";
package cline;
import "common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
// Enum for webview provider types
enum WebviewProviderType {
SIDEBAR = 0;
+1 -2
View File
@@ -1,11 +1,10 @@
syntax = "proto3";
package cline;
import "common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
service WebService {
rpc checkIsImageUrl(StringRequest) returns (IsImageUrl);
rpc fetchOpenGraphData(StringRequest) returns (OpenGraphData);
@@ -27,5 +27,6 @@ export const hostServiceNameMap = {
workspace: "host.WorkspaceService",
env: "host.EnvService",
window: "host.WindowService",
diff: "host.DiffService",
// Add new host services here
}
@@ -9,22 +9,22 @@ import chalk from "chalk"
import os from "os"
import { createRequire } from "module"
import { serviceNameMap, hostServiceNameMap } from "./build-proto-config.js"
import { serviceNameMap, hostServiceNameMap } from "./build-proto-config.mjs"
const require = createRequire(import.meta.url)
const PROTOC = path.join(require.resolve("grpc-tools"), "../bin/protoc")
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url))
const ROOT_DIR = path.resolve(SCRIPT_DIR, "..")
const SCRIPT_NAME = path.relative(process.cwd(), fileURLToPath(import.meta.url))
const TS_OUT_DIR = path.join(ROOT_DIR, "src", "shared", "proto")
const GRPC_JS_OUT_DIR = path.join(ROOT_DIR, "src", "generated", "grpc-js")
const NICE_JS_OUT_DIR = path.join(ROOT_DIR, "src", "generated", "nice-grpc")
const DESCRIPTOR_OUT_DIR = path.join(ROOT_DIR, "dist-standalone", "proto")
const PROTO_DIR = path.resolve("proto")
const TS_OUT_DIR = path.resolve("src/shared/proto")
const GRPC_JS_OUT_DIR = path.resolve("src/generated/grpc-js")
const NICE_JS_OUT_DIR = path.resolve("src/generated/nice-grpc")
const DESCRIPTOR_OUT_DIR = path.resolve("dist-standalone/proto")
const isWindows = process.platform === "win32"
const TS_PROTO_PLUGIN = isWindows
? path.join(ROOT_DIR, "node_modules", ".bin", "protoc-gen-ts_proto.cmd") // Use the .bin directory path for Windows
? path.resolve("node_modules/.bin/protoc-gen-ts_proto.cmd") // Use the .bin directory path for Windows
: require.resolve("ts-proto/protoc-gen-ts_proto")
const TS_PROTO_OPTIONS = [
@@ -37,12 +37,7 @@ const TS_PROTO_OPTIONS = [
]
// Service directories derived from imported serviceNameMap
const serviceDirs = Object.keys(serviceNameMap).map((serviceKey) => path.join(ROOT_DIR, "src", "core", "controller", serviceKey))
// Host service directories derived from imported hostServiceNameMap
const hostServiceDirs = Object.keys(hostServiceNameMap).map((serviceKey) =>
path.join(ROOT_DIR, "src", "hosts", "vscode", serviceKey),
)
const serviceDirs = Object.keys(serviceNameMap).map((serviceKey) => path.join("src/core/controller", serviceKey))
async function main() {
console.log(chalk.bold.blue("Starting Protocol Buffer code generation..."))
@@ -61,8 +56,8 @@ async function main() {
await ensureProtoFilesExist()
// Process all proto files
const protoFiles = await globby("**/*.proto", { cwd: SCRIPT_DIR, realpath: true })
console.log(chalk.cyan(`Processing ${protoFiles.length} proto files from`), SCRIPT_DIR)
const protoFiles = await globby("**/*.proto", { cwd: PROTO_DIR, realpath: true })
console.log(chalk.cyan(`Processing ${protoFiles.length} proto files from`), PROTO_DIR)
tsProtoc(TS_OUT_DIR, protoFiles, TS_PROTO_OPTIONS)
// grpc-js is used to generate service impls for the ProtoBus service.
@@ -73,7 +68,7 @@ async function main() {
const descriptorFile = path.join(DESCRIPTOR_OUT_DIR, "descriptor_set.pb")
const descriptorProtocCommand = [
PROTOC,
`--proto_path="${SCRIPT_DIR}"`,
`--proto_path="${PROTO_DIR}"`,
`--descriptor_set_out="${descriptorFile}"`,
"--include_imports",
...protoFiles,
@@ -89,11 +84,12 @@ async function main() {
log_verbose(chalk.green("Protocol Buffer code generation completed successfully."))
log_verbose(chalk.green(`TypeScript files generated in: ${TS_OUT_DIR}`))
await generateMethodRegistrations()
await generateHostMethodRegistrations()
await generateServiceConfig()
await generateHostServiceConfig()
await generateGrpcClientConfig()
await generateProtoBusServiceConfig()
await generateProtoBusMethodRegistrations()
await generateProtoBusGrpcClientConfig()
await generateHostBridgeServiceConfig()
await generateHostBridgeMethodRegistrations()
console.log(chalk.bold.blue("Finished Protocol Buffer code generation."))
}
@@ -102,7 +98,7 @@ async function tsProtoc(outDir, protoFiles, protoOptions) {
// Build the protoc command with proper path handling for cross-platform
const command = [
PROTOC,
`--proto_path="${SCRIPT_DIR}"`,
`--proto_path="${PROTO_DIR}"`,
`--plugin=protoc-gen-ts_proto="${TS_PROTO_PLUGIN}"`,
`--ts_proto_out="${outDir}"`,
`--ts_proto_opt=${protoOptions.join(",")} `,
@@ -122,7 +118,7 @@ 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 generateGrpcClientConfig() {
async function generateProtoBusGrpcClientConfig() {
log_verbose(chalk.cyan("Generating gRPC client configuration..."))
const serviceImports = []
@@ -147,7 +143,7 @@ async function generateGrpcClientConfig() {
// Generate the file content
const content = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Generated by ${SCRIPT_NAME}
import { createGrpcClient } from "./grpc-client-base"
${serviceImports.join("\n")}
@@ -158,7 +154,7 @@ export {
${serviceExports.join(",\n\t")}
}`
const filePath = path.join(ROOT_DIR, "webview-ui", "src", "services", "grpc-client.ts")
const filePath = path.resolve("webview-ui/src/services/grpc-client.ts")
await writeFileWithMkdirs(filePath, content)
log_verbose(chalk.green(`Generated gRPC client at ${filePath}`))
}
@@ -221,12 +217,12 @@ async function parseProtoForStreamingMethods(protoFiles, scriptDir) {
return streamingMethodsMap
}
async function generateMethodRegistrations() {
async function generateProtoBusMethodRegistrations() {
log_verbose(chalk.cyan("Generating method registration files..."))
// Parse proto files for streaming methods
const protoFiles = await globby("*.proto", { cwd: SCRIPT_DIR })
const streamingMethodsMap = await parseProtoForStreamingMethods(protoFiles, SCRIPT_DIR)
const protoFiles = await globby("*.proto", { cwd: PROTO_DIR })
const streamingMethodsMap = await parseProtoForStreamingMethods(protoFiles, PROTO_DIR)
for (const serviceDir of serviceDirs) {
const serviceName = path.basename(serviceDir)
@@ -243,7 +239,7 @@ async function generateMethodRegistrations() {
// Create the methods.ts file with header
let methodsContent = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Generated by ${SCRIPT_NAME}
// Import all method implementations
import { registerMethod } from "./index"\n`
@@ -292,7 +288,7 @@ export function registerAllMethods(): void {
// Generate index.ts file
const capitalizedServiceName = serviceName.charAt(0).toUpperCase() + serviceName.slice(1)
const indexContent = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Generated by ${SCRIPT_NAME}
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../grpc-service"
import { StreamingResponseHandler } from "../grpc-handler"
@@ -327,7 +323,7 @@ registerAllMethods()`
* Generate a service configuration file that maps service names to their handlers
* This eliminates the need for manual switch/case statements in grpc-handler.ts
*/
async function generateServiceConfig() {
async function generateProtoBusServiceConfig() {
log_verbose(chalk.cyan("Generating service configuration file..."))
const serviceImports = []
@@ -347,7 +343,7 @@ async function generateServiceConfig() {
}
const content = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Generated by ${SCRIPT_NAME}
import { Controller } from "./index"
import { StreamingResponseHandler } from "./grpc-handler"
@@ -367,7 +363,7 @@ export interface ServiceHandlerConfig {
export const serviceHandlers: Record<string, ServiceHandlerConfig> = {${serviceConfigs.join(",")}
};`
const configPath = path.join(ROOT_DIR, "src", "core", "controller", "grpc-service-config.ts")
const configPath = path.resolve("src/core/controller/grpc-service-config.ts")
await writeFileWithMkdirs(configPath, content)
log_verbose(chalk.green(`Generated service configuration at ${configPath}`))
}
@@ -380,7 +376,7 @@ async function ensureProtoFilesExist() {
log_verbose(chalk.cyan("Checking for missing proto files..."))
// Get existing proto files
const existingProtoFiles = await globby("*.proto", { cwd: SCRIPT_DIR })
const existingProtoFiles = await globby("*.proto", { cwd: PROTO_DIR })
const existingProtoServices = existingProtoFiles.map((file) => path.basename(file, ".proto"))
// Check each service in serviceNameMap
@@ -417,7 +413,7 @@ service ${serviceClassName} {
`
// Write the template proto file
const protoFilePath = path.join(SCRIPT_DIR, `${serviceName}.proto`)
const protoFilePath = path.join(PROTO_DIR, `${serviceName}.proto`)
await fs.writeFile(protoFilePath, protoContent)
log_verbose(chalk.green(`Created template proto file at ${protoFilePath}`))
}
@@ -427,17 +423,22 @@ service ${serviceClassName} {
/**
* Generate method registration files for host services
*/
async function generateHostMethodRegistrations() {
async function generateHostBridgeMethodRegistrations() {
log_verbose(chalk.cyan("Generating host method registration files..."))
// Host service directories derived from imported hostServiceNameMap
const hostServiceDirs = Object.keys(hostServiceNameMap).map((serviceKey) =>
path.join("src/hosts/vscode/hostbridge", serviceKey),
)
// Parse proto files for streaming methods
const hostProtoFiles = await globby("*.proto", { cwd: path.join(SCRIPT_DIR, "host") })
const streamingMethodsMap = await parseProtoForStreamingMethods(hostProtoFiles, path.join(SCRIPT_DIR, "host"))
const hostProtoFiles = await globby("*.proto", { cwd: path.join(PROTO_DIR, "host") })
const streamingMethodsMap = await parseProtoForStreamingMethods(hostProtoFiles, path.join(PROTO_DIR, "host"))
for (const serviceDir of hostServiceDirs) {
const serviceName = path.basename(serviceDir)
const fullServiceName = hostServiceNameMap[serviceName]
const streamingMethods = streamingMethodsMap.get(fullServiceName) || []
const outputDir = path.join("src/generated/hosts/vscode/hostbridge", serviceName)
log_verbose(chalk.cyan(`Generating method registrations for host ${serviceName}...`))
@@ -449,7 +450,7 @@ async function generateHostMethodRegistrations() {
// Create the methods.ts file with header
let methodsContent = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Generated ${SCRIPT_NAME}
// Import all method implementations
import { registerMethod } from "./index"\n`
@@ -457,7 +458,7 @@ import { registerMethod } from "./index"\n`
// Import implementations directly
for (const file of implementationFiles) {
const baseName = path.basename(file, ".ts")
methodsContent += `import { ${baseName} } from "./${baseName}"\n`
methodsContent += `import { ${baseName} } from "@hosts/vscode/hostbridge/${serviceName}/${baseName}"\n`
}
// Add streaming methods information
@@ -491,17 +492,17 @@ export function registerAllMethods(): void {
methodsContent += `}`
// Write the methods.ts file
const registryFile = path.join(serviceDir, "methods.ts")
const registryFile = path.join(outputDir, "methods.ts")
await writeFileWithMkdirs(registryFile, methodsContent)
log_verbose(chalk.green(`Generated ${registryFile}`))
// Generate index.ts file
const capitalizedServiceName = serviceName.charAt(0).toUpperCase() + serviceName.slice(1)
const indexContent = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Generated by ${SCRIPT_NAME}
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../host-grpc-service"
import { StreamingResponseHandler } from "../host-grpc-handler"
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "@hosts/vscode/hostbridge-grpc-service"
import { StreamingResponseHandler } from "@hosts/vscode/hostbridge-grpc-handler"
import { registerAllMethods } from "./methods"
// Create ${serviceName} service registry
@@ -521,7 +522,7 @@ export const isStreamingMethod = ${serviceName}Service.isStreamingMethod
registerAllMethods()`
// Write the index.ts file
const indexFile = path.join(serviceDir, "index.ts")
const indexFile = path.join(outputDir, "index.ts")
await writeFileWithMkdirs(indexFile, indexContent)
log_verbose(chalk.green(`Generated ${indexFile}`))
}
@@ -532,7 +533,7 @@ registerAllMethods()`
/**
* Generate a service configuration file for host services
*/
async function generateHostServiceConfig() {
async function generateHostBridgeServiceConfig() {
log_verbose(chalk.cyan("Generating host service configuration file..."))
const serviceImports = []
@@ -542,7 +543,7 @@ async function generateHostServiceConfig() {
for (const [dirName, fullServiceName] of Object.entries(hostServiceNameMap)) {
const capitalizedName = dirName.charAt(0).toUpperCase() + dirName.slice(1)
serviceImports.push(
`import { handle${capitalizedName}ServiceRequest, handle${capitalizedName}ServiceStreamingRequest } from "./${dirName}/index"`,
`import { handle${capitalizedName}ServiceRequest, handle${capitalizedName}ServiceStreamingRequest } from "@generated/hosts/vscode/hostbridge/${dirName}/index"`,
)
serviceConfigs.push(`
"${fullServiceName}": {
@@ -552,9 +553,9 @@ async function generateHostServiceConfig() {
}
const content = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Generated by ${SCRIPT_NAME}
import { StreamingResponseHandler } from "./host-grpc-handler"
import { StreamingResponseHandler } from "@/hosts/vscode/hostbridge-grpc-handler"
${serviceImports.join("\n")}
/**
@@ -571,7 +572,7 @@ export interface HostServiceHandlerConfig {
export const hostServiceHandlers: Record<string, HostServiceHandlerConfig> = {${serviceConfigs.join(",")}
};`
const filePath = path.join(ROOT_DIR, "src/hosts/vscode/host-grpc-service-config.ts")
const filePath = "src/generated/hosts/vscode/hostbridge-grpc-service-config.ts"
await writeFileWithMkdirs(filePath, content)
log_verbose(chalk.green(`Generated host service configuration at ${filePath}`))
}
@@ -583,15 +584,33 @@ async function cleanup() {
for (const file of existingFiles) {
await fs.unlink(path.join(TS_OUT_DIR, file))
}
await rmdir(path.join(ROOT_DIR, "src", "generated"))
await rmdir("src/generated")
// Clean up generated files that were moved.
await fs.rm(path.join(ROOT_DIR, "src", "standalone", "services", "host-grpc-client.ts"), { force: true })
await rmdir(path.join(ROOT_DIR, "src", "standalone", "services"))
await fs.rm(path.join(ROOT_DIR, "hosts", "vscode"), { force: true, recursive: true })
await rmdir(path.join(ROOT_DIR, "hosts"))
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(path.join(ROOT_DIR, "src/standalone/server-setup.ts"), { force: true })
await fs.rm("src/standalone/server-setup.ts", { force: true })
await fs.rm("src/hosts/vscode/host-grpc-service-config.ts", { force: true })
const oldhostbridgefiles = [
"src/hosts/vscode/workspace/methods.ts",
"src/hosts/vscode/workspace/index.ts",
"src/hosts/vscode/diff/methods.ts",
"src/hosts/vscode/diff/index.ts",
"src/hosts/vscode/env/methods.ts",
"src/hosts/vscode/env/index.ts",
"src/hosts/vscode/window/methods.ts",
"src/hosts/vscode/window/index.ts",
"src/hosts/vscode/watch/methods.ts",
"src/hosts/vscode/watch/index.ts",
"src/hosts/vscode/uri/methods.ts",
"src/hosts/vscode/uri/index.ts",
]
for (const file of oldhostbridgefiles) {
await fs.rm(file, { force: true })
}
}
/**
+16 -2
View File
@@ -5,17 +5,31 @@ DIR=${1:-src/}
DEST_DIR=dist-standalone
SDK_DEST=$DEST_DIR/vscode-sdk-uses.txt
CSS_DEST=$DEST_DIR/vscode-css-uses.txt
TMP=/tmp/vscode-sdk-uses.txt.tmp
mkdir -p $DEST_DIR
{
git grep -h 'vscode\.' $DIR |
grep -Ev '//.*vscode' | # remove commented out code
grep -v vscode.commands.executeCommand | # executeCommand is handled separately
grep -Ev '"vscode' | # remove command strings that get included because they start with vscode
sed 's|.*vscode\.|vscode.|'| # remove everything before vscode.
sed 's/[^a-zA-Z0-9_.].*$//' | # remove everything after last identifier
grep -E '\.[a-z][^.]+$' | # remove types (last part of identifier should be lowercase)
sort | uniq -c | sort -n | # Count occurrences
cat > $SDK_DEST
cat > $TMP
}
{
grep -rh vscode.commands.executeCommand $DIR |
perl -ne 'print if /["\x27"]/' | # Remove occurrences where the command is not on the same line (line doesnt contain quote chars) :(
sed -n 's|.*\(vscode.commands.executeCommand[^,]*\).*|\1|p'| # Remove all params after the first one
sed 's|\(".*"\).*|\1)|'| # Close the parantheses
cat >> $TMP
}
# Count occurrences
cat $TMP | sort | uniq -c | sort -n > $SDK_DEST
rm $TMP
echo Wrote uses of the vscode SDK to $(realpath $SDK_DEST)
{
Regular → Executable
+161 -69
View File
@@ -1,83 +1,175 @@
import fs from "fs"
import path from "path"
import { glob } from "glob"
#!/usr/bin/env node
import archiver from "archiver"
import { cp } from "fs/promises"
import { execSync } from "child_process"
import fs from "fs"
import { cp } from "fs/promises"
import { glob } from "glob"
import minimatch from "minimatch"
import path from "path"
const BUILD_DIR = "dist-standalone"
const SOURCE_DIR = "standalone/runtime-files"
const RUNTIME_DEPS_DIR = "standalone/runtime-files"
await cp(SOURCE_DIR, BUILD_DIR, { recursive: true })
// Run npm install in the distribution directory
console.log("Running npm install in distribution directory...")
const cwd = process.cwd()
process.chdir(BUILD_DIR)
try {
execSync("npm install", { stdio: "inherit" })
// Move the vscode directory into node_modules.
// It can't be installed using npm because it will create a symlink which is not portable.
fs.renameSync("vscode", path.join("node_modules", "vscode"))
} catch (error) {
console.error("Error during setup:", error)
process.exit(1)
} finally {
process.chdir(cwd)
async function main() {
await installNodeDependencies()
await zipDistribution()
}
// Check for native .node modules.
const nativeModules = await glob("**/*.node", { cwd: BUILD_DIR, nodir: true })
if (nativeModules.length > 0) {
console.error("Native node modules cannot be included in the standalone distribution:\n", nativeModules.join("\n"))
process.exit(1)
async function installNodeDependencies() {
await cpr(RUNTIME_DEPS_DIR, BUILD_DIR)
console.log("Running npm install in distribution directory...")
const cwd = process.cwd()
process.chdir(BUILD_DIR)
try {
execSync("npm install", { stdio: "inherit" })
// Move the vscode directory into node_modules.
// It can't be installed using npm because it will create a symlink which cannot be unzipped correctly on windows.
fs.renameSync("vscode", path.join("node_modules", "vscode"))
} catch (error) {
console.error("Error during setup:", error)
process.exit(1)
} finally {
process.chdir(cwd)
}
// Check for native .node modules.
const nativeModules = await glob("**/*.node", { cwd: BUILD_DIR, nodir: true })
if (nativeModules.length > 0) {
console.error("Native node modules cannot be included in the standalone distribution:\n", nativeModules.join("\n"))
process.exit(1)
}
}
// Zip the build directory (excluding any pre-existing output zip).
const zipPath = path.join(BUILD_DIR, "standalone.zip")
const output = fs.createWriteStream(zipPath)
const archive = archiver("zip", { zlib: { level: 3 } })
async function zipDistribution() {
// Zip the build directory (excluding any pre-existing output zip).
const zipPath = path.join(BUILD_DIR, "standalone.zip")
const output = fs.createWriteStream(zipPath)
const archive = archiver("zip", { zlib: { level: 3 } })
output.on("close", () => {
console.log(`Created ${zipPath} (${(archive.pointer() / 1024 / 1024).toFixed(1)} MB)`)
})
archive.on("warning", (err) => {
console.warn(`Warning: ${err}`)
})
archive.on("error", (err) => {
throw err
})
output.on("close", () => {
console.log(`Created ${zipPath} (${(archive.pointer() / 1024 / 1024).toFixed(1)} MB)`)
})
archive.on("warning", (err) => {
console.warn(`Warning: ${err}`)
})
archive.on("error", (err) => {
throw err
})
archive.pipe(output)
archive.glob("**/*", {
cwd: BUILD_DIR,
ignore: ["standalone.zip"],
})
archive.pipe(output)
// Add all the files from the standalone build dir.
archive.glob("**/*", {
cwd: BUILD_DIR,
ignore: ["standalone.zip"],
})
// Add the whole cline directory under "extension"
archive.directory(process.cwd(), "extension", (entry) => {
// Skip certain directories.
const exclude = [
BUILD_DIR + "/",
"node_modules/", // node_modules nearly 1GB.
"webview-ui/node_modules/", // node_modules nearly 1GB.
]
// These node modules are used at runtime as assets, they need to be included.
const include = ["node_modules/@vscode/", "webview-ui/node_modules/katex"]
const name = entry.name
// Exclude the same files as the VCE vscode extension packager.
// Also ignore the dist directory, the build directory for the extension.
const isIgnored = createIsIgnored(["dist/**"])
if (include.some((prefix) => name.startsWith(prefix))) {
// Add the whole cline directory under "extension", except the for the ignored files.
archive.directory(process.cwd(), "extension", (entry) => {
if (isIgnored(entry.name)) {
log_verbose("Ignoring", entry.name)
return false
}
return entry
}
if (exclude.some((prefix) => name.startsWith(prefix))) {
return false
}
if (name.match(/(^|\/)\./)) {
// exclude dot directories
return false
}
return entry
})
})
console.log("Zipping package...")
await archive.finalize()
console.log("Zipping package...")
await archive.finalize()
}
/**
* This is based on https://github.com/microsoft/vscode-vsce/blob/fafad8a63e9cf31179f918eb7a4eeb376834c904/src/package.ts#L1695
* because the .vscodeignore format is not compatible with the `ignore` npm module.
*/
function createIsIgnored(standaloneIgnores) {
const MinimatchOptions = { dot: true }
const defaultIgnore = [
".vscodeignore",
"package-lock.json",
"npm-debug.log",
"yarn.lock",
"yarn-error.log",
"npm-shrinkwrap.json",
".editorconfig",
".npmrc",
".yarnrc",
".gitattributes",
"*.todo",
"tslint.yaml",
".eslintrc*",
".babelrc*",
".prettierrc*",
".cz-config.js",
".commitlintrc*",
"webpack.config.js",
"ISSUE_TEMPLATE.md",
"CONTRIBUTING.md",
"PULL_REQUEST_TEMPLATE.md",
"CODE_OF_CONDUCT.md",
".github",
".travis.yml",
"appveyor.yml",
"**/.git",
"**/.git/**",
"**/*.vsix",
"**/.DS_Store",
"**/*.vsixmanifest",
"**/.vscode-test/**",
"**/.vscode-test-web/**",
]
const rawIgnore = fs.readFileSync(".vscodeignore", "utf8")
// Parse raw ignore by splitting output into lines and filtering out empty lines and comments
const parsedIgnore = rawIgnore
.split(/[\n\r]/)
.map((s) => s.trim())
.filter((s) => !!s)
.filter((i) => !/^\s*#/.test(i))
// Add '/**' to possible folder names
const expandedIgnore = [
...parsedIgnore,
...parsedIgnore.filter((i) => !/(^|\/)[^/]*\*[^/]*$/.test(i)).map((i) => (/\/$/.test(i) ? `${i}**` : `${i}/**`)),
]
// Combine with default ignore list
// Also ignore the dist directory- the build directory for the extension.
const allIgnore = [...defaultIgnore, ...expandedIgnore, ...standaloneIgnores]
// Split into ignore and negate list
const [ignore, negate] = allIgnore.reduce(
(r, e) => (!/^\s*!/.test(e) ? [[...r[0], e], r[1]] : [r[0], [...r[1], e]]),
[[], []],
)
function isIgnored(f) {
return (
ignore.some((i) => minimatch(f, i, MinimatchOptions)) &&
!negate.some((i) => minimatch(f, i.substr(1), MinimatchOptions))
)
}
return isIgnored
}
/* cp -r */
async function cpr(source, dest) {
await cp(source, dest, {
recursive: true,
preserveTimestamps: true,
dereference: false, // preserve symlinks instead of following them
})
}
function log_verbose(...args) {
if (process.argv.includes("-v") || process.argv.includes("--verbose")) {
console.log(...args)
}
}
await main()
+185 -28
View File
@@ -27,6 +27,8 @@ import { SambanovaHandler } from "./providers/sambanova"
import { CerebrasHandler } from "./providers/cerebras"
import { SapAiCoreHandler } from "./providers/sapaicore"
import { ClaudeCodeHandler } from "./providers/claude-code"
import { MoonshotHandler } from "./providers/moonshot"
import { GroqHandler } from "./providers/groq"
export interface ApiHandler {
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream
@@ -38,62 +40,217 @@ export interface SingleCompletionHandler {
completePrompt(prompt: string): Promise<string>
}
function createHandlerForProvider(apiProvider: string | undefined, options: any): ApiHandler {
function createHandlerForProvider(apiProvider: string | undefined, options: Omit<ApiConfiguration, "apiProvider">): ApiHandler {
switch (apiProvider) {
case "anthropic":
return new AnthropicHandler(options)
return new AnthropicHandler({
apiKey: options.apiKey,
anthropicBaseUrl: options.anthropicBaseUrl,
apiModelId: options.apiModelId,
thinkingBudgetTokens: options.thinkingBudgetTokens,
})
case "openrouter":
return new OpenRouterHandler(options)
return new OpenRouterHandler({
openRouterApiKey: options.openRouterApiKey,
openRouterModelId: options.openRouterModelId,
openRouterModelInfo: options.openRouterModelInfo,
openRouterProviderSorting: options.openRouterProviderSorting,
reasoningEffort: options.reasoningEffort,
thinkingBudgetTokens: options.thinkingBudgetTokens,
})
case "bedrock":
return new AwsBedrockHandler(options)
return new AwsBedrockHandler({
apiModelId: options.apiModelId,
awsAccessKey: options.awsAccessKey,
awsSecretKey: options.awsSecretKey,
awsSessionToken: options.awsSessionToken,
awsRegion: options.awsRegion,
awsAuthentication: options.awsAuthentication,
awsBedrockApiKey: options.awsBedrockApiKey,
awsUseCrossRegionInference: options.awsUseCrossRegionInference,
awsBedrockUsePromptCache: options.awsBedrockUsePromptCache,
awsUseProfile: options.awsUseProfile,
awsProfile: options.awsProfile,
awsBedrockEndpoint: options.awsBedrockEndpoint,
awsBedrockCustomSelected: options.awsBedrockCustomSelected,
awsBedrockCustomModelBaseId: options.awsBedrockCustomModelBaseId,
thinkingBudgetTokens: options.thinkingBudgetTokens,
})
case "vertex":
return new VertexHandler(options)
return new VertexHandler({
vertexProjectId: options.vertexProjectId,
vertexRegion: options.vertexRegion,
apiModelId: options.apiModelId,
thinkingBudgetTokens: options.thinkingBudgetTokens,
geminiApiKey: options.geminiApiKey,
geminiBaseUrl: options.geminiBaseUrl,
taskId: options.taskId,
})
case "openai":
return new OpenAiHandler(options)
return new OpenAiHandler({
openAiApiKey: options.openAiApiKey,
openAiBaseUrl: options.openAiBaseUrl,
azureApiVersion: options.azureApiVersion,
openAiHeaders: options.openAiHeaders,
openAiModelId: options.openAiModelId,
openAiModelInfo: options.openAiModelInfo,
reasoningEffort: options.reasoningEffort,
})
case "ollama":
return new OllamaHandler(options)
return new OllamaHandler({
ollamaBaseUrl: options.ollamaBaseUrl,
ollamaModelId: options.ollamaModelId,
ollamaApiOptionsCtxNum: options.ollamaApiOptionsCtxNum,
requestTimeoutMs: options.requestTimeoutMs,
})
case "lmstudio":
return new LmStudioHandler(options)
return new LmStudioHandler({
lmStudioBaseUrl: options.lmStudioBaseUrl,
lmStudioModelId: options.lmStudioModelId,
})
case "gemini":
return new GeminiHandler(options)
return new GeminiHandler({
vertexProjectId: options.vertexProjectId,
vertexRegion: options.vertexRegion,
geminiApiKey: options.geminiApiKey,
geminiBaseUrl: options.geminiBaseUrl,
thinkingBudgetTokens: options.thinkingBudgetTokens,
apiModelId: options.apiModelId,
taskId: options.taskId,
})
case "openai-native":
return new OpenAiNativeHandler(options)
return new OpenAiNativeHandler({
openAiNativeApiKey: options.openAiNativeApiKey,
reasoningEffort: options.reasoningEffort,
apiModelId: options.apiModelId,
})
case "deepseek":
return new DeepSeekHandler(options)
return new DeepSeekHandler({
deepSeekApiKey: options.deepSeekApiKey,
apiModelId: options.apiModelId,
})
case "requesty":
return new RequestyHandler(options)
return new RequestyHandler({
requestyApiKey: options.requestyApiKey,
reasoningEffort: options.reasoningEffort,
thinkingBudgetTokens: options.thinkingBudgetTokens,
requestyModelId: options.requestyModelId,
requestyModelInfo: options.requestyModelInfo,
})
case "fireworks":
return new FireworksHandler(options)
return new FireworksHandler({
fireworksApiKey: options.fireworksApiKey,
fireworksModelId: options.fireworksModelId,
fireworksModelMaxCompletionTokens: options.fireworksModelMaxCompletionTokens,
fireworksModelMaxTokens: options.fireworksModelMaxTokens,
})
case "together":
return new TogetherHandler(options)
return new TogetherHandler({
togetherApiKey: options.togetherApiKey,
togetherModelId: options.togetherModelId,
})
case "qwen":
return new QwenHandler(options)
return new QwenHandler({
qwenApiKey: options.qwenApiKey,
qwenApiLine: options.qwenApiLine,
apiModelId: options.apiModelId,
thinkingBudgetTokens: options.thinkingBudgetTokens,
})
case "doubao":
return new DoubaoHandler(options)
return new DoubaoHandler({
doubaoApiKey: options.doubaoApiKey,
apiModelId: options.apiModelId,
})
case "mistral":
return new MistralHandler(options)
return new MistralHandler({
mistralApiKey: options.mistralApiKey,
apiModelId: options.apiModelId,
})
case "vscode-lm":
return new VsCodeLmHandler(options)
return new VsCodeLmHandler({
vsCodeLmModelSelector: options.vsCodeLmModelSelector,
})
case "cline":
return new ClineHandler(options)
return new ClineHandler({
taskId: options.taskId,
reasoningEffort: options.reasoningEffort,
thinkingBudgetTokens: options.thinkingBudgetTokens,
openRouterProviderSorting: options.openRouterProviderSorting,
openRouterModelId: options.openRouterModelId,
openRouterModelInfo: options.openRouterModelInfo,
})
case "litellm":
return new LiteLlmHandler(options)
return new LiteLlmHandler({
liteLlmApiKey: options.liteLlmApiKey,
liteLlmBaseUrl: options.liteLlmBaseUrl,
liteLlmModelId: options.liteLlmModelId,
liteLlmModelInfo: options.liteLlmModelInfo,
thinkingBudgetTokens: options.thinkingBudgetTokens,
liteLlmUsePromptCache: options.liteLlmUsePromptCache,
taskId: options.taskId,
})
case "moonshot":
return new MoonshotHandler({
moonshotApiKey: options.moonshotApiKey,
moonshotApiLine: options.moonshotApiLine,
apiModelId: options.apiModelId,
})
case "nebius":
return new NebiusHandler(options)
return new NebiusHandler({
nebiusApiKey: options.nebiusApiKey,
apiModelId: options.apiModelId,
})
case "asksage":
return new AskSageHandler(options)
return new AskSageHandler({
asksageApiKey: options.asksageApiKey,
asksageApiUrl: options.asksageApiUrl,
apiModelId: options.apiModelId,
})
case "xai":
return new XAIHandler(options)
return new XAIHandler({
xaiApiKey: options.xaiApiKey,
reasoningEffort: options.reasoningEffort,
apiModelId: options.apiModelId,
})
case "sambanova":
return new SambanovaHandler(options)
return new SambanovaHandler({
sambanovaApiKey: options.sambanovaApiKey,
apiModelId: options.apiModelId,
})
case "cerebras":
return new CerebrasHandler(options)
return new CerebrasHandler({
cerebrasApiKey: options.cerebrasApiKey,
apiModelId: options.apiModelId,
})
case "groq":
return new GroqHandler({
groqApiKey: options.groqApiKey,
groqModelId: options.groqModelId,
groqModelInfo: options.groqModelInfo,
apiModelId: options.apiModelId,
})
case "sapaicore":
return new SapAiCoreHandler(options)
return new SapAiCoreHandler({
sapAiCoreClientId: options.sapAiCoreClientId,
sapAiCoreClientSecret: options.sapAiCoreClientSecret,
sapAiCoreTokenUrl: options.sapAiCoreTokenUrl,
sapAiResourceGroup: options.sapAiResourceGroup,
sapAiCoreBaseUrl: options.sapAiCoreBaseUrl,
apiModelId: options.apiModelId,
})
case "claude-code":
return new ClaudeCodeHandler(options)
return new ClaudeCodeHandler({
claudeCodePath: options.claudeCodePath,
apiModelId: options.apiModelId,
thinkingBudgetTokens: options.thinkingBudgetTokens,
})
default:
return new AnthropicHandler(options)
return new AnthropicHandler({
apiKey: options.apiKey,
anthropicBaseUrl: options.anthropicBaseUrl,
apiModelId: options.apiModelId,
thinkingBudgetTokens: options.thinkingBudgetTokens,
})
}
}
@@ -182,6 +182,24 @@ describe("AwsBedrockHandler", () => {
process.env["AWS_PROFILE"]!.should.equal(preAWSProfile)
})
it("should work with AWS_BEARER_TOKEN_BEDROCK", async () => {
process.env["AWS_BEARER_TOKEN_BEDROCK"] = "test-key"
const preAWSProfile = process.env["AWS_BEARER_TOKEN_BEDROCK"]
await AwsBedrockHandler["withTempEnv"](
() => {
delete process.env["AWS_BEARER_TOKEN_BEDROCK"]
},
async () => {
should.not.exist(process.env["AWS_BEARER_TOKEN_BEDROCK"])
return "test"
},
)
process.env["AWS_BEARER_TOKEN_BEDROCK"]!.should.equal(preAWSProfile)
})
})
const mockOptions: ApiHandlerOptions = {
@@ -192,6 +210,7 @@ describe("AwsBedrockHandler", () => {
awsSessionToken: "",
awsUseProfile: false,
awsProfile: "",
awsBedrockApiKey: "",
awsBedrockUsePromptCache: false,
awsUseCrossRegionInference: false,
awsBedrockEndpoint: "",
+8 -1
View File
@@ -5,11 +5,18 @@ import { anthropicDefaultModelId, AnthropicModelId, anthropicModels, ApiHandlerO
import { ApiHandler } from "../index"
import { ApiStream } from "../transform/stream"
interface AnthropicHandlerOptions {
apiKey?: string
anthropicBaseUrl?: string
apiModelId?: string
thinkingBudgetTokens?: number
}
export class AnthropicHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: Anthropic | undefined
constructor(options: ApiHandlerOptions) {
constructor(options: AnthropicHandlerOptions) {
this.options = options
}
+9 -10
View File
@@ -1,16 +1,15 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ApiHandler } from ".."
import {
ApiHandlerOptions,
ModelInfo,
AskSageModelId,
askSageModels,
askSageDefaultModelId,
askSageDefaultURL,
} from "@shared/api"
import { ModelInfo, AskSageModelId, askSageModels, askSageDefaultModelId, askSageDefaultURL } from "@shared/api"
import { ApiStream } from "../transform/stream"
import { withRetry } from "../retry"
interface AskSageHandlerOptions {
asksageApiKey?: string
asksageApiUrl?: string
apiModelId?: string
}
type AskSageRequest = {
system_prompt: string
message: {
@@ -31,11 +30,11 @@ type AskSageResponse = {
}
export class AskSageHandler implements ApiHandler {
private options: ApiHandlerOptions
private options: AskSageHandlerOptions
private apiUrl: string
private apiKey: string
constructor(options: ApiHandlerOptions) {
constructor(options: AskSageHandlerOptions) {
console.log("init api url", options.asksageApiUrl, askSageDefaultURL)
this.options = options
this.apiKey = options.asksageApiKey || ""
+43 -11
View File
@@ -2,7 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
import { withRetry } from "../retry"
import { ApiHandler } from "../"
import { convertToR1Format } from "../transform/r1-format"
import { ApiHandlerOptions, bedrockDefaultModelId, BedrockModelId, bedrockModels, ModelInfo } from "@shared/api"
import { bedrockDefaultModelId, BedrockModelId, bedrockModels, ModelInfo } from "@shared/api"
import { calculateApiCostOpenAI } from "../../utils/cost"
import { ApiStream } from "../transform/stream"
import { fromNodeProviderChain } from "@aws-sdk/credential-providers"
@@ -16,6 +16,24 @@ import {
// Import proper AWS SDK types
import type { Message, ContentBlock } from "@aws-sdk/client-bedrock-runtime"
interface AwsBedrockHandlerOptions {
apiModelId?: string
awsAccessKey?: string
awsSecretKey?: string
awsSessionToken?: string
awsRegion?: string
awsAuthentication?: string
awsBedrockApiKey?: string
awsUseCrossRegionInference?: boolean
awsBedrockUsePromptCache?: boolean
awsUseProfile?: boolean
awsProfile?: string
awsBedrockEndpoint?: string
awsBedrockCustomSelected?: boolean
awsBedrockCustomModelBaseId?: BedrockModelId
thinkingBudgetTokens?: number
}
// Extend AWS SDK types to include additionalModelResponseFields
interface ExtendedMetadata {
usage?: {
@@ -90,9 +108,9 @@ interface ProviderChainOptions {
// https://docs.anthropic.com/en/api/claude-on-amazon-bedrock
export class AwsBedrockHandler implements ApiHandler {
private options: ApiHandlerOptions
private options: AwsBedrockHandlerOptions
constructor(options: ApiHandlerOptions) {
constructor(options: AwsBedrockHandlerOptions) {
this.options = options
}
@@ -170,7 +188,10 @@ export class AwsBedrockHandler implements ApiHandler {
}> {
// Configure provider options
const providerOptions: ProviderChainOptions = {}
if (this.options.awsUseProfile) {
const useProfile =
(this.options.awsAuthentication === undefined && this.options.awsUseProfile) ||
this.options.awsAuthentication === "profile"
if (useProfile) {
// For profile-based auth, always use ignoreCache to detect credential file changes
// This solves the AWS Identity Manager issue where credential files change externally
providerOptions.ignoreCache = true
@@ -184,7 +205,7 @@ export class AwsBedrockHandler implements ApiHandler {
return await AwsBedrockHandler.withTempEnv(
() => {
AwsBedrockHandler.setEnv("AWS_REGION", this.options.awsRegion)
if (this.options.awsUseProfile) {
if (useProfile) {
AwsBedrockHandler.setEnv("AWS_PROFILE", this.options.awsProfile)
} else {
delete process.env["AWS_PROFILE"]
@@ -208,15 +229,26 @@ export class AwsBedrockHandler implements ApiHandler {
* Creates a BedrockRuntimeClient with the appropriate credentials
*/
private async getBedrockClient(): Promise<BedrockRuntimeClient> {
const credentials = await this.getAwsCredentials()
let auth: any
if (this.options.awsAuthentication === "apikey") {
auth = {
token: { token: this.options.awsBedrockApiKey },
authSchemePreference: ["httpBearerAuth"],
}
} else {
const credentials = await this.getAwsCredentials()
auth = {
credentials: {
accessKeyId: credentials.accessKeyId,
secretAccessKey: credentials.secretAccessKey,
sessionToken: credentials.sessionToken,
},
}
}
return new BedrockRuntimeClient({
region: this.getRegion(),
credentials: {
accessKeyId: credentials.accessKeyId,
secretAccessKey: credentials.secretAccessKey,
sessionToken: credentials.sessionToken,
},
...auth,
...(this.options.awsBedrockEndpoint && { endpoint: this.options.awsBedrockEndpoint }),
})
}
+8 -3
View File
@@ -1,15 +1,20 @@
import { Anthropic } from "@anthropic-ai/sdk"
import Cerebras from "@cerebras/cerebras_cloud_sdk"
import { withRetry } from "../retry"
import { ApiHandlerOptions, ModelInfo, CerebrasModelId, cerebrasDefaultModelId, cerebrasModels } from "@shared/api"
import { ModelInfo, CerebrasModelId, cerebrasDefaultModelId, cerebrasModels } from "@shared/api"
import { ApiHandler } from "../index"
import { ApiStream } from "@api/transform/stream"
interface CerebrasHandlerOptions {
cerebrasApiKey?: string
apiModelId?: string
}
export class CerebrasHandler implements ApiHandler {
private options: ApiHandlerOptions
private options: CerebrasHandlerOptions
private client: Cerebras | undefined
constructor(options: ApiHandlerOptions) {
constructor(options: CerebrasHandlerOptions) {
this.options = options
}
+10 -4
View File
@@ -1,15 +1,21 @@
import type { Anthropic } from "@anthropic-ai/sdk"
import { claudeCodeDefaultModelId, ClaudeCodeModelId, claudeCodeModels, type ApiHandlerOptions } from "@/shared/api"
import { claudeCodeDefaultModelId, ClaudeCodeModelId, claudeCodeModels } from "@/shared/api"
import { type ApiHandler } from ".."
import { ApiStreamUsageChunk, type ApiStream } from "../transform/stream"
import { withRetry } from "../retry"
import { runClaudeCode } from "@/integrations/claude-code/run"
import { filterMessagesForClaudeCode } from "@/integrations/claude-code/message-filter"
export class ClaudeCodeHandler implements ApiHandler {
private options: ApiHandlerOptions
interface ClaudeCodeHandlerOptions {
claudeCodePath?: string
apiModelId?: string
thinkingBudgetTokens?: number
}
constructor(options: ApiHandlerOptions) {
export class ClaudeCodeHandler implements ApiHandler {
private options: ClaudeCodeHandlerOptions
constructor(options: ClaudeCodeHandlerOptions) {
this.options = options
}
+153 -106
View File
@@ -1,157 +1,207 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { ApiHandler } from "../"
import { ApiHandlerOptions, ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "@shared/api"
import { ClineAccountService } from "@/services/account/ClineAccountService"
import { ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "@shared/api"
import { createOpenRouterStream } from "../transform/openrouter-stream"
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
import axios from "axios"
import { OpenRouterErrorResponse } from "./types"
import { withRetry } from "../retry"
import { AuthService } from "@/services/auth/AuthService"
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"
interface ClineHandlerOptions {
taskId?: string
reasoningEffort?: string
thinkingBudgetTokens?: number
openRouterProviderSorting?: string
openRouterModelId?: string
openRouterModelInfo?: ModelInfo
clineAccountId?: string
}
export class ClineHandler implements ApiHandler {
private options: ApiHandlerOptions
private options: ClineHandlerOptions
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"
lastGenerationId?: string
private counter = 0
constructor(options: ApiHandlerOptions) {
constructor(options: ClineHandlerOptions) {
this.options = options
this._authService = AuthService.getInstance()
}
private ensureClient(): OpenAI {
private async ensureClient(): Promise<OpenAI> {
const clineAccountAuthToken = await this._authService.getAuthToken()
if (!clineAccountAuthToken) {
throw new Error(CLINE_ACCOUNT_AUTH_ERROR_MESSAGE)
}
if (!this.client) {
if (!this.options.clineApiKey) {
throw new Error("You don't seem to be logged in to a Cline account.")
}
try {
this.client = new OpenAI({
baseURL: "https://api.cline.bot/v1",
apiKey: this.options.clineApiKey || "",
baseURL: `${this._baseUrl}/api/v1`,
apiKey: clineAccountAuthToken,
defaultHeaders: {
"HTTP-Referer": "https://cline.bot", // Optional, for including your app on cline.bot rankings.
"X-Title": "Cline", // Optional. Shows in rankings on cline.bot.
"X-Task-ID": this.options.taskId || "", // Include the task ID in the request headers
"HTTP-Referer": "https://cline.bot",
"X-Title": "Cline",
"X-Task-ID": this.options.taskId || "",
"X-Cline-Version": extensionVersion,
},
})
} catch (error) {
} catch (error: any) {
throw new Error(`Error creating Cline client: ${error.message}`)
}
}
// Ensure the client is always using the latest auth token
this.client.apiKey = clineAccountAuthToken
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
this.lastGenerationId = undefined
try {
const client = await this.ensureClient()
const stream = await createOpenRouterStream(
client,
systemPrompt,
messages,
this.getModel(),
this.options.reasoningEffort,
this.options.thinkingBudgetTokens,
this.options.openRouterProviderSorting,
)
this.lastGenerationId = undefined
let didOutputUsage: boolean = false
let didOutputUsage: boolean = false
for await (const chunk of stream) {
// openrouter returns an error object instead of the openai sdk throwing an error
if ("error" in chunk) {
const error = chunk.error as OpenRouterErrorResponse["error"]
console.error(`Cline API Error: ${error?.code} - ${error?.message}`)
// Include metadata in the error message if available
const metadataStr = error.metadata ? `\nMetadata: ${JSON.stringify(error.metadata, null, 2)}` : ""
throw new Error(`Cline API Error ${error.code}: ${error.message}${metadataStr}`)
}
const stream = await createOpenRouterStream(
client,
systemPrompt,
messages,
this.getModel(),
this.options.reasoningEffort,
this.options.thinkingBudgetTokens,
this.options.openRouterProviderSorting,
)
if (!this.lastGenerationId && chunk.id) {
this.lastGenerationId = chunk.id
}
// Check for mid-stream error via finish_reason
const choice = chunk.choices?.[0]
// OpenRouter may return finish_reason = "error" with error details
if ((choice?.finish_reason as string) === "error") {
const choiceWithError = choice as any
if (choiceWithError.error) {
const error = choiceWithError.error
console.error(`Cline Mid-Stream Error: ${error.code || error.type || "Unknown"} - ${error.message}`)
throw new Error(`Cline Mid-Stream Error: ${error.code || error.type || "Unknown"} - ${error.message}`)
} else {
throw new Error("Cline Mid-Stream Error: Stream terminated with error status but no error details provided")
for await (const chunk of stream) {
// openrouter returns an error object instead of the openai sdk throwing an error
if ("error" in chunk) {
const error = chunk.error as OpenRouterErrorResponse["error"]
console.error(`Cline API Error: ${error?.code} - ${error?.message}`)
// Include metadata in the error message if available
const metadataStr = error.metadata ? `\nMetadata: ${JSON.stringify(error.metadata, null, 2)}` : ""
throw new Error(`Cline API Error ${error.code}: ${error.message}${metadataStr}`)
}
}
const delta = choice?.delta
if (delta?.content) {
yield {
type: "text",
text: delta.content,
if (!this.lastGenerationId && chunk.id) {
this.lastGenerationId = chunk.id
}
}
// Reasoning tokens are returned separately from the content
if ("reasoning" in delta && delta.reasoning) {
yield {
type: "reasoning",
// Check for mid-stream error via finish_reason
const choice = chunk.choices?.[0]
// OpenRouter may return finish_reason = "error" with error details
if ((choice?.finish_reason as string) === "error") {
const choiceWithError = choice as any
if (choiceWithError.error) {
const error = choiceWithError.error
console.error(`Cline Mid-Stream Error: ${error.code || error.type || "Unknown"} - ${error.message}`)
throw new Error(`Cline Mid-Stream Error: ${error.code || error.type || "Unknown"} - ${error.message}`)
} else {
throw new Error(
"Cline Mid-Stream Error: Stream terminated with error status but no error details provided",
)
}
}
const delta = choice?.delta
if (delta?.content) {
yield {
type: "text",
text: delta.content,
}
}
// Reasoning tokens are returned separately from the content
// Skip reasoning content for Grok 4 models since it only displays "thinking" without providing useful information
if ("reasoning" in delta && delta.reasoning && !shouldSkipReasoningForModel(this.options.openRouterModelId)) {
yield {
type: "reasoning",
// @ts-ignore-next-line
reasoning: delta.reasoning,
}
}
if (!didOutputUsage && chunk.usage) {
// @ts-ignore-next-line
reasoning: delta.reasoning,
}
}
let totalCost = (chunk.usage.cost || 0) + (chunk.usage.cost_details?.upstream_inference_cost || 0)
const modelId = this.getModel().id
if (!didOutputUsage && chunk.usage) {
// @ts-ignore-next-line
let totalCost = (chunk.usage.cost || 0) + (chunk.usage.cost_details?.upstream_inference_cost || 0)
const modelId = this.getModel().id
const provider = modelId.split("/")[0]
// const provider = modelId.split("/")[0]
// // If provider is x-ai, set totalCost to 0 (we're doing a promo)
// if (provider === "x-ai") {
// totalCost = 0
// }
// If provider is x-ai, set totalCost to 0 (we're doing a promo)
if (provider === "x-ai") {
totalCost = 0
}
if (modelId.includes("gemini")) {
yield {
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
inputTokens: (chunk.usage.prompt_tokens || 0) - (chunk.usage.prompt_tokens_details?.cached_tokens || 0),
outputTokens: chunk.usage.completion_tokens || 0,
// @ts-ignore-next-line
totalCost,
}
} else {
yield {
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
// @ts-ignore-next-line
totalCost,
if (modelId.includes("gemini")) {
yield {
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
inputTokens:
(chunk.usage.prompt_tokens || 0) - (chunk.usage.prompt_tokens_details?.cached_tokens || 0),
outputTokens: chunk.usage.completion_tokens || 0,
// @ts-ignore-next-line
totalCost,
}
} else {
yield {
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
// @ts-ignore-next-line
totalCost,
}
}
didOutputUsage = true
}
didOutputUsage = true
}
}
// Fallback to generation endpoint if usage chunk not returned
if (!didOutputUsage) {
const apiStreamUsage = await this.getApiStreamUsage()
if (apiStreamUsage) {
yield apiStreamUsage
// Fallback to generation endpoint if usage chunk not returned
if (!didOutputUsage) {
console.warn("Cline API did not return usage chunk, fetching from generation endpoint")
const apiStreamUsage = await this.getApiStreamUsage()
if (apiStreamUsage) {
yield apiStreamUsage
}
}
} 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
}
}
async getApiStreamUsage(): Promise<ApiStreamUsageChunk | undefined> {
if (this.lastGenerationId) {
try {
const response = await axios.get(`https://api.cline.bot/v1/generation?id=${this.lastGenerationId}`, {
// TODO: replace this with firebase auth
// TODO: use global API Host
const response = await axios.get(`${this.clineAccountService.baseUrl}/generation?id=${this.lastGenerationId}`, {
headers: {
Authorization: `Bearer ${this.options.clineApiKey}`,
Authorization: `Bearer ${this.options.clineAccountId}`,
},
timeout: 15_000, // this request hangs sometimes
})
@@ -189,9 +239,6 @@ export class ClineHandler implements ApiHandler {
getModel(): { id: string; info: ModelInfo } {
let modelId = this.options.openRouterModelId
if (modelId === "x-ai/grok-3") {
modelId = "x-ai/grok-3-beta"
}
const modelInfo = this.options.openRouterModelInfo
if (modelId && modelInfo) {
return { id: modelId, info: modelInfo }
+7 -2
View File
@@ -8,11 +8,16 @@ import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { convertToR1Format } from "../transform/r1-format"
interface DeepSeekHandlerOptions {
deepSeekApiKey?: string
apiModelId?: string
}
export class DeepSeekHandler implements ApiHandler {
private options: ApiHandlerOptions
private options: DeepSeekHandlerOptions
private client: OpenAI | undefined
constructor(options: ApiHandlerOptions) {
constructor(options: DeepSeekHandlerOptions) {
this.options = options
}
+8 -3
View File
@@ -1,15 +1,20 @@
import { ApiHandler } from ".."
import { ApiHandlerOptions, doubaoDefaultModelId, DoubaoModelId, doubaoModels, ModelInfo } from "@shared/api"
import { doubaoDefaultModelId, DoubaoModelId, doubaoModels, ModelInfo } from "@shared/api"
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { withRetry } from "../retry"
interface DoubaoHandlerOptions {
doubaoApiKey?: string
apiModelId?: string
}
export class DoubaoHandler implements ApiHandler {
private options: ApiHandlerOptions
private options: DoubaoHandlerOptions
private client: OpenAI | undefined
constructor(options: ApiHandlerOptions) {
constructor(options: DoubaoHandlerOptions) {
this.options = options
}
+10 -10
View File
@@ -2,22 +2,22 @@ import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { withRetry } from "../retry"
import { ApiHandler } from ".."
import {
ApiHandlerOptions,
DeepSeekModelId,
ModelInfo,
deepSeekDefaultModelId,
deepSeekModels,
openAiModelInfoSaneDefaults,
} from "../../shared/api"
import { ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
interface FireworksHandlerOptions {
fireworksApiKey?: string
fireworksModelId?: string
fireworksModelMaxCompletionTokens?: number
fireworksModelMaxTokens?: number
}
export class FireworksHandler implements ApiHandler {
private options: ApiHandlerOptions
private options: FireworksHandlerOptions
private client: OpenAI | undefined
constructor(options: ApiHandlerOptions) {
constructor(options: FireworksHandlerOptions) {
this.options = options
}
+8 -1
View File
@@ -12,8 +12,15 @@ import { telemetryService } from "@services/posthog/telemetry/TelemetryService"
// Define a default TTL for the cache (e.g., 15 minutes in seconds)
const DEFAULT_CACHE_TTL_SECONDS = 900
interface GeminiHandlerOptions extends ApiHandlerOptions {
interface GeminiHandlerOptions {
isVertex?: boolean
vertexProjectId?: string
vertexRegion?: string
geminiApiKey?: string
geminiBaseUrl?: string
thinkingBudgetTokens?: number
apiModelId?: string
taskId?: string
}
/**
+290
View File
@@ -0,0 +1,290 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { withRetry } from "../retry"
import { ApiHandler } from "../"
import { GroqModelId, ModelInfo, groqDefaultModelId, groqModels } from "@shared/api"
import { calculateApiCostOpenAI } from "../../utils/cost"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
interface GroqHandlerOptions {
groqApiKey?: string
groqModelId?: string
groqModelInfo?: ModelInfo
apiModelId?: string // For backward compatibility
}
// Model family definitions for enhanced behavior
interface GroqModelFamily {
name: string
supportedFeatures: {
streaming: boolean
temperature: boolean
vision: boolean
tools: boolean
}
maxTokensOverride?: number
specialParams?: Record<string, any>
}
const MODEL_FAMILIES: Record<string, GroqModelFamily> = {
// Moonshort 4 Family - Latest generation with vision support
"kimi-k2": {
name: "kimi-k2",
supportedFeatures: { streaming: true, temperature: true, vision: true, tools: true },
maxTokensOverride: 8192,
},
// Llama 4 Family - Latest generation with vision support
llama4: {
name: "Llama 4",
supportedFeatures: { streaming: true, temperature: true, vision: true, tools: true },
maxTokensOverride: 8192,
},
// Llama 3.3 Family - Balanced performance
"llama3.3": {
name: "Llama 3.3",
supportedFeatures: { streaming: true, temperature: true, vision: false, tools: true },
maxTokensOverride: 32768,
},
// Llama 3.1 Family - Fast inference
"llama3.1": {
name: "Llama 3.1",
supportedFeatures: { streaming: true, temperature: true, vision: false, tools: true },
maxTokensOverride: 131072,
},
// DeepSeek Family - Reasoning-optimized
deepseek: {
name: "DeepSeek",
supportedFeatures: { streaming: true, temperature: true, vision: false, tools: true },
maxTokensOverride: 8192,
specialParams: {
top_p: 0.95,
reasoning_format: "parsed",
},
},
// Qwen Family - Enhanced for Q&A
qwen: {
name: "Qwen",
supportedFeatures: { streaming: true, temperature: true, vision: false, tools: true },
maxTokensOverride: 32768,
},
// Compound Models - Hybrid architectures
compound: {
name: "Compound",
supportedFeatures: { streaming: true, temperature: true, vision: false, tools: true },
maxTokensOverride: 8192,
},
}
export class GroqHandler implements ApiHandler {
private options: GroqHandlerOptions
private client: OpenAI | undefined
constructor(options: GroqHandlerOptions) {
this.options = options
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.groqApiKey) {
throw new Error("Groq API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://api.groq.com/openai/v1",
apiKey: this.options.groqApiKey,
})
} catch (error) {
throw new Error(`Error creating Groq client: ${error.message}`)
}
}
return this.client
}
private async *yieldUsage(info: ModelInfo, usage: OpenAI.Completions.CompletionUsage | undefined): ApiStream {
const inputTokens = usage?.prompt_tokens || 0
const outputTokens = usage?.completion_tokens || 0
const totalCost = calculateApiCostOpenAI(info, inputTokens, outputTokens)
yield {
type: "usage",
inputTokens,
outputTokens,
cacheWriteTokens: 0,
cacheReadTokens: 0,
totalCost,
}
}
/**
* Detects the model family based on the model ID
*/
private detectModelFamily(modelId: string): GroqModelFamily {
if (modelId.includes("kimi-k2")) {
return MODEL_FAMILIES["kimi-k2"]
}
// Llama 4 variants
if (modelId.includes("llama-4") || modelId.includes("llama/llama-4")) {
return MODEL_FAMILIES.llama4
}
// Llama 3.3 variants
if (modelId.includes("llama-3.3")) {
return MODEL_FAMILIES["llama3.3"]
}
// Llama 3.1 variants
if (modelId.includes("llama-3.1")) {
return MODEL_FAMILIES["llama3.1"]
}
// DeepSeek variants
if (modelId.includes("deepseek")) {
return MODEL_FAMILIES.deepseek
}
// Qwen variants
if (modelId.includes("qwen")) {
return MODEL_FAMILIES.qwen
}
// Compound variants
if (modelId.includes("compound")) {
return MODEL_FAMILIES.compound
}
// Default fallback to Llama 3.3 behavior
return MODEL_FAMILIES["kimi-k2"]
}
/**
* Gets the optimal max_tokens based on model family and capabilities
*/
private getOptimalMaxTokens(model: { id: string; info: ModelInfo }, modelFamily: GroqModelFamily): number {
// Use model-specific max tokens if available
if (model.info.maxTokens && model.info.maxTokens > 0) {
return model.info.maxTokens
}
// Use family override if available
if (modelFamily.maxTokensOverride) {
return modelFamily.maxTokensOverride
}
// Default fallback
return 8192
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
const modelFamily = this.detectModelFamily(model.id)
// Optimize parameters based on model family
const temperature = 0
const maxTokens = this.getOptimalMaxTokens(model, modelFamily)
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
// Build request parameters with model-specific optimizations
const requestParams: OpenAI.Chat.ChatCompletionCreateParamsStreaming & {
reasoning_format?: "parsed" | "raw" | "hidden"
top_p?: number
} = {
model: model.id,
max_tokens: maxTokens,
messages: openAiMessages,
stream: true,
stream_options: { include_usage: true },
temperature,
}
// Add any special parameters for specific model families
if (modelFamily.specialParams) {
Object.assign(requestParams, modelFamily.specialParams)
}
const stream = await client.chat.completions.create(requestParams)
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
// Handle reasoning field if present (for reasoning models with parsed output)
if ((delta as any)?.reasoning) {
const reasoningContent = (delta as any).reasoning as string
yield {
type: "reasoning",
reasoning: reasoningContent,
}
continue
}
// Handle content field - trust the parsed output from Groq
if (delta?.content) {
yield {
type: "text",
text: delta.content,
}
}
// Handle usage information
if (chunk.usage) {
yield* this.yieldUsage(model.info, chunk.usage)
}
}
}
/**
* Checks if the current model supports vision/images
*/
supportsImages(): boolean {
const model = this.getModel()
return model.info.supportsImages === true
}
/**
* Checks if the current model supports tools
*/
supportsTools(): boolean {
const model = this.getModel()
const modelFamily = this.detectModelFamily(model.id)
return modelFamily.supportedFeatures.tools
}
/**
* Gets model information with enhanced family detection
*/
getModel(): { id: string; info: ModelInfo } {
// First priority: groqModelId and groqModelInfo (like Requesty does)
const groqModelId = this.options.groqModelId
const groqModelInfo = this.options.groqModelInfo
if (groqModelId && groqModelInfo) {
return { id: groqModelId, info: groqModelInfo }
}
// Second priority: groqModelId with static model info
if (groqModelId && groqModelId in groqModels) {
const id = groqModelId as GroqModelId
return { id, info: groqModels[id] }
}
// Third priority: apiModelId (for backward compatibility)
const apiModelId = this.options.apiModelId
if (apiModelId && apiModelId in groqModels) {
const id = apiModelId as GroqModelId
return { id, info: groqModels[id] }
}
// Default fallback
return {
id: groqDefaultModelId,
info: groqModels[groqDefaultModelId],
}
}
/**
* Gets model family information for debugging/introspection
*/
getModelFamily(): GroqModelFamily {
const model = this.getModel()
return this.detectModelFamily(model.id)
}
}
+13 -3
View File
@@ -1,16 +1,26 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { ApiHandlerOptions, liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults } from "@shared/api"
import { liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults, LiteLLMModelInfo } from "@shared/api"
import { ApiHandler } from ".."
import { ApiStream } from "../transform/stream"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { withRetry } from "../retry"
interface LiteLlmHandlerOptions {
liteLlmApiKey?: string
liteLlmBaseUrl?: string
liteLlmModelId?: string
liteLlmModelInfo?: LiteLLMModelInfo
thinkingBudgetTokens?: number
liteLlmUsePromptCache?: boolean
taskId?: string
}
export class LiteLlmHandler implements ApiHandler {
private options: ApiHandlerOptions
private options: LiteLlmHandlerOptions
private client: OpenAI | undefined
constructor(options: ApiHandlerOptions) {
constructor(options: LiteLlmHandlerOptions) {
this.options = options
}
+7 -2
View File
@@ -6,11 +6,16 @@ import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { withRetry } from "../retry"
interface LmStudioHandlerOptions {
lmStudioBaseUrl?: string
lmStudioModelId?: string
}
export class LmStudioHandler implements ApiHandler {
private options: ApiHandlerOptions
private options: LmStudioHandlerOptions
private client: OpenAI | undefined
constructor(options: ApiHandlerOptions) {
constructor(options: LmStudioHandlerOptions) {
this.options = options
}
+8 -3
View File
@@ -2,15 +2,20 @@ import { Anthropic } from "@anthropic-ai/sdk"
import { Mistral } from "@mistralai/mistralai"
import { withRetry } from "../retry"
import { ApiHandler } from "../"
import { ApiHandlerOptions, mistralDefaultModelId, MistralModelId, mistralModels, ModelInfo } from "@shared/api"
import { mistralDefaultModelId, MistralModelId, mistralModels, ModelInfo } from "@shared/api"
import { convertToMistralMessages } from "../transform/mistral-format"
import { ApiStream } from "../transform/stream"
interface MistralHandlerOptions {
mistralApiKey?: string
apiModelId?: string
}
export class MistralHandler implements ApiHandler {
private options: ApiHandlerOptions
private options: MistralHandlerOptions
private client: Mistral | undefined
constructor(options: ApiHandlerOptions) {
constructor(options: MistralHandlerOptions) {
this.options = options
}
+90
View File
@@ -0,0 +1,90 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { withRetry } from "../retry"
import { ApiHandler } from "../index"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { ModelInfo, MoonshotModelId, moonshotModels, moonshotDefaultModelId } from "@/shared/api"
interface MoonshotHandlerOptions {
moonshotApiKey?: string
moonshotApiLine?: string
apiModelId?: string
}
export class MoonshotHandler implements ApiHandler {
private client: OpenAI | undefined
constructor(private readonly options: MoonshotHandlerOptions) {}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.moonshotApiKey) {
throw new Error("Moonshot API key is required")
}
try {
this.client = new OpenAI({
baseURL:
this.options.moonshotApiLine === "china" ? "https://api.moonshot.cn/v1" : "https://api.moonshot.ai/v1",
apiKey: this.options.moonshotApiKey,
})
} catch (error) {
throw new Error(`Error creating Moonshot client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
const stream = await client.chat.completions.create({
model: model.id,
messages: openAiMessages,
temperature: 0,
stream: true,
stream_options: { include_usage: true },
})
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
if (delta?.content) {
yield {
type: "text",
text: delta.content,
}
}
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
yield {
type: "reasoning",
reasoning: (delta.reasoning_content as string | undefined) || "",
}
}
if (chunk.usage) {
yield {
type: "usage",
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
}
}
}
}
getModel(): { id: MoonshotModelId; info: ModelInfo } {
const modelId = this.options.apiModelId
if (modelId && modelId in moonshotModels) {
const id = modelId as MoonshotModelId
return { id, info: moonshotModels[id] }
}
return { id: moonshotDefaultModelId, info: moonshotModels[moonshotDefaultModelId] }
}
}
+7 -2
View File
@@ -5,12 +5,17 @@ import { ApiHandler } from "../index"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { convertToR1Format } from "../transform/r1-format"
import { nebiusDefaultModelId, nebiusModels, type ModelInfo, type ApiHandlerOptions, type NebiusModelId } from "../../shared/api"
import { nebiusDefaultModelId, nebiusModels, type ModelInfo, type NebiusModelId } from "../../shared/api"
interface NebiusHandlerOptions {
nebiusApiKey?: string
apiModelId?: string
}
export class NebiusHandler implements ApiHandler {
private client: OpenAI | undefined
constructor(private readonly options: ApiHandlerOptions) {}
constructor(private readonly options: NebiusHandlerOptions) {}
private ensureClient(): OpenAI {
if (!this.client) {
+9 -2
View File
@@ -6,11 +6,18 @@ import { convertToOllamaMessages } from "../transform/ollama-format"
import { ApiStream } from "../transform/stream"
import { withRetry } from "../retry"
interface OllamaHandlerOptions {
ollamaBaseUrl?: string
ollamaModelId?: string
ollamaApiOptionsCtxNum?: string
requestTimeoutMs?: number
}
export class OllamaHandler implements ApiHandler {
private options: ApiHandlerOptions
private options: OllamaHandlerOptions
private client: Ollama | undefined
constructor(options: ApiHandlerOptions) {
constructor(options: OllamaHandlerOptions) {
this.options = options
}
+8 -2
View File
@@ -8,11 +8,17 @@ import { calculateApiCostOpenAI } from "../../utils/cost"
import { ApiStream } from "../transform/stream"
import type { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
interface OpenAiNativeHandlerOptions {
openAiNativeApiKey?: string
reasoningEffort?: string
apiModelId?: string
}
export class OpenAiNativeHandler implements ApiHandler {
private options: ApiHandlerOptions
private options: OpenAiNativeHandlerOptions
private client: OpenAI | undefined
constructor(options: ApiHandlerOptions) {
constructor(options: OpenAiNativeHandlerOptions) {
this.options = options
}
+13 -3
View File
@@ -1,18 +1,28 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI, { AzureOpenAI } from "openai"
import { withRetry } from "../retry"
import { ApiHandlerOptions, azureOpenAiDefaultApiVersion, ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
import { azureOpenAiDefaultApiVersion, ModelInfo, openAiModelInfoSaneDefaults, OpenAiCompatibleModelInfo } from "@shared/api"
import { ApiHandler } from "../index"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { convertToR1Format } from "../transform/r1-format"
import type { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
interface OpenAiHandlerOptions {
openAiApiKey?: string
openAiBaseUrl?: string
azureApiVersion?: string
openAiHeaders?: Record<string, string>
openAiModelId?: string
openAiModelInfo?: OpenAiCompatibleModelInfo
reasoningEffort?: string
}
export class OpenAiHandler implements ApiHandler {
private options: ApiHandlerOptions
private options: OpenAiHandlerOptions
private client: OpenAI | undefined
constructor(options: ApiHandlerOptions) {
constructor(options: OpenAiHandlerOptions) {
this.options = options
}
+15 -7
View File
@@ -3,18 +3,28 @@ import axios from "axios"
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
import OpenAI from "openai"
import { ApiHandler } from "../"
import { ApiHandlerOptions, ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "@shared/api"
import { ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "@shared/api"
import { withRetry } from "../retry"
import { createOpenRouterStream } from "../transform/openrouter-stream"
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
import { OpenRouterErrorResponse } from "./types"
import { shouldSkipReasoningForModel } from "@utils/model-utils"
interface OpenRouterHandlerOptions {
openRouterApiKey?: string
openRouterModelId?: string
openRouterModelInfo?: ModelInfo
openRouterProviderSorting?: string
reasoningEffort?: string
thinkingBudgetTokens?: number
}
export class OpenRouterHandler implements ApiHandler {
private options: ApiHandlerOptions
private options: OpenRouterHandlerOptions
private client: OpenAI | undefined
lastGenerationId?: string
constructor(options: ApiHandlerOptions) {
constructor(options: OpenRouterHandlerOptions) {
this.options = options
}
@@ -103,7 +113,8 @@ export class OpenRouterHandler implements ApiHandler {
}
// Reasoning tokens are returned separately from the content
if ("reasoning" in delta && delta.reasoning) {
// Skip reasoning content for Grok 4 models since it only displays "thinking" without providing useful information
if ("reasoning" in delta && delta.reasoning && !shouldSkipReasoningForModel(this.options.openRouterModelId)) {
yield {
type: "reasoning",
// @ts-ignore-next-line
@@ -204,9 +215,6 @@ export class OpenRouterHandler implements ApiHandler {
getModel(): { id: string; info: ModelInfo } {
let modelId = this.options.openRouterModelId
if (modelId === "x-ai/grok-3") {
modelId = "x-ai/grok-3-beta"
}
const modelInfo = this.options.openRouterModelInfo
if (modelId && modelInfo) {
return { id: modelId, info: modelInfo }
+9 -3
View File
@@ -2,7 +2,6 @@ import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { ApiHandler } from "../"
import {
ApiHandlerOptions,
ModelInfo,
mainlandQwenModels,
internationalQwenModels,
@@ -16,11 +15,18 @@ import { ApiStream } from "../transform/stream"
import { convertToR1Format } from "../transform/r1-format"
import { withRetry } from "../retry"
interface QwenHandlerOptions {
qwenApiKey?: string
qwenApiLine?: string
apiModelId?: string
thinkingBudgetTokens?: number
}
export class QwenHandler implements ApiHandler {
private options: ApiHandlerOptions
private options: QwenHandlerOptions
private client: OpenAI | undefined
constructor(options: ApiHandlerOptions) {
constructor(options: QwenHandlerOptions) {
this.options = options
}
+10 -2
View File
@@ -7,6 +7,14 @@ import { convertToOpenAiMessages } from "@api/transform/openai-format"
import { calculateApiCostOpenAI } from "@utils/cost"
import { ApiStream } from "@api/transform/stream"
interface RequestyHandlerOptions {
requestyApiKey?: string
reasoningEffort?: string
thinkingBudgetTokens?: number
requestyModelId?: string
requestyModelInfo?: ModelInfo
}
// Requesty usage includes an extra field for Anthropic use cases.
// Safely cast the prompt token details section to the appropriate structure.
interface RequestyUsage extends OpenAI.CompletionUsage {
@@ -18,10 +26,10 @@ interface RequestyUsage extends OpenAI.CompletionUsage {
}
export class RequestyHandler implements ApiHandler {
private options: ApiHandlerOptions
private options: RequestyHandlerOptions
private client: OpenAI | undefined
constructor(options: ApiHandlerOptions) {
constructor(options: RequestyHandlerOptions) {
this.options = options
}
+8 -3
View File
@@ -1,17 +1,22 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { withRetry } from "../retry"
import { ApiHandlerOptions, ModelInfo, SambanovaModelId, sambanovaDefaultModelId, sambanovaModels } from "@shared/api"
import { ModelInfo, SambanovaModelId, sambanovaDefaultModelId, sambanovaModels } from "@shared/api"
import { ApiHandler } from "../index"
import { convertToOpenAiMessages } from "@/api/transform/openai-format"
import { ApiStream } from "@api/transform/stream"
import { convertToR1Format } from "@api/transform/r1-format"
interface SambanovaHandlerOptions {
sambanovaApiKey?: string
apiModelId?: string
}
export class SambanovaHandler implements ApiHandler {
private options: ApiHandlerOptions
private options: SambanovaHandlerOptions
private client: OpenAI | undefined
constructor(options: ApiHandlerOptions) {
constructor(options: SambanovaHandlerOptions) {
this.options = options
}
+15 -7
View File
@@ -2,10 +2,19 @@ import { Anthropic } from "@anthropic-ai/sdk"
import axios from "axios"
import OpenAI from "openai"
import { ApiHandler } from "../"
import { ApiHandlerOptions, ModelInfo, sapAiCoreDefaultModelId, SapAiCoreModelId, sapAiCoreModels } from "../../shared/api"
import { ModelInfo, sapAiCoreDefaultModelId, SapAiCoreModelId, sapAiCoreModels } from "../../shared/api"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
interface SapAiCoreHandlerOptions {
sapAiCoreClientId?: string
sapAiCoreClientSecret?: string
sapAiCoreTokenUrl?: string
sapAiResourceGroup?: string
sapAiCoreBaseUrl?: string
apiModelId?: string
}
interface Deployment {
id: string
name: string
@@ -19,11 +28,11 @@ interface Token {
expires_at: number
}
export class SapAiCoreHandler implements ApiHandler {
private options: ApiHandlerOptions
private options: SapAiCoreHandlerOptions
private token?: Token
private deployments?: Deployment[]
constructor(options: ApiHandlerOptions) {
constructor(options: SapAiCoreHandlerOptions) {
this.options = options
}
@@ -60,6 +69,7 @@ export class SapAiCoreHandler implements ApiHandler {
Authorization: `Bearer ${token}`,
"AI-Resource-Group": this.options.sapAiResourceGroup || "default",
"Content-Type": "application/json",
"AI-Client-Type": "Cline",
}
const url = `${this.options.sapAiCoreBaseUrl}/v2/lm/deployments?$top=10000&$skip=0`
@@ -116,6 +126,7 @@ export class SapAiCoreHandler implements ApiHandler {
Authorization: `Bearer ${token}`,
"AI-Resource-Group": this.options.sapAiResourceGroup || "default",
"Content-Type": "application/json",
"AI-Client-Type": "Cline",
}
const model = this.getModel()
@@ -283,7 +294,6 @@ export class SapAiCoreHandler implements ApiHandler {
const jsonData = line.slice(6)
try {
const data = JSON.parse(jsonData)
console.log("Received data:", data)
if (data.type === "message_start") {
usage.input_tokens = data.message.usage.input_tokens
yield {
@@ -346,7 +356,6 @@ export class SapAiCoreHandler implements ApiHandler {
try {
// Parse the incoming JSON data from the stream
const data = JSON.parse(toStrictJson(jsonData))
console.log("Received data:", data)
// Handle metadata (token usage)
if (data.metadata?.usage) {
@@ -422,7 +431,6 @@ export class SapAiCoreHandler implements ApiHandler {
const jsonData = line.slice(6)
try {
const data = JSON.parse(jsonData)
console.log("Received GPT data:", data)
if (data.choices && data.choices.length > 0) {
const choice = data.choices[0]
@@ -446,7 +454,7 @@ export class SapAiCoreHandler implements ApiHandler {
}
}
if (data.choices && data.choices[0].finish_reason === "stop") {
if (data.choices?.[0]?.finish_reason === "stop") {
// Final usage yield, if not already provided
if (!data.usage) {
yield {
+8 -3
View File
@@ -1,17 +1,22 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { withRetry } from "../retry"
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
import { ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
import { ApiHandler } from "../index"
import { convertToOpenAiMessages } from "@api/transform/openai-format"
import { ApiStream } from "@api/transform/stream"
import { convertToR1Format } from "@api/transform/r1-format"
interface TogetherHandlerOptions {
togetherApiKey?: string
togetherModelId?: string
}
export class TogetherHandler implements ApiHandler {
private options: ApiHandlerOptions
private options: TogetherHandlerOptions
private client: OpenAI | undefined
constructor(options: ApiHandlerOptions) {
constructor(options: TogetherHandlerOptions) {
this.options = options
}
+12 -2
View File
@@ -6,12 +6,22 @@ import { ApiHandlerOptions, ModelInfo, vertexDefaultModelId, VertexModelId, vert
import { ApiStream } from "@api/transform/stream"
import { GeminiHandler } from "./gemini"
interface VertexHandlerOptions {
vertexProjectId?: string
vertexRegion?: string
apiModelId?: string
thinkingBudgetTokens?: number
geminiApiKey?: string
geminiBaseUrl?: string
taskId?: string
}
export class VertexHandler implements ApiHandler {
private geminiHandler: GeminiHandler | undefined
private clientAnthropic: AnthropicVertex | undefined
private options: ApiHandlerOptions
private options: VertexHandlerOptions
constructor(options: ApiHandlerOptions) {
constructor(options: VertexHandlerOptions) {
this.options = options
}
+7 -3
View File
@@ -5,10 +5,14 @@ import { calculateApiCostAnthropic } from "@utils/cost"
import { ApiStream } from "@api/transform/stream"
import { convertToVsCodeLmMessages } from "@api/transform/vscode-lm-format"
import { SELECTOR_SEPARATOR, stringifyVsCodeLmModelSelector } from "@shared/vsCodeSelectorUtils"
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
import { ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
import type { LanguageModelChatSelector as LanguageModelChatSelectorFromTypes } from "./types"
import { withRetry } from "../retry"
interface VsCodeLmHandlerOptions {
vsCodeLmModelSelector?: any
}
// Cline does not update VSCode type definitions or engine requirements to maintain compatibility.
// This declaration (as seen in src/integrations/TerminalManager.ts) provides types for the Language Model API in newer versions of VSCode.
// Extracted from https://github.com/microsoft/vscode/blob/131ee0ef660d600cd0a7e6058375b281553abe20/src/vscode-dts/vscode.d.ts
@@ -124,12 +128,12 @@ declare module "vscode" {
* ```
*/
export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
private options: ApiHandlerOptions
private options: VsCodeLmHandlerOptions
private client: vscode.LanguageModelChat | null
private disposable: vscode.Disposable | null
private currentRequestCancellation: vscode.CancellationTokenSource | null
constructor(options: ApiHandlerOptions) {
constructor(options: VsCodeLmHandlerOptions) {
this.options = options
this.client = null
this.disposable = null
+17 -7
View File
@@ -1,17 +1,24 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { ApiHandler } from "../"
import { ApiHandlerOptions, XAIModelId, ModelInfo, xaiDefaultModelId, xaiModels } from "@shared/api"
import { XAIModelId, ModelInfo, xaiDefaultModelId, xaiModels } from "@shared/api"
import { convertToOpenAiMessages } from "@api/transform/openai-format"
import { ApiStream } from "@api/transform/stream"
import { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
import { withRetry } from "../retry"
import { shouldSkipReasoningForModel } from "@utils/model-utils"
interface XAIHandlerOptions {
xaiApiKey?: string
reasoningEffort?: string
apiModelId?: string
}
export class XAIHandler implements ApiHandler {
private options: ApiHandlerOptions
private options: XAIHandlerOptions
private client: OpenAI | undefined
constructor(options: ApiHandlerOptions) {
constructor(options: XAIHandlerOptions) {
this.options = options
}
@@ -64,10 +71,13 @@ export class XAIHandler implements ApiHandler {
}
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
yield {
type: "reasoning",
// @ts-ignore-next-line
reasoning: delta.reasoning_content,
// Skip reasoning content for Grok 4 models since it only displays "thinking" without providing useful information
if (!shouldSkipReasoningForModel(modelId)) {
yield {
type: "reasoning",
// @ts-ignore-next-line
reasoning: delta.reasoning_content,
}
}
}
+6
View File
@@ -139,6 +139,10 @@ export async function createOpenRouterStream(
shouldApplyMiddleOutTransform = true
}
// hardcoded provider sorting for kimi-k2
const isKimiK2 = model.id.startsWith("moonshotai/kimi-k2")
openRouterProviderSorting = isKimiK2 ? undefined : openRouterProviderSorting
// @ts-ignore-next-line
const stream = await client.chat.completions.create({
model: model.id,
@@ -153,6 +157,8 @@ export async function createOpenRouterStream(
...(model.id.startsWith("openai/o") ? { reasoning_effort: reasoningEffort || "medium" } : {}),
...(reasoning ? { reasoning } : {}),
...(openRouterProviderSorting ? { provider: { sort: openRouterProviderSorting } } : {}),
// limit providers to only those that support the 131k context window
...(isKimiK2 ? { provider: { order: ["groq", "together"], allow_fallbacks: false } } : {}),
})
return stream
@@ -6,9 +6,9 @@ import * as path from "path"
import { FileContextTracker } from "./FileContextTracker"
import * as diskModule from "@core/storage/disk"
import type { TaskMetadata, FileMetadataEntry } from "./ContextTrackerTypes"
import type { WebviewProviderCreator } from "@/hosts/host-providers"
import type { DiffViewProviderCreator, WebviewProviderCreator } from "@/hosts/host-providers"
import * as hostProviders from "@hosts/host-providers"
import { vscodeHostBridgeClient } from "@/hosts/vscode/client/host-grpc-client"
import { vscodeHostBridgeClient } from "@/hosts/vscode/hostbridge/client/host-grpc-client"
describe("FileContextTracker", () => {
let sandbox: sinon.SinonSandbox
@@ -53,7 +53,11 @@ describe("FileContextTracker", () => {
mockTaskMetadata = { files_in_context: [], model_usage: [] }
getTaskMetadataStub = sandbox.stub(diskModule, "getTaskMetadata").resolves(mockTaskMetadata)
saveTaskMetadataStub = sandbox.stub(diskModule, "saveTaskMetadata").resolves()
hostProviders.initializeHostProviders(((_) => {}) as WebviewProviderCreator, vscodeHostBridgeClient)
hostProviders.initializeHostProviders(
((_) => {}) as WebviewProviderCreator,
(() => {}) as DiffViewProviderCreator,
vscodeHostBridgeClient,
)
// Create tracker instance
taskId = "test-task-id"
@@ -1,8 +1,9 @@
import * as vscode from "vscode"
import crypto from "crypto"
import { Controller } from "../index"
import { storeSecret } from "../../storage/state"
import { AuthService } from "@/services/auth/AuthService"
import { EmptyRequest, String } from "../../../shared/proto/common"
import { openExternal } from "@utils/env"
const authService = AuthService.getInstance()
/**
* Handles the user clicking the login link in the UI.
@@ -13,21 +14,5 @@ import { EmptyRequest, String } from "../../../shared/proto/common"
* @returns The login URL as a string.
*/
export async function accountLoginClicked(controller: Controller, _: EmptyRequest): Promise<String> {
// Generate nonce for state validation
const nonce = crypto.randomBytes(32).toString("hex")
await storeSecret(controller.context, "authNonce", nonce)
// Open browser for authentication with state param
console.log("Login button clicked in account page")
console.log("Opening auth page with state param")
const uriScheme = vscode.env.uriScheme
const authUrl = vscode.Uri.parse(
`https://app.cline.bot/auth?state=${encodeURIComponent(nonce)}&callback_url=${encodeURIComponent(`${uriScheme || "vscode"}://saoudrizwan.claude-dev/auth`)}`,
)
await vscode.env.openExternal(authUrl)
return String.create({
value: authUrl.toString(),
})
return await authService.createAuthRequest()
}
@@ -1,7 +1,9 @@
import { AuthService } from "@/services/auth/AuthService"
import { Empty } from "../../../shared/proto/common"
import type { EmptyRequest } from "../../../shared/proto/common"
import type { Controller } from "../index"
const authService = AuthService.getInstance()
/**
* Handles the account logout action
* @param controller The controller instance
@@ -10,5 +12,6 @@ import type { Controller } from "../index"
*/
export async function accountLogoutClicked(controller: Controller, _request: EmptyRequest): Promise<Empty> {
await controller.handleSignOut()
await authService.handleDeauth()
return Empty.create({})
}
@@ -1,4 +1,4 @@
import { AuthStateChangedRequest, AuthStateChanged } from "@shared/proto/account"
import { AuthStateChangedRequest, AuthState } from "@shared/proto/account"
import type { Controller } from "../index"
import { updateGlobalState } from "../../storage/state"
@@ -9,13 +9,13 @@ import { updateGlobalState } from "../../storage/state"
* @param request The auth state change request
* @returns The updated user info
*/
export async function authStateChanged(controller: Controller, request: AuthStateChangedRequest): Promise<AuthStateChanged> {
export async function authStateChanged(controller: Controller, request: AuthStateChangedRequest): Promise<AuthState> {
try {
// Store the user info directly in global state
await updateGlobalState(controller.context, "userInfo", request.user)
// Return the same user info
return AuthStateChanged.create({ user: request.user })
return AuthState.create({ user: request.user })
} catch (error) {
console.error(`Failed to update auth state: ${error}`)
throw error
@@ -0,0 +1,50 @@
import type { Controller } from "../index"
import { GetOrganizationCreditsRequest, OrganizationCreditsData, OrganizationUsageTransaction } from "@shared/proto/account"
/**
* Handles fetching all organization credits data (balance, usage, payments)
* @param controller The controller instance
* @param request Organization credits request
* @returns Organization credits data response
*/
export async function getOrganizationCredits(
controller: Controller,
request: GetOrganizationCreditsRequest,
): Promise<OrganizationCreditsData> {
try {
if (!controller.accountService) {
throw new Error("Account service not available")
}
// Call the individual RPC variants in parallel
const [balanceData, usageTransactions] = await Promise.all([
controller.accountService.fetchOrganizationCreditsRPC(request.organizationId),
controller.accountService.fetchOrganizationUsageTransactionsRPC(request.organizationId),
])
return OrganizationCreditsData.create({
balance: balanceData ? { currentBalance: balanceData.balance / 100 } : { currentBalance: 0 },
organizationId: balanceData?.organizationId || "",
usageTransactions:
usageTransactions?.map((tx) =>
OrganizationUsageTransaction.create({
aiInferenceProviderName: tx.aiInferenceProviderName,
aiModelName: tx.aiModelName,
aiModelTypeName: tx.aiModelTypeName,
completionTokens: tx.completionTokens,
costUsd: tx.costUsd,
createdAt: tx.createdAt,
creditsUsed: tx.creditsUsed,
generationId: tx.generationId,
organizationId: tx.organizationId,
promptTokens: tx.promptTokens,
totalTokens: tx.totalTokens,
userId: tx.userId,
}),
) || [],
})
} catch (error) {
console.error(`Failed to fetch organization credits data: ${error}`)
throw error
}
}
@@ -8,7 +8,7 @@ import { UserCreditsData } from "@shared/proto/account"
* @param request Empty request
* @returns User credits data response
*/
export async function fetchUserCreditsData(controller: Controller, request: EmptyRequest): Promise<UserCreditsData> {
export async function getUserCredits(controller: Controller, request: EmptyRequest): Promise<UserCreditsData> {
try {
if (!controller.accountService) {
throw new Error("Account service not available")
@@ -21,11 +21,10 @@ export async function fetchUserCreditsData(controller: Controller, request: Empt
controller.accountService.fetchPaymentTransactionsRPC(),
])
// Since generated types match exactly, no conversion needed!
return UserCreditsData.create({
balance: balance ? { currentBalance: balance.currentBalance } : { currentBalance: 0 },
usageTransactions: usageTransactions || [],
paymentTransactions: paymentTransactions || [],
balance: balance ? { currentBalance: balance.balance / 100 } : { currentBalance: 0 },
usageTransactions: usageTransactions,
paymentTransactions: paymentTransactions,
})
} catch (error) {
console.error(`Failed to fetch user credits data: ${error}`)
@@ -0,0 +1,35 @@
import type { Controller } from "../index"
import type { EmptyRequest } from "@shared/proto/common"
import { UserOrganization, UserOrganizationsResponse } from "@shared/proto/account"
/**
* Handles fetching all user credits data (balance, usage, payments)
* @param controller The controller instance
* @param request Empty request
* @returns User credits data response
*/
export async function getUserOrganizations(controller: Controller, request: EmptyRequest): Promise<UserOrganizationsResponse> {
try {
if (!controller.accountService) {
throw new Error("Account service not available")
}
// Fetch user organizations from the account service
const organizations = await controller.accountService.fetchUserOrganizationsRPC()
return UserOrganizationsResponse.create({
organizations:
organizations?.map((org) =>
UserOrganization.create({
active: org.active,
memberId: org.memberId,
name: org.name,
organizationId: org.organizationId,
roles: org.roles ? [...org.roles] : [],
}),
) || [],
})
} catch (error) {
throw error
}
}
@@ -0,0 +1,24 @@
import type { Controller } from "../index"
import { Empty } from "@shared/proto/common"
import { UserOrganizationUpdateRequest } from "@shared/proto/account"
/**
* Handles setting the user's active organization
* @param controller The controller instance
* @param request UserOrganization to set as active
* @returns Empty response
*/
export async function setUserOrganization(controller: Controller, request: UserOrganizationUpdateRequest): Promise<Empty> {
try {
if (!controller.accountService) {
throw new Error("Account service not available")
}
// Switch to the specified organization using the account service
await controller.accountService.switchAccount(request.organizationId)
return Empty.create({})
} catch (error) {
throw error
}
}
@@ -1,59 +0,0 @@
import { Controller } from "../index"
import { EmptyRequest } from "../../../shared/proto/common"
import { String as ProtoString } from "../../../shared/proto/common"
import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler"
// Keep track of active authCallback subscriptions
const activeAuthCallbackSubscriptions = new Set<StreamingResponseHandler>()
/**
* Subscribe to authCallback events
* @param controller The controller instance
* @param request The empty request
* @param responseStream The streaming response handler
* @param requestId The ID of the request (passed by the gRPC handler)
*/
export async function subscribeToAuthCallback(
controller: Controller,
request: EmptyRequest,
responseStream: StreamingResponseHandler,
requestId?: string,
): Promise<void> {
// Add this subscription to the active subscriptions
activeAuthCallbackSubscriptions.add(responseStream)
// Register cleanup when the connection is closed
const cleanup = () => {
activeAuthCallbackSubscriptions.delete(responseStream)
}
// Register the cleanup function with the request registry if we have a requestId
if (requestId) {
getRequestRegistry().registerRequest(requestId, cleanup, { type: "authCallback_subscription" }, responseStream)
}
}
/**
* Send an authCallback event to all active subscribers
* @param customToken The custom token for authentication
*/
export async function sendAuthCallbackEvent(customToken: string): Promise<void> {
// Send the event to all active subscribers
const promises = Array.from(activeAuthCallbackSubscriptions).map(async (responseStream) => {
try {
const event: ProtoString = {
value: customToken,
}
await responseStream(
event,
false, // Not the last message
)
} catch (error) {
console.error("Error sending authCallback event:", error)
// Remove the subscription if there was an error
activeAuthCallbackSubscriptions.delete(responseStream)
}
})
await Promise.all(promises)
}
@@ -0,0 +1,5 @@
import { AuthService } from "../../../services/auth/AuthService"
const authService = AuthService.getInstance()
export const subscribeToAuthStatusUpdate = authService.subscribeToAuthStatusUpdate.bind(authService)
export const sendAuthStatusUpdateEvent = authService.sendAuthStatusUpdate.bind(authService)
+15 -4
View File
@@ -3,11 +3,12 @@ import { RuleFileRequest, RuleFile } from "@shared/proto/file"
import { FileMethodHandler } from "./index"
import { refreshClineRulesToggles } from "@core/context/instructions/user-instructions/cline-rules"
import { createRuleFile as createRuleFileImpl } from "@core/context/instructions/user-instructions/rule-helpers"
import * as vscode from "vscode"
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 { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
/**
* Creates a rule file in either global or workspace rules directory
@@ -42,7 +43,13 @@ export const createRuleFile: FileMethodHandler = async (controller: Controller,
const fileTypeName = request.type === "workflow" ? "workflow" : "rule"
if (fileExists) {
vscode.window.showWarningMessage(`${fileTypeName} file "${request.filename}" already exists.`)
const message = `${fileTypeName} file "${request.filename}" already exists.`
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.WARNING,
message,
}),
)
// Still open it for editing
await handleFileServiceRequest(controller, "openFile", { value: filePath })
} else {
@@ -55,8 +62,12 @@ export const createRuleFile: FileMethodHandler = async (controller: Controller,
await handleFileServiceRequest(controller, "openFile", { value: filePath })
vscode.window.showInformationMessage(
`Created new ${request.isGlobal ? "global" : "workspace"} ${fileTypeName} file: ${request.filename}`,
const message = `Created new ${request.isGlobal ? "global" : "workspace"} ${fileTypeName} file: ${request.filename}`
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message,
}),
)
}
+9 -2
View File
@@ -1,9 +1,10 @@
import { deleteRuleFile as deleteRuleFileImpl } from "@core/context/instructions/user-instructions/rule-helpers"
import { RuleFile, RuleFileRequest } from "@shared/proto/file"
import * as path from "path"
import * as vscode from "vscode"
import { Controller } from ".."
import { FileMethodHandler } from "./index"
import { getHostBridgeProvider } from "@/hosts/host-providers"
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
/**
* Deletes a rule file from either global or workspace rules directory
@@ -44,7 +45,13 @@ export const deleteRuleFile: FileMethodHandler = async (controller: Controller,
const fileTypeName = request.type === "workflow" ? "workflow" : "rule"
vscode.window.showInformationMessage(`${fileTypeName} file "${fileName}" deleted successfully`)
const message = `${fileTypeName} file "${fileName}" deleted successfully`
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message,
}),
)
return RuleFile.create({
filePath: request.rulePath,
+29 -44
View File
@@ -1,10 +1,10 @@
import { Controller } from ".."
import { RelativePathsRequest, RelativePaths } from "@shared/proto/file"
import { FileMethodHandler } from "./index"
import * as vscode from "vscode"
import { asRelativePath } from "@/utils/path"
import { RelativePaths, RelativePathsRequest } from "@shared/proto/file"
import * as path from "path"
import { StringRequest } from "@shared/proto/common"
import { getHostBridgeProvider } from "@hosts/host-providers"
import { URI } from "vscode-uri"
import { Controller } from ".."
import { FileMethodHandler } from "./index"
import { isDirectory } from "@/utils/fs"
/**
* Converts a list of URIs to workspace-relative paths
@@ -13,47 +13,32 @@ import { getHostBridgeProvider } from "@hosts/host-providers"
* @returns Response with resolved relative paths
*/
export const getRelativePaths: FileMethodHandler = async (
controller: Controller,
_controller: Controller,
request: RelativePathsRequest,
): Promise<RelativePaths> => {
const resolvedPaths = await Promise.all(
request.uris.map(async (uriString) => {
try {
// Use the host URI service client instead of directly using vscode.Uri.parse
const parseResponse = await getHostBridgeProvider().uriServiceClient.parse(
StringRequest.create({
value: uriString,
}),
)
const fileUri = vscode.Uri.parse(`${parseResponse.scheme}://${parseResponse.authority}${parseResponse.path}`)
console.log("[DEBUG] UriServiceClient.parse:", fileUri)
const relativePathToGet = vscode.workspace.asRelativePath(fileUri, false)
const result = []
for (const uriString of request.uris) {
try {
result.push(await getRelativePath(uriString))
} catch (error) {
console.error(`Error calculating relative path for ${uriString}:`, error)
}
}
return RelativePaths.create({ paths: result })
}
// If the path is still absolute, it's outside the workspace
if (path.isAbsolute(relativePathToGet)) {
console.warn(`Dropped file ${relativePathToGet} is outside the workspace. Sending original path.`)
return fileUri.fsPath.replace(/\\/g, "/")
} else {
let finalPath = "/" + relativePathToGet.replace(/\\/g, "/")
try {
const stat = await vscode.workspace.fs.stat(fileUri)
if (stat.type === vscode.FileType.Directory) {
finalPath += "/"
}
} catch (statError) {
console.error(`Error stating file ${fileUri.fsPath}:`, statError)
}
return finalPath
}
} catch (error) {
console.error(`Error calculating relative path for ${uriString}:`, error)
return null
}
}),
)
async function getRelativePath(uriString: string): Promise<string> {
const filePath = URI.parse(uriString, true).fsPath
const relativePath = await asRelativePath(filePath)
// Filter out any null values from errors
const validPaths = resolvedPaths.filter((path): path is string => path !== null)
// If the path is still absolute, it's outside the workspace
if (path.isAbsolute(relativePath)) {
throw new Error(`Dropped file ${relativePath} is outside the workspace.`)
}
return RelativePaths.create({ paths: validPaths })
let result = "/" + relativePath.replace(/\\/g, "/")
if (await isDirectory(filePath)) {
result += "/"
}
return result
}
+114 -62
View File
@@ -27,21 +27,15 @@ import pWaitFor from "p-wait-for"
import * as path from "path"
import * as vscode from "vscode"
import { ensureMcpServersDirectoryExists, ensureSettingsDirectoryExists, GlobalFileNames } from "../storage/disk"
import {
getAllExtensionState,
getGlobalState,
getSecret,
getWorkspaceState,
storeSecret,
updateGlobalState,
updateWorkspaceState,
} from "../storage/state"
import { getAllExtensionState, getGlobalState, getWorkspaceState, storeSecret, updateGlobalState } from "../storage/state"
import { Task } from "../task"
import { sendAuthCallbackEvent } from "./account/subscribeToAuthCallback"
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
@@ -54,11 +48,11 @@ export class Controller {
private postMessage: (message: ExtensionMessage) => Thenable<boolean> | undefined
private disposables: vscode.Disposable[] = []
private mode: "plan" | "act" = "plan" // In-memory plan/act mode state
task?: Task
workspaceTracker: WorkspaceTracker
mcpHub: McpHub
accountService: ClineAccountService
authService: AuthService
latestAnnouncementId = "june-25-2025_16:11:00" // update to some unique identifier when we add a new announcement
constructor(
@@ -78,10 +72,9 @@ export class Controller {
(msg) => this.postMessageToWebview(msg),
this.context.extension?.packageJSON?.version ?? "1.0.0",
)
this.accountService = new ClineAccountService(async () => {
const { apiConfiguration } = await this.getStateToPostToWebview()
return apiConfiguration?.clineApiKey
})
this.accountService = ClineAccountService.getInstance()
this.authService = AuthService.getInstance(context)
this.authService.restoreRefreshTokenAndRetrieveAuthInfo()
// Clean up legacy checkpoints
cleanupLegacyCheckpoints(this.context.globalStorageUri.fsPath, this.outputChannel).catch((error) => {
@@ -89,6 +82,10 @@ export class Controller {
})
}
private async getCurrentMode(): Promise<"plan" | "act"> {
return ((await getGlobalState(this.context, "mode")) as "plan" | "act" | undefined) || "act"
}
/*
VSCode extensions use the disposable pattern to clean up resources when the sidebar/editor tab is closed by the user or system. This applies to event listening, commands, interacting with the UI, etc.
- https://vscode-docs.readthedocs.io/en/stable/extensions/patterns-and-principles/
@@ -111,13 +108,24 @@ export class Controller {
// Auth methods
async handleSignOut() {
try {
await storeSecret(this.context, "clineApiKey", undefined)
// 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 this.postStateToWebview()
vscode.window.showInformationMessage("Successfully logged out of Cline")
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: "Successfully logged out of Cline",
}),
)
} catch (error) {
vscode.window.showErrorMessage("Logout failed")
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: "Logout failed",
}),
)
}
}
@@ -141,10 +149,13 @@ export class Controller {
taskHistory,
} = await getAllExtensionState(this.context)
// Reconstruct ChatSettings with in-memory mode and stored preferences
// Get current mode using helper function
const currentMode = await this.getCurrentMode()
// Reconstruct ChatSettings with mode from global state and stored preferences
const chatSettings: ChatSettings = {
...storedChatSettings, // Spread stored preferences (preferredLanguage, openAIReasoningEffort)
mode: this.mode, // Use in-memory mode (override any stored mode)
mode: currentMode, // Use mode from global state
}
const NEW_USER_TASK_COUNT_THRESHOLD = 10
@@ -239,8 +250,8 @@ export class Controller {
async togglePlanActModeWithChatSettings(chatSettings: ChatSettings, chatContent?: ChatContent): Promise<boolean> {
const didSwitchToActMode = chatSettings.mode === "act"
// Store mode in-memory only
this.mode = chatSettings.mode
// Store mode to global state
await updateGlobalState(this.context, "mode", chatSettings.mode)
// Capture mode switch telemetry | Capture regardless of if we know the taskId
telemetryService.captureModeSwitch(this.task?.taskId ?? "0", chatSettings.mode)
@@ -394,7 +405,7 @@ export class Controller {
}
}
// Save only non-mode properties to workspace storage
// Save only non-mode properties to global storage
const { mode, ...persistentChatSettings }: { mode: string } & StoredChatSettings = chatSettings
await updateGlobalState(this.context, "chatSettings", persistentChatSettings)
await this.postStateToWebview()
@@ -450,24 +461,9 @@ export class Controller {
}
}
// Auth
public async validateAuthState(state: string | null): Promise<boolean> {
const storedNonce = await getSecret(this.context, "authNonce")
if (!state || state !== storedNonce) {
return false
}
await storeSecret(this.context, "authNonce", undefined) // Clear after use
return true
}
async handleAuthCallback(customToken: string, apiKey: string) {
async handleAuthCallback(customToken: string, provider: string | null = null) {
try {
// Store API key for API calls
await storeSecret(this.context, "clineApiKey", apiKey)
// Send custom token to webview for Firebase auth
await sendAuthCallbackEvent(customToken)
await this.authService.handleAuthCallback(customToken, provider ? provider : "google")
const clineProvider: ApiProvider = "cline"
await updateGlobalState(this.context, "apiProvider", clineProvider)
@@ -475,12 +471,10 @@ export class Controller {
// Mark welcome view as completed since user has successfully logged in
await updateGlobalState(this.context, "welcomeViewCompleted", true)
// Update API configuration with the new provider and API key
const { apiConfiguration } = await getAllExtensionState(this.context)
const updatedConfig = {
...apiConfiguration,
apiProvider: clineProvider,
clineApiKey: apiKey,
}
if (this.task) {
@@ -488,17 +482,20 @@ export class Controller {
}
await this.postStateToWebview()
// vscode.window.showInformationMessage("Successfully logged in to Cline")
} catch (error) {
console.error("Failed to handle auth callback:", error)
vscode.window.showErrorMessage("Failed to log in to Cline")
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
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
}
}
// MCP Marketplace
private async fetchMcpMarketplaceFromApi(silent: boolean = false): Promise<McpMarketplaceCatalog | undefined> {
try {
const response = await axios.get("https://api.cline.bot/v1/mcp/marketplace", {
@@ -527,7 +524,12 @@ 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"
vscode.window.showErrorMessage(errorMessage)
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: errorMessage,
}),
)
}
return undefined
}
@@ -611,7 +613,12 @@ 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"
vscode.window.showErrorMessage(errorMessage)
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: errorMessage,
}),
)
}
}
@@ -822,7 +829,7 @@ export class Controller {
chatSettings: storedChatSettings,
userInfo,
mcpMarketplaceEnabled,
mcpRichDisplayEnabled,
mcpDisplayMode,
telemetrySetting,
planActSeparateModelsSetting,
enableCheckpointsSetting,
@@ -837,10 +844,13 @@ export class Controller {
terminalOutputLineLimit,
} = await getAllExtensionState(this.context)
// Reconstruct ChatSettings with in-memory mode and stored preferences
// Get current mode using helper function
const currentMode = await this.getCurrentMode()
// Reconstruct ChatSettings with mode from global state and stored preferences
const chatSettings: ChatSettings = {
...storedChatSettings, // Spread stored preferences (preferredLanguage, openAIReasoningEffort)
mode: this.mode, // Use in-memory mode (override any stored mode)
mode: currentMode, // Use mode from global state
}
const localClineRulesToggles =
@@ -872,7 +882,7 @@ export class Controller {
chatSettings,
userInfo,
mcpMarketplaceEnabled,
mcpRichDisplayEnabled,
mcpDisplayMode,
telemetrySetting,
planActSeparateModelsSetting,
enableCheckpointsSetting: enableCheckpointsSetting ?? true,
@@ -972,14 +982,24 @@ export class Controller {
// Check if there's a workspace folder open
const cwd = await getCwd()
if (!cwd) {
vscode.window.showErrorMessage("No workspace folder open")
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") {
vscode.window.showInformationMessage("No changes in workspace for commit message")
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: "No changes in workspace for commit message",
}),
)
return
}
@@ -1046,27 +1066,59 @@ Commit message:`
if (api && api.repositories.length > 0) {
const repo = api.repositories[0]
repo.inputBox.value = commitMessage
vscode.window.showInformationMessage("Commit message generated and applied")
const message = "Commit message generated and applied"
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message,
}),
)
} else {
vscode.window.showErrorMessage("No Git repositories found")
const message = "No Git repositories found"
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message,
}),
)
}
} else {
vscode.window.showErrorMessage("Git extension not found")
const message = "Git extension not found"
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message,
}),
)
}
} else {
vscode.window.showErrorMessage("Failed to generate commit message")
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)
vscode.window.showErrorMessage(`Failed to generate commit message: ${innerErrorMessage}`)
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)
vscode.window.showErrorMessage(`Failed to generate commit message: ${errorMessage}`)
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Failed to generate commit message: ${errorMessage}`,
}),
)
}
}
// dev
}
+26 -18
View File
@@ -1,6 +1,7 @@
import { Controller } from ".."
import { Empty, StringRequest } from "../../../shared/proto/common"
import { McpServer, McpDownloadResponse } from "@shared/mcp"
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"
@@ -9,9 +10,9 @@ import { sendChatButtonClickedEvent } from "../ui/subscribeToChatButtonClicked"
* Download an MCP server from the marketplace
* @param controller The controller instance
* @param request The request containing the MCP ID
* @returns Empty response
* @returns MCP download response with details or error
*/
export async function downloadMcp(controller: Controller, request: StringRequest): Promise<Empty> {
export async function downloadMcp(controller: Controller, request: StringRequest): Promise<McpDownloadResponse> {
try {
// Check if mcpId is provided
if (!request.value) {
@@ -54,12 +55,6 @@ export async function downloadMcp(controller: Controller, request: StringRequest
throw new Error("Missing README content in MCP download response")
}
// Send details to webview
await controller.postMessageToWebview({
type: "mcpDownloadDetails",
mcpDownloadDetails: mcpDetails,
})
// Create task with context from README and added guidelines for MCP server installation
const task = `Set up the MCP server from ${mcpDetails.githubUrl} while adhering to these MCP server installation rules:
- Start by loading the MCP documentation.
@@ -80,8 +75,17 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
await controller.initTask(task)
await sendChatButtonClickedEvent(controller.id)
// Return an empty response - the client only cares if the call succeeded
return Empty.create()
// Return the download details directly
return McpDownloadResponse.create({
mcpId: mcpDetails.mcpId,
githubUrl: mcpDetails.githubUrl,
name: mcpDetails.name,
author: mcpDetails.author,
description: mcpDetails.description,
readmeContent: mcpDetails.readmeContent,
llmsInstallationContent: mcpDetails.llmsInstallationContent,
requiresApiKey: mcpDetails.requiresApiKey,
})
} catch (error) {
console.error("Failed to download MCP:", error)
let errorMessage = "Failed to download MCP"
@@ -100,13 +104,17 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
errorMessage = error.message
}
// Show error in both notification and marketplace UI
vscode.window.showErrorMessage(errorMessage)
await controller.postMessageToWebview({
type: "mcpDownloadDetails",
// Return error in the response instead of throwing
return McpDownloadResponse.create({
mcpId: "",
githubUrl: "",
name: "",
author: "",
description: "",
readmeContent: "",
llmsInstallationContent: "",
requiresApiKey: false,
error: errorMessage,
})
throw error
}
}
@@ -0,0 +1,254 @@
import { Controller } from ".."
import { EmptyRequest } from "../../../shared/proto/common"
import { OpenRouterCompatibleModelInfo, OpenRouterModelInfo } from "../../../shared/proto/models"
import { getAllExtensionState } from "../../storage/state"
import { groqModels } from "../../../shared/api"
import axios from "axios"
import path from "path"
import fs from "fs/promises"
import { fileExistsAtPath } from "@utils/fs"
import { GlobalFileNames } from "@core/storage/disk"
/**
* Refreshes the Groq models and returns the updated model list
* @param controller The controller instance
* @param request Empty request object
* @returns Response containing the Groq models
*/
export async function refreshGroqModels(controller: Controller, request: EmptyRequest): Promise<OpenRouterCompatibleModelInfo> {
const groqModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.groqModels)
// Get the Groq API key from the controller's state
const { apiConfiguration } = await getAllExtensionState(controller.context)
const groqApiKey = apiConfiguration?.groqApiKey
let models: Record<string, Partial<OpenRouterModelInfo>> = {}
try {
if (!groqApiKey) {
console.log("No Groq API key found, using static models as fallback")
// Don't throw an error, just use static models
for (const [modelId, modelInfo] of Object.entries(groqModels)) {
models[modelId] = {
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 || `${modelId} model`,
}
}
} else {
// Ensure the API key is properly formatted
const cleanApiKey = groqApiKey.trim()
if (!cleanApiKey.startsWith("gsk_")) {
throw new Error("Invalid Groq API key format. Groq API keys should start with 'gsk_'")
}
console.log("Fetching Groq models with API key:", cleanApiKey.substring(0, 10) + "...")
const response = await axios.get("https://api.groq.com/openai/v1/models", {
headers: {
Authorization: `Bearer ${cleanApiKey}`,
"Content-Type": "application/json",
"User-Agent": "Cline-VSCode-Extension",
},
timeout: 10000, // 10 second timeout
})
if (response.data?.data) {
const rawModels = response.data.data
for (const rawModel of rawModels) {
// Filter out non-chat models and validate model capabilities
if (!isValidChatModel(rawModel)) {
continue
}
// Check if we have static pricing information for this model
const staticModelInfo = groqModels[rawModel.id as keyof typeof groqModels]
const modelInfo: Partial<OpenRouterModelInfo> = {
maxTokens: rawModel.max_completion_tokens || staticModelInfo?.maxTokens || 8192,
contextWindow: rawModel.context_window || staticModelInfo?.contextWindow || 8192,
supportsImages: detectImageSupport(rawModel, staticModelInfo),
supportsPromptCache: staticModelInfo?.supportsPromptCache || false,
inputPrice: staticModelInfo?.inputPrice || 0,
outputPrice: staticModelInfo?.outputPrice || 0,
cacheWritesPrice: (staticModelInfo as any)?.cacheWritesPrice || 0,
cacheReadsPrice: (staticModelInfo as any).cacheReadsPrice || 0,
description: generateModelDescription(rawModel, staticModelInfo),
}
models[rawModel.id] = modelInfo
}
} else {
console.error("Invalid response from Groq API")
}
await fs.writeFile(groqModelsFilePath, JSON.stringify(models))
console.log("Groq models fetched and saved", models)
}
} catch (error) {
console.error("Error fetching Groq models:", error)
// Provide more specific error messages
let errorMessage = "Unknown error occurred"
if (axios.isAxiosError(error)) {
if (error.response?.status === 401) {
errorMessage = "Invalid Groq API key. Please check your API key in settings."
} else if (error.response?.status === 403) {
errorMessage = "Access forbidden. Please verify your Groq API key has the correct permissions."
} else if (error.response?.status === 429) {
errorMessage = "Rate limit exceeded. Please try again later."
} else if (error.code === "ECONNABORTED") {
errorMessage = "Request timeout. Please check your internet connection."
} else {
errorMessage = `API request failed: ${error.response?.status || error.code || "Unknown error"}`
}
} else if (error instanceof Error) {
errorMessage = error.message
}
console.error("Groq API Error:", errorMessage)
// If we failed to fetch models, try to read cached models first
const cachedModels = await readGroqModels(controller)
if (cachedModels && Object.keys(cachedModels).length > 0) {
console.log("Using cached Groq models")
models = cachedModels
} else {
// Fall back to static models from shared/api.ts
console.log("Using static Groq models as fallback")
for (const [modelId, modelInfo] of Object.entries(groqModels)) {
models[modelId] = {
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 || `${modelId} model`,
}
}
}
}
// Convert the Record<string, Partial<OpenRouterModelInfo>> to Record<string, OpenRouterModelInfo>
// by filling in any missing required fields with defaults
const typedModels: Record<string, OpenRouterModelInfo> = {}
for (const [key, model] of Object.entries(models)) {
typedModels[key] = {
maxTokens: model.maxTokens ?? 8192,
contextWindow: model.contextWindow ?? 8192,
supportsImages: model.supportsImages ?? false,
supportsPromptCache: model.supportsPromptCache ?? false,
inputPrice: model.inputPrice ?? 0,
outputPrice: model.outputPrice ?? 0,
cacheWritesPrice: model.cacheWritesPrice ?? 0,
cacheReadsPrice: model.cacheReadsPrice ?? 0,
description: model.description ?? "",
tiers: model.tiers ?? [],
}
}
return OpenRouterCompatibleModelInfo.create({ models: typedModels })
}
/**
* Reads cached Groq models from disk
*/
async function readGroqModels(controller: Controller): Promise<Record<string, Partial<OpenRouterModelInfo>> | undefined> {
const groqModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.groqModels)
const fileExists = await fileExistsAtPath(groqModelsFilePath)
if (fileExists) {
try {
const fileContents = await fs.readFile(groqModelsFilePath, "utf8")
return JSON.parse(fileContents)
} catch (error) {
console.error("Error reading cached Groq models:", error)
return undefined
}
}
return undefined
}
/**
* Validates if a model is suitable for chat completions
*/
function isValidChatModel(rawModel: any): boolean {
// Check if model is active (if the property exists)
if (rawModel.hasOwnProperty("active") && !rawModel.active) {
return false
}
// Filter out non-chat models (whisper, TTS, guard models, etc.)
if (
rawModel.id.includes("whisper") ||
rawModel.id.includes("tts") ||
rawModel.id.includes("guard") ||
rawModel.id.includes("embedding") ||
rawModel.id.includes("moderation") ||
rawModel.id.includes("allam")
) {
return false
}
// Check if model supports chat completions
if (rawModel.object === "model" && rawModel.id) {
return true
}
return false
}
/**
* Detects if a model supports image input
*/
function detectImageSupport(rawModel: any, staticModelInfo?: any): boolean {
// Use static info if available
if (staticModelInfo?.supportsImages !== undefined) {
return staticModelInfo.supportsImages
}
// Detect based on model name patterns
const modelId = rawModel.id.toLowerCase()
if (modelId.includes("vision") || modelId.includes("maverick") || modelId.includes("scout")) {
return true
}
return false
}
/**
* Generates a descriptive name for the model
*/
function generateModelDescription(rawModel: any, staticModelInfo?: any): string {
// Use static description if available
if (staticModelInfo?.description) {
return staticModelInfo.description
}
// Generate description based on model characteristics
const modelId = rawModel.id
const contextWindow = rawModel.context_window || 8192
const ownedBy = rawModel.owned_by || "Unknown"
// Special handling for new models
if (modelId.includes("compound")) {
return `${ownedBy}'s ${modelId} model with ${contextWindow.toLocaleString()} token context window - Advanced compound architecture`
}
return `${ownedBy} model with ${contextWindow.toLocaleString()} token context window`
}
/**
* 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")
await fs.mkdir(cacheDir, { recursive: true })
return cacheDir
}
@@ -101,9 +101,15 @@ export async function refreshOpenRouterModels(
break
case "x-ai/grok-3-beta":
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = 0
modelInfo.cacheWritesPrice = 0.75
modelInfo.cacheReadsPrice = 0
break
case "moonshotai/kimi-k2":
// forcing kimi-k2 to use the together provider for full context and best throughput
modelInfo.inputPrice = 1
modelInfo.outputPrice = 3
modelInfo.contextWindow = 131_000
break
default:
if (rawModel.id.startsWith("openai/")) {
modelInfo.cacheReadsPrice = parsePrice(rawModel.pricing?.input_cache_read)
@@ -122,11 +128,6 @@ export async function refreshOpenRouterModels(
break
}
// add new model id
if (rawModel.id === "x-ai/grok-3-beta") {
models["x-ai/grok-3"] = modelInfo
}
models[rawModel.id] = modelInfo
}
} else {
+26 -5
View File
@@ -2,8 +2,9 @@ import { Controller } from ".."
import { Empty } from "../../../shared/proto/common"
import { ResetStateRequest } from "../../../shared/proto/state"
import { resetGlobalState, resetWorkspaceState } from "../../../core/storage/state"
import * as vscode from "vscode"
import { sendChatButtonClickedEvent } from "../ui/subscribeToChatButtonClicked"
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
import { getHostBridgeProvider } from "@/hosts/host-providers"
/**
* Resets the extension state to its defaults
@@ -14,10 +15,20 @@ import { sendChatButtonClickedEvent } from "../ui/subscribeToChatButtonClicked"
export async function resetState(controller: Controller, request: ResetStateRequest): Promise<Empty> {
try {
if (request.global) {
vscode.window.showInformationMessage("Resetting global state...")
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: "Resetting global state...",
}),
)
await resetGlobalState(controller.context)
} else {
vscode.window.showInformationMessage("Resetting workspace state...")
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: "Resetting workspace state...",
}),
)
await resetWorkspaceState(controller.context)
}
@@ -26,7 +37,12 @@ export async function resetState(controller: Controller, request: ResetStateRequ
controller.task = undefined
}
vscode.window.showInformationMessage("State reset")
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: "State reset",
}),
)
await controller.postStateToWebview()
await sendChatButtonClickedEvent(controller.id)
@@ -34,7 +50,12 @@ export async function resetState(controller: Controller, request: ResetStateRequ
return Empty.create()
} catch (error) {
console.error("Error resetting state:", error)
vscode.window.showErrorMessage(`Failed to reset state: ${error instanceof Error ? error.message : String(error)}`)
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Failed to reset state: ${error instanceof Error ? error.message : String(error)}`,
}),
)
throw error
}
}
@@ -1,8 +1,9 @@
import * as vscode from "vscode"
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 { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
export async function updateDefaultTerminalProfile(
controller: Controller,
@@ -25,16 +26,25 @@ export async function updateDefaultTerminalProfile(
// Show information message if terminals were closed
if (closedCount > 0) {
vscode.window.showInformationMessage(
`Closed ${closedCount} ${closedCount === 1 ? "terminal" : "terminals"} with different profile.`,
const message = `Closed ${closedCount} ${closedCount === 1 ? "terminal" : "terminals"} with different profile.`
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message,
}),
)
}
// Show warning if there are busy terminals that couldn't be closed
if (busyTerminals.length > 0) {
vscode.window.showWarningMessage(
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.`,
`Close ${busyTerminals.length === 1 ? "it" : "them"} to use the new profile for all commands.`
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.WARNING,
message,
}),
)
}
}
+13 -4
View File
@@ -50,15 +50,24 @@ export async function updateSettings(controller: Controller, request: UpdateSett
await controller.context.globalState.update("mcpResponsesCollapsed", request.mcpResponsesCollapsed)
}
// Update MCP responses collapsed setting
if (request.mcpRichDisplayEnabled !== undefined) {
await controller.context.globalState.update("mcpRichDisplayEnabled", request.mcpRichDisplayEnabled)
// Update MCP display mode setting
if (request.mcpDisplayMode !== undefined) {
await controller.context.globalState.update("mcpDisplayMode", request.mcpDisplayMode)
}
// Update chat settings
if (request.chatSettings) {
const chatSettings = convertProtoChatSettingsToChatSettings(request.chatSettings)
await controller.context.workspaceState.update("chatSettings", chatSettings)
// Store mode to global state
if (chatSettings.mode !== undefined) {
await controller.context.globalState.update("mode", chatSettings.mode)
}
// Store chat settings (excluding mode) to global state
const { mode, ...globalChatSettings } = chatSettings
await controller.context.globalState.update("chatSettings", globalChatSettings)
if (controller.task) {
controller.task.chatSettings = chatSettings
}
@@ -4,7 +4,8 @@ import { Controller } from ".."
import { DeleteAllTaskHistoryCount } from "../../../shared/proto/task"
import { getGlobalState, updateGlobalState } from "../../storage/state"
import { fileExistsAtPath } from "../../../utils/fs"
import vscode from "vscode"
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
import { getHostBridgeProvider } from "@/hosts/host-providers"
/**
* Deletes all task history, with an option to preserve favorites
@@ -21,12 +22,18 @@ export async function deleteAllTaskHistory(controller: Controller): Promise<Dele
const taskHistory = ((await getGlobalState(controller.context, "taskHistory")) as any[]) || []
const totalTasks = taskHistory.length
const userChoice = await vscode.window.showWarningMessage(
"What would you like to delete?",
{ modal: true },
"Delete All Except Favorites",
"Delete Everything",
)
const userChoice = (
await getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.WARNING,
message: "What would you like to delete?",
options: {
modal: true,
items: ["Delete All Except Favorites", "Delete Everything"],
},
}),
)
)?.selectedOption
// Default VS Code Cancel button returns `undefined` - don't delete anything
if (userChoice === undefined) {
@@ -59,11 +66,18 @@ export async function deleteAllTaskHistory(controller: Controller): Promise<Dele
})
} else {
// No favorited tasks found - show warning and ask user what to do
const answer = await vscode.window.showWarningMessage(
"No favorited tasks found. Would you like to delete all tasks anyway?",
{ modal: true },
"Delete All Tasks",
)
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
// User cancelled - don't delete anything
if (answer === undefined) {
@@ -91,8 +105,11 @@ export async function deleteAllTaskHistory(controller: Controller): Promise<Dele
await fs.rm(checkpointsDirPath, { recursive: true, force: true })
}
} catch (error) {
vscode.window.showErrorMessage(
`Encountered error while deleting task history, there may be some files left behind. Error: ${error instanceof Error ? error.message : String(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)}`,
}),
)
}

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