Compare commits

...
Author SHA1 Message Date
celestial-vault 30c03ba18b adjust wording 2025-07-14 11:13:23 -07:00
celestial-vault ae01f0f1ba changeset 2025-07-14 11:12:43 -07:00
celestial-vault 85ba2d3816 add kimi k2 as trending model 2025-07-14 11:12:14 -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
celestial-vaultandSaoud Rizwan 7ab6189595 mark welcomeViewCompleted as true after auth redirect (#4699)
* mark welcomeViewCompleted as true after auth redirect

* Create real-ravens-nail.md

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-07-07 13:14:48 -07:00
celestial-vault 4beaa2a086 sync local search state after timeout (#4698) 2025-07-07 11:16:08 -07:00
Ara 5f90018ab5 Revert "remove blur (#4664)" (#4677) 2025-07-06 12:05:30 -06:00
celestial-vault 9e761cd1f0 lazily initialize provider sdks with error catching (#4661) 2025-07-06 11:41:16 -05:00
吴天一 e84f2ff962 remove blur (#4664) 2025-07-06 00:13:23 -05:00
tjandy98 1842254c57 Add support for Gemini 2.5 Pro and Flash models to SAP AI Core Provider (#4655)
* 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

---------

Signed-off-by: tjandy98 <3953059+tjandy98@users.noreply.github.com>
2025-07-05 12:10:00 -05:00
pashpashpashandCline Evaluation 4a2dad4552 showOpenDialogue host bridge migration (#4651)
* showOpenDialogue host bridge migration

* addressing comments

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-07-03 22:55:19 -07:00
Sarah Fortune 3248c37358 Replace uses of vscode workspaceFolders with host bridge (#4649)
* Instead of getting the cwd from the workspaceFolders use the host bridge util getCwd()

Replace uses in integrations/claude-code/run.ts

* Organize imports
2025-07-03 15:53:53 -07:00
Sarah Fortune 172b46f1b0 Remove unused files *.bak (#4648)
* Remove unused file Checkpoint-test-utils.ts.bak

* Remove all .bak files from src/integrations
2025-07-03 15:36:21 -07:00
Sarah Fortune 9578d7cde1 Instead of getting the cwd from the workspaceFolders use the host bridge util getCwd() (#4647)
Remove top-level property cwd task, await can't be used at the top level.
Make cwd a class property, and pass the cwd into the constructor of task (await can't be used in the constructor either).
2025-07-03 15:36:08 -07:00
Sarah Fortune 2979d47e01 Replace cwd from the workspaceFolders with the host bridge. (#4645)
Don't export cwd from task/index.ts. This top-level property cwd will be removed in a following PR because await cannot be used at the top-level.
Use the hostbridge util getCwd() in createRuleFile.ts and refreshRules.ts instead of import the cwd from `task`.
Add a util function to get the desktop directory instead of constructing it multiple places, update uses with the new function getDesktopDir()
Replace function getCwd in FileContextTracker.ts with just getCwd from paths.ts.
2025-07-03 15:08:52 -07:00
178 changed files with 5155 additions and 3174 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Add Kimi-K2 as the trending model in the Cline Provider
@@ -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",
{
+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": [
+7 -2
View File
@@ -2,20 +2,24 @@
.vscode/**
.vscode-test/**
out/**
dist-standalone/**
node_modules/**
src/**
standalone/**
.gitignore
.yarnrc
esbuild.js
vsc-extension-quickstart.md
**/tsconfig.json
**/.eslintrc.json
**/*.map
**/*.ts
**/.vscode-test.*
eslint-rules/**
.github/**
.husky/**
# Custom
demo.gif
**/demo.gif
.nvmrc
.gitattributes
.prettierignore
@@ -32,6 +36,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
+53
View File
@@ -1,5 +1,58 @@
# Changelog
## [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 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!)
@@ -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,
},
})
}
},
}
},
})
+198 -436
View File
@@ -1,12 +1,12 @@
{
"name": "claude-dev",
"version": "3.18.3",
"version": "3.18.14",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.18.3",
"version": "3.18.14",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/sdk": "^0.37.0",
@@ -51,6 +51,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",
@@ -104,6 +105,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",
@@ -2065,17 +2067,6 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/@eslint/eslintrc/node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -2085,19 +2076,6 @@
"node": ">= 4"
}
},
"node_modules/@eslint/eslintrc/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/@eslint/js": {
"version": "8.57.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz",
@@ -2945,30 +2923,6 @@
"node": ">=10.10.0"
}
},
"node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/@humanwhocodes/config-array/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/@humanwhocodes/module-importer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
@@ -4049,17 +4003,6 @@
"node": ">= 6.0.0"
}
},
"node_modules/@mapbox/node-pre-gyp/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/@mapbox/node-pre-gyp/node_modules/detect-libc": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
@@ -4132,19 +4075,6 @@
"semver": "bin/semver.js"
}
},
"node_modules/@mapbox/node-pre-gyp/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/@mapbox/node-pre-gyp/node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@@ -7473,16 +7403,6 @@
"ajv": "^8.0.1"
}
},
"node_modules/@stoplight/spectral-core/node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/@stoplight/spectral-core/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
@@ -7498,18 +7418,6 @@
"node": ">=6"
}
},
"node_modules/@stoplight/spectral-core/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/@stoplight/spectral-core/node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@@ -7803,6 +7711,21 @@
"path-browserify": "^1.0.1"
}
},
"node_modules/@ts-morph/common/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@tsconfig/node10": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
@@ -8314,6 +8237,22 @@
"node": ">= 4"
}
},
"node_modules/@typescript-eslint/parser/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@typescript-eslint/parser/node_modules/slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@@ -8515,6 +8454,22 @@
"node": ">= 4"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@@ -8640,6 +8595,22 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@typescript-eslint/utils/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@typescript-eslint/utils/node_modules/ts-api-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
@@ -8733,6 +8704,22 @@
"fsevents": "~2.3.2"
}
},
"node_modules/@vscode/test-cli/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@vscode/test-cli/node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -12050,17 +12037,6 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint/node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/eslint/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -12100,19 +12076,6 @@
"node": ">= 4"
}
},
"node_modules/eslint/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/eslint/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@@ -12454,16 +12417,6 @@
"readable-stream": "^3.4.0"
}
},
"node_modules/exceljs/node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/exceljs/node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
@@ -12537,18 +12490,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/exceljs/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/exceljs/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
@@ -13256,17 +13197,6 @@
"node": "^10.12.0 || >=12.0.0"
}
},
"node_modules/flat-cache/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/flat-cache/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -13289,19 +13219,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/flat-cache/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/flat-cache/node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@@ -13523,16 +13440,6 @@
"node": ">=0.6"
}
},
"node_modules/fstream/node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/fstream/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -13554,18 +13461,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/fstream/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/fstream/node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
@@ -13918,6 +13813,21 @@
"node": ">= 6"
}
},
"node_modules/glob/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/globals": {
"version": "13.24.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
@@ -15876,6 +15786,15 @@
"safe-buffer": "^5.0.1"
}
},
"node_modules/jwt-decode": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/katex": {
"version": "0.16.22",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.22.tgz",
@@ -17927,18 +17846,25 @@
}
},
"node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
"brace-expansion": "^1.1.7"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
"node": "*"
}
},
"node_modules/minimatch/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/minimist": {
@@ -18632,17 +18558,6 @@
"node": ">=4"
}
},
"node_modules/npm-run-all/node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/npm-run-all/node_modules/chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
@@ -18712,19 +18627,6 @@
"node": ">=4"
}
},
"node_modules/npm-run-all/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/npm-run-all/node_modules/path-key": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
@@ -22400,17 +22302,6 @@
"node": ">=8"
}
},
"node_modules/test-exclude/node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/test-exclude/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -22433,19 +22324,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/test-exclude/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/text-decoder": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.0.tgz",
@@ -25709,30 +25587,11 @@
"strip-json-comments": "^3.1.1"
},
"dependencies": {
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
"dev": true
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}
}
}
},
@@ -26467,27 +26326,6 @@
"@humanwhocodes/object-schema": "^2.0.2",
"debug": "^4.3.1",
"minimatch": "^3.0.5"
},
"dependencies": {
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}
}
}
},
"@humanwhocodes/module-importer": {
@@ -27114,16 +26952,6 @@
"debug": "4"
}
},
"brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"detect-libc": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
@@ -27171,15 +26999,6 @@
}
}
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@@ -29708,16 +29527,6 @@
"dev": true,
"requires": {}
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
@@ -29730,15 +29539,6 @@
"integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==",
"dev": true
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@@ -29989,6 +29789,16 @@
"fast-glob": "^3.3.2",
"minimatch": "^9.0.4",
"path-browserify": "^1.0.1"
},
"dependencies": {
"minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"requires": {
"brace-expansion": "^2.0.1"
}
}
}
},
"@tsconfig/node10": {
@@ -30388,6 +30198,15 @@
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
"dev": true
},
"minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"requires": {
"brace-expansion": "^2.0.1"
}
},
"slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@@ -30500,6 +30319,15 @@
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
"dev": true
},
"minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"requires": {
"brace-expansion": "^2.0.1"
}
},
"slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@@ -30570,6 +30398,15 @@
"integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
"dev": true
},
"minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"requires": {
"brace-expansion": "^2.0.1"
}
},
"ts-api-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
@@ -30633,6 +30470,15 @@
"readdirp": "~3.6.0"
}
},
"minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"requires": {
"brace-expansion": "^2.0.1"
}
},
"readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -32825,16 +32671,6 @@
"text-table": "^0.2.0"
},
"dependencies": {
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -32860,15 +32696,6 @@
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
"dev": true
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@@ -33145,15 +32972,6 @@
"readable-stream": "^3.4.0"
}
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
@@ -33196,14 +33014,6 @@
"path-is-absolute": "^1.0.0"
}
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"requires": {
"brace-expansion": "^1.1.7"
}
},
"readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
@@ -33689,16 +33499,6 @@
"rimraf": "^3.0.2"
},
"dependencies": {
"brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -33713,15 +33513,6 @@
"path-is-absolute": "^1.0.0"
}
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@@ -33867,15 +33658,6 @@
"rimraf": "2"
},
"dependencies": {
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -33889,14 +33671,6 @@
"path-is-absolute": "^1.0.0"
}
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"requires": {
"brace-expansion": "^1.1.7"
}
},
"mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
@@ -34125,6 +33899,16 @@
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^1.11.1"
},
"dependencies": {
"minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"requires": {
"brace-expansion": "^2.0.1"
}
}
}
},
"glob-parent": {
@@ -35454,6 +35238,11 @@
"safe-buffer": "^5.0.1"
}
},
"jwt-decode": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA=="
},
"katex": {
"version": "0.16.22",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.22.tgz",
@@ -36828,11 +36617,22 @@
"dev": true
},
"minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"requires": {
"brace-expansion": "^2.0.1"
"brace-expansion": "^1.1.7"
},
"dependencies": {
"brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
}
}
},
"minimist": {
@@ -37306,16 +37106,6 @@
"color-convert": "^1.9.0"
}
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
@@ -37367,15 +37157,6 @@
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"dev": true
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"path-key": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
@@ -39931,16 +39712,6 @@
"minimatch": "^3.0.4"
},
"dependencies": {
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -39954,15 +39725,6 @@
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}
}
}
},
+3 -1
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.18.14",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -392,6 +392,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",
@@ -448,6 +449,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",
+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
View File
@@ -27,5 +27,6 @@ export const hostServiceNameMap = {
workspace: "host.WorkspaceService",
env: "host.EnvService",
window: "host.WindowService",
diff: "host.DiffService",
// Add new host services here
}
+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
+25
View File
@@ -0,0 +1,25 @@
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);
}
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 {
// TODO(sfortune) the host needs to return a unique id for the diff editor.
}
+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;
}
+41
View File
@@ -10,6 +10,8 @@ import "common.proto";
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 {
@@ -30,3 +32,42 @@ message TextEditorInfo {
optional int32 view_column = 2;
bool is_active = 3;
}
message ShowOpenDialogueRequest {
cline.Metadata metadata = 1;
optional bool can_select_many = 2;
optional string open_label = 3;
optional ShowOpenDialogueFilterOption filters = 4;
}
message ShowOpenDialogueFilterOption {
repeated string files = 1;
}
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;
}
+2 -3
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
@@ -165,7 +164,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;
+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
+2 -3
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);
@@ -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;
+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);
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()
+168 -28
View File
@@ -38,62 +38,202 @@ 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,
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 "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 "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,
})
}
}
+6 -3
View File
@@ -45,8 +45,10 @@ describe("OllamaHandler", () => {
this.skip()
}
this.timeout(5000)
// Ensure client is initialized
const client = (handler as any).ensureClient()
// Mock the Ollama client's chat method
const chatStub = sinon.stub(handler["client"], "chat").resolves({
const chatStub = sinon.stub(client, "chat").resolves({
[Symbol.asyncIterator]: async function* () {
yield {
message: { content: "Hello, world!" },
@@ -139,8 +141,9 @@ describe("OllamaHandler", () => {
// Restore real timers for this test
clock.restore()
// Mock the Ollama client's chat method to fail on first call and succeed on second
const chatStub = sinon.stub(handler["client"], "chat")
// Ensure client is initialized and mock the Ollama client's chat method to fail on first call and succeed on second
const client = (handler as any).ensureClient()
const chatStub = sinon.stub(client, "chat")
// First call throws an error
chatStub.onFirstCall().rejects(new Error("API Error"))
+30 -8
View File
@@ -5,20 +5,42 @@ 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
private client: Anthropic | undefined
constructor(options: ApiHandlerOptions) {
constructor(options: AnthropicHandlerOptions) {
this.options = options
this.client = new Anthropic({
apiKey: this.options.apiKey,
baseURL: this.options.anthropicBaseUrl || undefined,
})
}
private ensureClient(): Anthropic {
if (!this.client) {
if (!this.options.apiKey) {
throw new Error("Anthropic API key is required")
}
try {
this.client = new Anthropic({
apiKey: this.options.apiKey,
baseURL: this.options.anthropicBaseUrl || undefined,
})
} catch (error) {
throw new Error(`Error creating Anthropic client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
let stream: AnthropicStream<Anthropic.RawMessageStreamEvent>
const modelId = model.id
@@ -44,7 +66,7 @@ export class AnthropicHandler implements ApiHandler {
)
const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
stream = await this.client.messages.create(
stream = await client.messages.create(
{
model: modelId,
thinking: reasoningOn ? { type: "enabled", budget_tokens: budget_tokens } : undefined,
@@ -118,7 +140,7 @@ export class AnthropicHandler implements ApiHandler {
break
}
default: {
stream = await this.client.messages.create({
stream = await client.messages.create({
model: modelId,
max_tokens: model.info.maxTokens || 8192,
temperature: 0,
+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 || ""
+19 -3
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,22 @@ 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
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 +106,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
}
+30 -14
View File
@@ -1,32 +1,48 @@
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 client: Cerebras
private options: CerebrasHandlerOptions
private client: Cerebras | undefined
constructor(options: ApiHandlerOptions) {
constructor(options: CerebrasHandlerOptions) {
this.options = options
}
// Clean and validate the API key
const cleanApiKey = this.options.cerebrasApiKey?.trim()
private ensureClient(): Cerebras {
if (!this.client) {
// Clean and validate the API key
const cleanApiKey = this.options.cerebrasApiKey?.trim()
if (!cleanApiKey) {
throw new Error("Cerebras API key is required")
if (!cleanApiKey) {
throw new Error("Cerebras API key is required")
}
try {
this.client = new Cerebras({
apiKey: cleanApiKey,
timeout: 30000, // 30 second timeout
})
} catch (error) {
throw new Error(`Error creating Cerebras client: ${error.message}`)
}
}
this.client = new Cerebras({
apiKey: cleanApiKey,
timeout: 30000, // 30 second timeout
})
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
// Convert Anthropic messages to Cerebras format
const cerebrasMessages: Array<{
role: "system" | "user" | "assistant"
@@ -81,7 +97,7 @@ export class CerebrasHandler implements ApiHandler {
}
try {
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: this.getModel().id,
messages: cerebrasMessages,
temperature: 0,
+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
}
+165 -104
View File
@@ -1,143 +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"
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 client: OpenAI
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.client = new OpenAI({
baseURL: "https://api.cline.bot/v1",
apiKey: this.options.clineApiKey || "",
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
},
})
this._authService = AuthService.getInstance()
}
private async ensureClient(): Promise<OpenAI> {
const clineAccountAuthToken = await this._authService.getAuthToken()
if (!clineAccountAuthToken) {
throw new Error("Cline account authentication token is required")
}
if (!this.client) {
try {
this.client = new OpenAI({
baseURL: `${this._baseUrl}/api/v1`,
apiKey: clineAccountAuthToken,
defaultHeaders: {
"HTTP-Referer": "https://cline.bot",
"X-Title": "Cline",
"X-Task-ID": this.options.taskId || "",
"X-Cline-Version": extensionVersion,
},
})
} 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 {
this.lastGenerationId = undefined
try {
// Only continue the request if the user:
// 1. Has signed in to Cline with a token
// 2. Has more than 0 credits
// Or an error is thrown.
await this.clineAccountService.validateRequest()
const stream = await createOpenRouterStream(
this.client,
systemPrompt,
messages,
this.getModel(),
this.options.reasoningEffort,
this.options.thinkingBudgetTokens,
this.options.openRouterProviderSorting,
)
const client = await this.ensureClient()
let didOutputUsage: boolean = false
this.lastGenerationId = undefined
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}`)
}
let didOutputUsage: boolean = false
if (!this.lastGenerationId && chunk.id) {
this.lastGenerationId = chunk.id
}
const stream = await createOpenRouterStream(
client,
systemPrompt,
messages,
this.getModel(),
this.options.reasoningEffort,
this.options.thinkingBudgetTokens,
this.options.openRouterProviderSorting,
)
// 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) {
if (error.code === "ERR_BAD_REQUEST" || error.status === 401) {
throw new Error("Unauthorized: Please sign in to Cline before trying again.") // match with webview-ui/src/components/chat/ChatRow.tsx
} else if (error.code === "insufficient_credits" || error.status === 402) {
throw new Error(error.error ? JSON.stringify(error.error) : "Insufficient credits or unknown error.")
}
console.error("Cline API Error:", error)
throw error instanceof Error ? error : new Error(String(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
})
@@ -175,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 }
+28 -9
View File
@@ -8,16 +8,34 @@ import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { convertToR1Format } from "../transform/r1-format"
export class DeepSeekHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
interface DeepSeekHandlerOptions {
deepSeekApiKey?: string
apiModelId?: string
}
constructor(options: ApiHandlerOptions) {
export class DeepSeekHandler implements ApiHandler {
private options: DeepSeekHandlerOptions
private client: OpenAI | undefined
constructor(options: DeepSeekHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://api.deepseek.com/v1",
apiKey: this.options.deepSeekApiKey,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.deepSeekApiKey) {
throw new Error("DeepSeek API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://api.deepseek.com/v1",
apiKey: this.options.deepSeekApiKey,
})
} catch (error) {
throw new Error(`Error creating DeepSeek client: ${error.message}`)
}
}
return this.client
}
private async *yieldUsage(info: ModelInfo, usage: OpenAI.Completions.CompletionUsage | undefined): ApiStream {
@@ -54,6 +72,7 @@ export class DeepSeekHandler implements ApiHandler {
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
const isDeepseekReasoner = model.id.includes("deepseek-reasoner")
@@ -67,7 +86,7 @@ export class DeepSeekHandler implements ApiHandler {
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
}
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: model.id,
max_completion_tokens: model.info.maxTokens,
messages: openAiMessages,
+28 -9
View File
@@ -1,20 +1,38 @@
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 client: OpenAI
constructor(options: ApiHandlerOptions) {
private options: DoubaoHandlerOptions
private client: OpenAI | undefined
constructor(options: DoubaoHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://ark.cn-beijing.volces.com/api/v3/",
apiKey: this.options.doubaoApiKey,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.doubaoApiKey) {
throw new Error("Doubao API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://ark.cn-beijing.volces.com/api/v3/",
apiKey: this.options.doubaoApiKey,
})
} catch (error) {
throw new Error(`Error creating Doubao client: ${error.message}`)
}
}
return this.client
}
getModel(): { id: DoubaoModelId; info: ModelInfo } {
@@ -31,12 +49,13 @@ export class DoubaoHandler implements ApiHandler {
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: model.id,
max_completion_tokens: model.info.maxTokens,
messages: openAiMessages,
+31 -17
View File
@@ -2,31 +2,45 @@ 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"
export class FireworksHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
interface FireworksHandlerOptions {
fireworksApiKey?: string
fireworksModelId?: string
fireworksModelMaxCompletionTokens?: number
fireworksModelMaxTokens?: number
}
constructor(options: ApiHandlerOptions) {
export class FireworksHandler implements ApiHandler {
private options: FireworksHandlerOptions
private client: OpenAI | undefined
constructor(options: FireworksHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://api.fireworks.ai/inference/v1",
apiKey: this.options.fireworksApiKey,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.fireworksApiKey) {
throw new Error("Fireworks API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://api.fireworks.ai/inference/v1",
apiKey: this.options.fireworksApiKey,
})
} catch (error) {
throw new Error(`Error creating Fireworks client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const modelId = this.options.fireworksModelId ?? ""
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
@@ -34,7 +48,7 @@ export class FireworksHandler implements ApiHandler {
...convertToOpenAiMessages(messages),
]
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: modelId,
...(this.options.fireworksModelMaxCompletionTokens
? { max_completion_tokens: this.options.fireworksModelMaxCompletionTokens }
+43 -19
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
}
/**
@@ -38,30 +45,45 @@ interface GeminiHandlerOptions extends ApiHandlerOptions {
*/
export class GeminiHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: GoogleGenAI
private client: GoogleGenAI | undefined
constructor(options: GeminiHandlerOptions) {
// Store the options
this.options = options
}
if (options.isVertex) {
// Initialize with Vertex AI configuration
const project = this.options.vertexProjectId ?? "not-provided"
const location = this.options.vertexRegion ?? "not-provided"
private ensureClient(): GoogleGenAI {
if (!this.client) {
const options = this.options as GeminiHandlerOptions
this.client = new GoogleGenAI({
vertexai: true,
project,
location,
})
} else {
// Initialize with standard API key
if (!options.geminiApiKey) {
throw new Error("API key is required for Google Gemini when not using Vertex AI")
if (options.isVertex) {
// Initialize with Vertex AI configuration
const project = this.options.vertexProjectId ?? "not-provided"
const location = this.options.vertexRegion ?? "not-provided"
try {
this.client = new GoogleGenAI({
vertexai: true,
project,
location,
})
} catch (error) {
throw new Error(`Error creating Gemini Vertex AI client: ${error.message}`)
}
} else {
// Initialize with standard API key
if (!options.geminiApiKey) {
throw new Error("API key is required for Google Gemini when not using Vertex AI")
}
try {
this.client = new GoogleGenAI({ apiKey: options.geminiApiKey })
} catch (error) {
throw new Error(`Error creating Gemini client: ${error.message}`)
}
}
this.client = new GoogleGenAI({ apiKey: options.geminiApiKey })
}
return this.client
}
/**
@@ -80,6 +102,7 @@ export class GeminiHandler implements ApiHandler {
maxDelay: 15000,
})
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const { id: modelId, info } = this.getModel()
const contents = messages.map(convertAnthropicMessageToGemini)
@@ -117,7 +140,7 @@ export class GeminiHandler implements ApiHandler {
let lastUsageMetadata: GenerateContentResponseUsageMetadata | undefined
try {
const result = await this.client.models.generateContentStream({
const result = await client.models.generateContentStream({
model: modelId,
contents: contents,
config: {
@@ -351,6 +374,7 @@ export class GeminiHandler implements ApiHandler {
*/
async countTokens(content: Array<any>): Promise<number> {
try {
const client = this.ensureClient()
const { id: model } = this.getModel()
// Convert content to Gemini format
@@ -362,7 +386,7 @@ export class GeminiHandler implements ApiHandler {
})
// Use Gemini's token counting API
const response = await this.client.models.countTokens({
const response = await client.models.countTokens({
model,
contents: [{ parts: geminiContent }],
})
+36 -11
View File
@@ -1,28 +1,52 @@
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"
export class LiteLlmHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
interface LiteLlmHandlerOptions {
liteLlmApiKey?: string
liteLlmBaseUrl?: string
liteLlmModelId?: string
liteLlmModelInfo?: LiteLLMModelInfo
thinkingBudgetTokens?: number
liteLlmUsePromptCache?: boolean
taskId?: string
}
constructor(options: ApiHandlerOptions) {
export class LiteLlmHandler implements ApiHandler {
private options: LiteLlmHandlerOptions
private client: OpenAI | undefined
constructor(options: LiteLlmHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: this.options.liteLlmBaseUrl || "http://localhost:4000",
apiKey: this.options.liteLlmApiKey || "noop",
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.liteLlmApiKey) {
throw new Error("LiteLLM API key is required")
}
try {
this.client = new OpenAI({
baseURL: this.options.liteLlmBaseUrl || "http://localhost:4000",
apiKey: this.options.liteLlmApiKey || "noop",
})
} catch (error) {
throw new Error(`Error creating LiteLLM client: ${error.message}`)
}
}
return this.client
}
async calculateCost(prompt_tokens: number, completion_tokens: number): Promise<number | undefined> {
// Reference: https://github.com/BerriAI/litellm/blob/122ee634f434014267af104814022af1d9a0882f/litellm/proxy/spend_tracking/spend_management_endpoints.py#L1473
const client = this.ensureClient()
const modelId = this.options.liteLlmModelId || liteLlmDefaultModelId
try {
const response = await fetch(`${this.client.baseURL}/spend/calculate`, {
const response = await fetch(`${client.baseURL}/spend/calculate`, {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -54,6 +78,7 @@ export class LiteLlmHandler implements ApiHandler {
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const formattedMessages = convertToOpenAiMessages(messages)
const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
role: "system",
@@ -101,7 +126,7 @@ export class LiteLlmHandler implements ApiHandler {
return message
})
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: this.options.liteLlmModelId || liteLlmDefaultModelId,
messages: [enhancedSystemMessage, ...enhancedMessages],
temperature,
+25 -9
View File
@@ -6,27 +6,43 @@ import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { withRetry } from "../retry"
export class LmStudioHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
interface LmStudioHandlerOptions {
lmStudioBaseUrl?: string
lmStudioModelId?: string
}
constructor(options: ApiHandlerOptions) {
export class LmStudioHandler implements ApiHandler {
private options: LmStudioHandlerOptions
private client: OpenAI | undefined
constructor(options: LmStudioHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: (this.options.lmStudioBaseUrl || "http://localhost:1234") + "/v1",
apiKey: "noop",
})
}
private ensureClient(): OpenAI {
if (!this.client) {
try {
this.client = new OpenAI({
baseURL: (this.options.lmStudioBaseUrl || "http://localhost:1234") + "/v1",
apiKey: "noop",
})
} catch (error) {
throw new Error(`Error creating LM Studio client: ${error.message}`)
}
}
return this.client
}
@withRetry({ retryAllErrors: true })
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
try {
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: this.getModel().id,
messages: openAiMessages,
stream: true,
+28 -9
View File
@@ -2,24 +2,43 @@ 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"
export class MistralHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: Mistral
interface MistralHandlerOptions {
mistralApiKey?: string
apiModelId?: string
}
constructor(options: ApiHandlerOptions) {
export class MistralHandler implements ApiHandler {
private options: MistralHandlerOptions
private client: Mistral | undefined
constructor(options: MistralHandlerOptions) {
this.options = options
this.client = new Mistral({
apiKey: this.options.mistralApiKey,
})
}
private ensureClient(): Mistral {
if (!this.client) {
if (!this.options.mistralApiKey) {
throw new Error("Mistral API key is required")
}
try {
this.client = new Mistral({
apiKey: this.options.mistralApiKey,
})
} catch (error) {
throw new Error(`Error creating Mistral client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const stream = await this.client.chat
const client = this.ensureClient()
const stream = await client.chat
.stream({
model: this.getModel().id,
// max_completion_tokens: this.getModel().info.maxTokens,
+26 -8
View File
@@ -5,27 +5,45 @@ 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
private client: OpenAI | undefined
constructor(private readonly options: ApiHandlerOptions) {
this.client = new OpenAI({
baseURL: "https://api.studio.nebius.ai/v1",
apiKey: this.options.nebiusApiKey,
})
constructor(private readonly options: NebiusHandlerOptions) {}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.nebiusApiKey) {
throw new Error("Nebius API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://api.studio.nebius.ai/v1",
apiKey: this.options.nebiusApiKey,
})
} catch (error) {
throw new Error(`Error creating Nebius 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[] = model.id.includes("DeepSeek-R1")
? convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)]
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: model.id,
messages: openAiMessages,
temperature: 0,
+24 -6
View File
@@ -6,17 +6,35 @@ import { convertToOllamaMessages } from "../transform/ollama-format"
import { ApiStream } from "../transform/stream"
import { withRetry } from "../retry"
export class OllamaHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: Ollama
interface OllamaHandlerOptions {
ollamaBaseUrl?: string
ollamaModelId?: string
ollamaApiOptionsCtxNum?: string
requestTimeoutMs?: number
}
constructor(options: ApiHandlerOptions) {
export class OllamaHandler implements ApiHandler {
private options: OllamaHandlerOptions
private client: Ollama | undefined
constructor(options: OllamaHandlerOptions) {
this.options = options
this.client = new Ollama({ host: this.options.ollamaBaseUrl || "http://localhost:11434" })
}
private ensureClient(): Ollama {
if (!this.client) {
try {
this.client = new Ollama({ host: this.options.ollamaBaseUrl || "http://localhost:11434" })
} catch (error) {
throw new Error(`Error creating Ollama client: ${error.message}`)
}
}
return this.client
}
@withRetry({ retryAllErrors: true })
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const ollamaMessages: Message[] = [{ role: "system", content: systemPrompt }, ...convertToOllamaMessages(messages)]
try {
@@ -27,7 +45,7 @@ export class OllamaHandler implements ApiHandler {
})
// Create the actual API request promise
const apiPromise = this.client.chat({
const apiPromise = client.chat({
model: this.getModel().id,
messages: ollamaMessages,
stream: true,
+30 -10
View File
@@ -8,15 +8,34 @@ import { calculateApiCostOpenAI } from "../../utils/cost"
import { ApiStream } from "../transform/stream"
import type { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
export class OpenAiNativeHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
interface OpenAiNativeHandlerOptions {
openAiNativeApiKey?: string
reasoningEffort?: string
apiModelId?: string
}
constructor(options: ApiHandlerOptions) {
export class OpenAiNativeHandler implements ApiHandler {
private options: OpenAiNativeHandlerOptions
private client: OpenAI | undefined
constructor(options: OpenAiNativeHandlerOptions) {
this.options = options
this.client = new OpenAI({
apiKey: this.options.openAiNativeApiKey,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.openAiNativeApiKey) {
throw new Error("OpenAI API key is required")
}
try {
this.client = new OpenAI({
apiKey: this.options.openAiNativeApiKey,
})
} catch (error: any) {
throw new Error(`Error creating OpenAI client: ${error.message}`)
}
}
return this.client
}
private async *yieldUsage(info: ModelInfo, usage: OpenAI.Completions.CompletionUsage | undefined): ApiStream {
@@ -38,6 +57,7 @@ export class OpenAiNativeHandler implements ApiHandler {
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
switch (model.id) {
@@ -45,7 +65,7 @@ export class OpenAiNativeHandler implements ApiHandler {
case "o1-preview":
case "o1-mini": {
// o1 doesn't support streaming, non-1 temp, or system prompt
const response = await this.client.chat.completions.create({
const response = await client.chat.completions.create({
model: model.id,
messages: [{ role: "user", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
})
@@ -61,7 +81,7 @@ export class OpenAiNativeHandler implements ApiHandler {
case "o4-mini":
case "o3":
case "o3-mini": {
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: model.id,
messages: [{ role: "developer", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
stream: true,
@@ -85,7 +105,7 @@ export class OpenAiNativeHandler implements ApiHandler {
break
}
default: {
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: model.id,
// max_completion_tokens: this.getModel().info.maxTokens,
temperature: 0,
+50 -26
View File
@@ -1,44 +1,68 @@
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"
export class OpenAiHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
interface OpenAiHandlerOptions {
openAiApiKey?: string
openAiBaseUrl?: string
azureApiVersion?: string
openAiHeaders?: Record<string, string>
openAiModelId?: string
openAiModelInfo?: OpenAiCompatibleModelInfo
reasoningEffort?: string
}
constructor(options: ApiHandlerOptions) {
export class OpenAiHandler implements ApiHandler {
private options: OpenAiHandlerOptions
private client: OpenAI | undefined
constructor(options: OpenAiHandlerOptions) {
this.options = options
// Azure API shape slightly differs from the core API shape: https://github.com/openai/openai-node?tab=readme-ov-file#microsoft-azure-openai
// Use azureApiVersion to determine if this is an Azure endpoint, since the URL may not always contain 'azure.com'
if (
this.options.azureApiVersion ||
((this.options.openAiBaseUrl?.toLowerCase().includes("azure.com") ||
this.options.openAiBaseUrl?.toLowerCase().includes("azure.us")) &&
!this.options.openAiModelId?.toLowerCase().includes("deepseek"))
) {
this.client = new AzureOpenAI({
baseURL: this.options.openAiBaseUrl,
apiKey: this.options.openAiApiKey,
apiVersion: this.options.azureApiVersion || azureOpenAiDefaultApiVersion,
defaultHeaders: this.options.openAiHeaders,
})
} else {
this.client = new OpenAI({
baseURL: this.options.openAiBaseUrl,
apiKey: this.options.openAiApiKey,
defaultHeaders: this.options.openAiHeaders,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.openAiApiKey) {
throw new Error("OpenAI API key is required")
}
try {
// Azure API shape slightly differs from the core API shape: https://github.com/openai/openai-node?tab=readme-ov-file#microsoft-azure-openai
// Use azureApiVersion to determine if this is an Azure endpoint, since the URL may not always contain 'azure.com'
if (
this.options.azureApiVersion ||
((this.options.openAiBaseUrl?.toLowerCase().includes("azure.com") ||
this.options.openAiBaseUrl?.toLowerCase().includes("azure.us")) &&
!this.options.openAiModelId?.toLowerCase().includes("deepseek"))
) {
this.client = new AzureOpenAI({
baseURL: this.options.openAiBaseUrl,
apiKey: this.options.openAiApiKey,
apiVersion: this.options.azureApiVersion || azureOpenAiDefaultApiVersion,
defaultHeaders: this.options.openAiHeaders,
})
} else {
this.client = new OpenAI({
baseURL: this.options.openAiBaseUrl,
apiKey: this.options.openAiApiKey,
defaultHeaders: this.options.openAiHeaders,
})
}
} catch (error: any) {
throw new Error(`Error creating OpenAI client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const modelId = this.options.openAiModelId ?? ""
const isDeepseekReasoner = modelId.includes("deepseek-reasoner")
const isR1FormatRequired = this.options.openAiModelInfo?.isR1FormatRequired ?? false
@@ -68,7 +92,7 @@ export class OpenAiHandler implements ApiHandler {
reasoningEffort = (this.options.reasoningEffort as ChatCompletionReasoningEffort) || "medium"
}
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: modelId,
messages: openAiMessages,
temperature,
+39 -17
View File
@@ -3,35 +3,59 @@ 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 client: OpenAI
private options: OpenRouterHandlerOptions
private client: OpenAI | undefined
lastGenerationId?: string
constructor(options: ApiHandlerOptions) {
constructor(options: OpenRouterHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://openrouter.ai/api/v1",
apiKey: this.options.openRouterApiKey,
defaultHeaders: {
"HTTP-Referer": "https://cline.bot", // Optional, for including your app on openrouter.ai rankings.
"X-Title": "Cline", // Optional. Shows in rankings on openrouter.ai.
},
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.openRouterApiKey) {
throw new Error("OpenRouter API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://openrouter.ai/api/v1",
apiKey: this.options.openRouterApiKey,
defaultHeaders: {
"HTTP-Referer": "https://cline.bot", // Optional, for including your app on openrouter.ai rankings.
"X-Title": "Cline", // Optional. Shows in rankings on openrouter.ai.
},
})
} catch (error: any) {
throw new Error(`Error creating OpenRouter client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
this.lastGenerationId = undefined
const stream = await createOpenRouterStream(
this.client,
client,
systemPrompt,
messages,
this.getModel(),
@@ -89,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
@@ -190,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 }
+33 -13
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,19 +15,39 @@ import { ApiStream } from "../transform/stream"
import { convertToR1Format } from "../transform/r1-format"
import { withRetry } from "../retry"
export class QwenHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
interface QwenHandlerOptions {
qwenApiKey?: string
qwenApiLine?: string
apiModelId?: string
thinkingBudgetTokens?: number
}
constructor(options: ApiHandlerOptions) {
export class QwenHandler implements ApiHandler {
private options: QwenHandlerOptions
private client: OpenAI | undefined
constructor(options: QwenHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL:
this.options.qwenApiLine === "china"
? "https://dashscope.aliyuncs.com/compatible-mode/v1"
: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
apiKey: this.options.qwenApiKey,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.qwenApiKey) {
throw new Error("Alibaba API key is required")
}
try {
this.client = new OpenAI({
baseURL:
this.options.qwenApiLine === "china"
? "https://dashscope.aliyuncs.com/compatible-mode/v1"
: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
apiKey: this.options.qwenApiKey,
})
} catch (error: any) {
throw new Error(`Error creating Alibaba client: ${error.message}`)
}
}
return this.client
}
getModel(): { id: MainlandQwenModelId | InternationalQwenModelId; info: ModelInfo } {
@@ -51,6 +70,7 @@ export class QwenHandler implements ApiHandler {
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
const isDeepseekReasoner = model.id.includes("deepseek-r1")
const isReasoningModelFamily = model.id.includes("qwen3") || ["qwen-plus-latest", "qwen-turbo-latest"].includes(model.id)
@@ -76,7 +96,7 @@ export class QwenHandler implements ApiHandler {
temperature = undefined
}
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: model.id,
max_completion_tokens: model.info.maxTokens,
messages: openAiMessages,
+34 -12
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,23 +26,37 @@ interface RequestyUsage extends OpenAI.CompletionUsage {
}
export class RequestyHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
private options: RequestyHandlerOptions
private client: OpenAI | undefined
constructor(options: ApiHandlerOptions) {
constructor(options: RequestyHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://router.requesty.ai/v1",
apiKey: this.options.requestyApiKey,
defaultHeaders: {
"HTTP-Referer": "https://cline.bot",
"X-Title": "Cline",
},
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.requestyApiKey) {
throw new Error("Requesty API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://router.requesty.ai/v1",
apiKey: this.options.requestyApiKey,
defaultHeaders: {
"HTTP-Referer": "https://cline.bot",
"X-Title": "Cline",
},
})
} catch (error: any) {
throw new Error(`Error creating Requesty 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[] = [
@@ -57,7 +79,7 @@ export class RequestyHandler implements ApiHandler {
: {}
// @ts-ignore-next-line
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: model.id,
max_tokens: model.info.maxTokens || undefined,
messages: openAiMessages,
+29 -10
View File
@@ -1,26 +1,45 @@
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"
export class SambanovaHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
interface SambanovaHandlerOptions {
sambanovaApiKey?: string
apiModelId?: string
}
constructor(options: ApiHandlerOptions) {
export class SambanovaHandler implements ApiHandler {
private options: SambanovaHandlerOptions
private client: OpenAI | undefined
constructor(options: SambanovaHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://api.sambanova.ai/v1",
apiKey: this.options.sambanovaApiKey,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.sambanovaApiKey) {
throw new Error("SambaNova API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://api.sambanova.ai/v1",
apiKey: this.options.sambanovaApiKey,
})
} catch (error: any) {
throw new Error(`Error creating SambaNova client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
@@ -34,7 +53,7 @@ export class SambanovaHandler implements ApiHandler {
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
}
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: this.getModel().id,
messages: openAiMessages,
temperature: 0,
+148 -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()
@@ -133,6 +144,8 @@ export class SapAiCoreHandler implements ApiHandler {
const openAIModels = ["gpt-4o", "gpt-4", "gpt-4o-mini", "o1", "gpt-4.1", "gpt-4.1-nano", "o3-mini", "o3", "o4-mini"]
const geminiModels = ["gemini-2.5-flash", "gemini-2.5-pro"]
let url: string
let payload: any
if (anthropicModels.includes(model.id)) {
@@ -187,6 +200,9 @@ export class SapAiCoreHandler implements ApiHandler {
delete payload.stream
delete payload.stream_options
}
} else if (geminiModels.includes(model.id)) {
url = `${this.options.sapAiCoreBaseUrl}/v2/inference/deployments/${deploymentId}/models/${model.id}:streamGenerateContent`
payload = this.convertToGeminiFormat(systemPrompt, messages)
} else {
throw new Error(`Unsupported model: ${model.id}`)
}
@@ -233,6 +249,8 @@ export class SapAiCoreHandler implements ApiHandler {
model.id === "anthropic--claude-3.7-sonnet"
) {
yield* this.streamCompletionSonnet37(response.data, model)
} else if (geminiModels.includes(model.id)) {
yield* this.streamCompletionGemini(response.data, model)
} else {
yield* this.streamCompletion(response.data, model)
}
@@ -276,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 {
@@ -339,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) {
@@ -415,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]
@@ -439,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 {
@@ -461,6 +476,88 @@ export class SapAiCoreHandler implements ApiHandler {
}
}
private async *streamCompletionGemini(
stream: any,
model: { id: SapAiCoreModelId; info: ModelInfo },
): AsyncGenerator<any, void, unknown> {
let promptTokens = 0
let outputTokens = 0
let cacheReadTokens = 0
let thoughtsTokenCount = 0
try {
for await (const chunk of stream) {
const lines = chunk.toString().split("\n").filter(Boolean)
for (const line of lines) {
if (line.startsWith("data: ")) {
const jsonData = line.slice(6)
try {
const data = JSON.parse(jsonData)
const candidateForThoughts = data?.candidates?.[0]
const partsForThoughts = candidateForThoughts?.content?.parts
let thoughts = ""
if (partsForThoughts) {
for (const part of partsForThoughts) {
const { thought, text } = part
if (thought && text) {
thoughts += text + "\n"
}
}
}
if (thoughts.trim() !== "") {
yield {
type: "reasoning",
reasoning: thoughts.trim(),
}
}
if (data.text) {
yield {
type: "text",
text: data.text,
}
}
if (data.candidates && data.candidates[0]?.content?.parts) {
for (const part of data.candidates[0].content.parts) {
if (part.text && !part.thought) {
// Only non-thought text
yield {
type: "text",
text: part.text,
}
}
}
}
if (data.usageMetadata) {
promptTokens = data.usageMetadata.promptTokenCount ?? promptTokens
outputTokens = data.usageMetadata.candidatesTokenCount ?? outputTokens
thoughtsTokenCount = data.usageMetadata.thoughtsTokenCount ?? thoughtsTokenCount
cacheReadTokens = data.usageMetadata.cachedContentTokenCount ?? cacheReadTokens
yield {
type: "usage",
inputTokens: promptTokens - cacheReadTokens,
outputTokens,
thoughtsTokenCount,
cacheReadTokens,
}
}
} catch (error) {
console.error("Failed to parse Gemini JSON data:", error)
}
}
}
}
} catch (error) {
console.error("Error streaming Gemini completion:", error)
throw error
}
}
createUserReadableRequest(
userContent: Array<
Anthropic.TextBlockParam | Anthropic.ImageBlockParam | Anthropic.ToolUseBlockParam | Anthropic.ToolResultBlockParam
@@ -495,6 +592,50 @@ export class SapAiCoreHandler implements ApiHandler {
throw new Error(`Unsupported image format: ${format}`)
}
private convertToGeminiFormat(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]) {
const contents = messages.map(this.convertAnthropicMessageToGemini)
const payload = {
contents,
systemInstruction: {
parts: [
{
text: systemPrompt,
},
],
},
generationConfig: {
maxOutputTokens: this.getModel().info.maxTokens,
temperature: 0.0,
},
}
return payload
}
private convertAnthropicMessageToGemini(message: Anthropic.Messages.MessageParam) {
const role = message.role === "assistant" ? "model" : "user"
const parts = []
if (typeof message.content === "string") {
parts.push({ text: message.content })
} else if (Array.isArray(message.content)) {
for (const block of message.content) {
if (block.type === "text") {
parts.push({ text: block.text })
} else if (block.type === "image") {
parts.push({
inlineData: {
mimeType: block.source.media_type,
data: block.source.data,
},
})
}
}
}
return { role, parts }
}
private formatAnthropicMessages(messages: Anthropic.Messages.MessageParam[]): any[] {
return messages.map((m) => {
const contentBlocks: any[] = []
+29 -10
View File
@@ -1,26 +1,45 @@
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"
export class TogetherHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
interface TogetherHandlerOptions {
togetherApiKey?: string
togetherModelId?: string
}
constructor(options: ApiHandlerOptions) {
export class TogetherHandler implements ApiHandler {
private options: TogetherHandlerOptions
private client: OpenAI | undefined
constructor(options: TogetherHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://api.together.xyz/v1",
apiKey: this.options.togetherApiKey,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.togetherApiKey) {
throw new Error("Together API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://api.together.xyz/v1",
apiKey: this.options.togetherApiKey,
})
} catch (error: any) {
throw new Error(`Error creating Together client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const modelId = this.options.togetherModelId ?? ""
const isDeepseekReasoner = modelId.includes("deepseek-reasoner")
@@ -33,7 +52,7 @@ export class TogetherHandler implements ApiHandler {
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
}
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: modelId,
messages: openAiMessages,
temperature: 0,
+55 -18
View File
@@ -6,26 +6,60 @@ 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
private clientAnthropic: AnthropicVertex
private options: ApiHandlerOptions
private geminiHandler: GeminiHandler | undefined
private clientAnthropic: AnthropicVertex | undefined
private options: VertexHandlerOptions
constructor(options: ApiHandlerOptions) {
constructor(options: VertexHandlerOptions) {
this.options = options
}
// Create a GeminiHandler with isVertex flag for Gemini models
this.geminiHandler = new GeminiHandler({
...options,
isVertex: true,
})
private ensureGeminiHandler(): GeminiHandler {
if (!this.geminiHandler) {
try {
// Create a GeminiHandler with isVertex flag for Gemini models
this.geminiHandler = new GeminiHandler({
...this.options,
isVertex: true,
})
} catch (error: any) {
throw new Error(`Error creating Vertex AI Gemini handler: ${error.message}`)
}
}
return this.geminiHandler
}
// Initialize Anthropic client for Claude models
this.clientAnthropic = new AnthropicVertex({
projectId: this.options.vertexProjectId,
// https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude#regions
region: this.options.vertexRegion,
})
private ensureAnthropicClient(): AnthropicVertex {
if (!this.clientAnthropic) {
if (!this.options.vertexProjectId) {
throw new Error("Vertex AI project ID is required")
}
if (!this.options.vertexRegion) {
throw new Error("Vertex AI region is required")
}
try {
// Initialize Anthropic client for Claude models
this.clientAnthropic = new AnthropicVertex({
projectId: this.options.vertexProjectId,
// https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude#regions
region: this.options.vertexRegion,
})
} catch (error: any) {
throw new Error(`Error creating Vertex AI Anthropic client: ${error.message}`)
}
}
return this.clientAnthropic
}
@withRetry()
@@ -35,10 +69,13 @@ export class VertexHandler implements ApiHandler {
// For Gemini models, use the GeminiHandler
if (!modelId.includes("claude")) {
yield* this.geminiHandler.createMessage(systemPrompt, messages)
const geminiHandler = this.ensureGeminiHandler()
yield* geminiHandler.createMessage(systemPrompt, messages)
return
}
const clientAnthropic = this.ensureAnthropicClient()
// Claude implementation
let budget_tokens = this.options.thinkingBudgetTokens || 0
const reasoningOn =
@@ -63,7 +100,7 @@ export class VertexHandler implements ApiHandler {
)
const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
stream = await this.clientAnthropic.beta.messages.create(
stream = await clientAnthropic.beta.messages.create(
{
model: modelId,
max_tokens: model.info.maxTokens || 8192,
@@ -125,7 +162,7 @@ export class VertexHandler implements ApiHandler {
break
}
default: {
stream = await this.clientAnthropic.beta.messages.create({
stream = await clientAnthropic.beta.messages.create({
model: modelId,
max_tokens: model.info.maxTokens || 8192,
temperature: 0,
+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
+37 -13
View File
@@ -1,26 +1,47 @@
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 client: OpenAI
private options: XAIHandlerOptions
private client: OpenAI | undefined
constructor(options: ApiHandlerOptions) {
constructor(options: XAIHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://api.x.ai/v1",
apiKey: this.options.xaiApiKey,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.xaiApiKey) {
throw new Error("xAI API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://api.x.ai/v1",
apiKey: this.options.xaiApiKey,
})
} catch (error: any) {
throw new Error(`Error creating xAI client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const modelId = this.getModel().id
// ensure reasoning effort is either "low" or "high" for grok-3-mini
let reasoningEffort: ChatCompletionReasoningEffort | undefined
@@ -30,7 +51,7 @@ export class XAIHandler implements ApiHandler {
reasoningEffort = undefined
}
}
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: modelId,
max_completion_tokens: this.getModel().info.maxTokens,
temperature: 0,
@@ -50,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,7 +6,7 @@ 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"
@@ -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"
@@ -36,17 +36,6 @@ export class FileContextTracker {
this.taskId = taskId
}
/**
* Gets the current working directory or returns undefined if it cannot be determined
*/
private async getCwd(): Promise<string | undefined> {
const cwd = await getCwd(undefined)
if (!cwd) {
console.info("No workspace folder available - cannot determine current working directory")
}
return cwd
}
/**
* File watchers are set up for each file that is tracked in the task metadata.
*/
@@ -56,8 +45,9 @@ export class FileContextTracker {
return
}
const cwd = await this.getCwd()
const cwd = await getCwd()
if (!cwd) {
console.info("No workspace folder available - cannot determine current working directory")
return
}
@@ -87,8 +77,9 @@ export class FileContextTracker {
*/
async trackFileContext(filePath: string, operation: "read_tool" | "user_edited" | "cline_edited" | "file_mentioned") {
try {
const cwd = await this.getCwd()
const cwd = await getCwd()
if (!cwd) {
console.info("No workspace folder available - cannot determine current working directory")
return
}
@@ -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)
+17 -5
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 { cwd } from "@core/task"
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
@@ -32,6 +33,7 @@ export const createRuleFile: FileMethodHandler = async (controller: Controller,
throw new Error("Missing or invalid parameters")
}
const cwd = await getCwd(getDesktopDir())
const { filePath, fileExists } = await createRuleFileImpl(request.isGlobal, request.filename, cwd, request.type)
if (!filePath) {
@@ -41,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 {
@@ -54,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
}
+2 -1
View File
@@ -4,7 +4,7 @@ import type { Controller } from "../index"
import { refreshClineRulesToggles } from "@core/context/instructions/user-instructions/cline-rules"
import { refreshExternalRulesToggles } from "@core/context/instructions/user-instructions/external-rules"
import { refreshWorkflowToggles } from "@core/context/instructions/user-instructions/workflows"
import { cwd } from "@core/task"
import { getCwd, getDesktopDir } from "@/utils/path"
/**
* Refreshes all rule toggles (Cline, External, and Workflows)
@@ -14,6 +14,7 @@ import { cwd } from "@core/task"
*/
export async function refreshRules(controller: Controller, _request: EmptyRequest): Promise<RefreshedRules> {
try {
const cwd = await getCwd(getDesktopDir())
const { globalToggles, localToggles } = await refreshClineRulesToggles(controller.context, cwd)
const { cursorLocalToggles, windsurfLocalToggles } = await refreshExternalRulesToggles(controller.context, cwd)
const { localWorkflowToggles, globalWorkflowToggles } = await refreshWorkflowToggles(controller.context, cwd)
+118 -57
View File
@@ -1,5 +1,5 @@
import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
import { getCwd } from "@/utils/path"
import { getCwd, getDesktopDir } from "@/utils/path"
import { Anthropic } from "@anthropic-ai/sdk"
import { buildApiHandler } from "@api/index"
import { cleanupLegacyCheckpoints } from "@integrations/checkpoints/CheckpointMigration"
@@ -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
@@ -179,6 +190,7 @@ export class Controller {
terminalOutputLineLimit ?? 500,
defaultTerminalProfile ?? "default",
enableCheckpointsSetting ?? true,
await getCwd(getDesktopDir()),
task,
images,
files,
@@ -238,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)
@@ -393,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,33 +462,24 @@ 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
return state === this.authService.authNonce
}
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)
// Update API configuration with the new provider and API key
// Mark welcome view as completed since user has successfully logged in
await updateGlobalState(this.context, "welcomeViewCompleted", true)
const { apiConfiguration } = await getAllExtensionState(this.context)
const updatedConfig = {
...apiConfiguration,
apiProvider: clineProvider,
clineApiKey: apiKey,
}
if (this.task) {
@@ -484,17 +487,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", {
@@ -523,7 +529,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
}
@@ -607,7 +618,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,
}),
)
}
}
@@ -833,10 +849,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 =
@@ -968,14 +987,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
}
@@ -1042,27 +1071,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
}
}
@@ -101,7 +101,7 @@ 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
default:
@@ -122,11 +122,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,
}),
)
}
}
+10 -1
View File
@@ -58,7 +58,16 @@ export async function updateSettings(controller: Controller, request: UpdateSett
// 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)}`,
}),
)
}
+10 -3
View File
@@ -1,10 +1,11 @@
import path from "path"
import fs from "fs/promises"
import vscode from "vscode"
import { Controller } from ".."
import { Empty, StringArrayRequest, BooleanRequest } from "../../../shared/proto/common"
import { Empty, StringArrayRequest } from "../../../shared/proto/common"
import { TaskMethodHandler } from "./index"
import { fileExistsAtPath } from "../../../utils/fs"
import { getHostBridgeProvider } from "@/hosts/host-providers"
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
/**
* Deletes tasks with the specified IDs
@@ -27,7 +28,13 @@ export const deleteTasksWithIds: TaskMethodHandler = async (
? "Are you sure you want to delete this task? This action cannot be undone."
: `Are you sure you want to delete these ${taskCount} tasks? This action cannot be undone.`
const userChoice = await vscode.window.showWarningMessage(message, { modal: true }, "Delete")
const userChoice = await getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.WARNING,
message,
options: { modal: true, items: ["Delete"] },
}),
)
if (userChoice === undefined) {
return Empty.create()
-1
View File
@@ -1,7 +1,6 @@
import { Controller } from ".."
import { Empty } from "../../../shared/proto/common"
import { NewTaskRequest } from "../../../shared/proto/task"
import { handleFileServiceRequest } from "../file"
/**
* Creates a new task with the given text and optional images
+2 -2
View File
@@ -1,6 +1,6 @@
import * as vscode from "vscode"
import { Controller } from ".."
import { Empty, StringRequest } from "../../../shared/proto/common"
import { openExternal } from "@utils/env"
/**
* Opens a URL in the user's default browser
@@ -11,7 +11,7 @@ import { Empty, StringRequest } from "../../../shared/proto/common"
export async function openInBrowser(controller: Controller, request: StringRequest): Promise<Empty> {
try {
if (request.value) {
await vscode.env.openExternal(vscode.Uri.parse(request.value))
await openExternal(request.value)
}
return Empty.create()
} catch (error) {
+19 -10
View File
@@ -12,6 +12,9 @@ import { getCommitInfo } from "@utils/git"
import { getWorkingState } from "@utils/git"
import { FileContextTracker } from "../context/context-tracking/FileContextTracker"
import { getCwd } from "@/utils/path"
import { openExternal } from "@utils/env"
import { getHostBridgeProvider } from "@/hosts/host-providers"
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
export async function openMention(mention?: string): Promise<void> {
if (!mention) {
@@ -36,7 +39,7 @@ export async function openMention(mention?: string): Promise<void> {
} else if (mention === "terminal") {
vscode.commands.executeCommand("workbench.action.terminal.focus")
} else if (mention.startsWith("http")) {
vscode.env.openExternal(vscode.Uri.parse(mention))
await openExternal(mention)
}
}
@@ -75,7 +78,12 @@ export async function parseMentions(
await urlContentFetcher.launchBrowser()
} catch (error) {
launchBrowserError = error
vscode.window.showErrorMessage(`Error fetching content for ${urlMention}: ${error.message}`)
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Error fetching content for ${urlMention}: ${error.message}`,
}),
)
}
}
@@ -92,7 +100,12 @@ export async function parseMentions(
const markdown = await urlContentFetcher.urlToMarkdown(mention)
result = markdown
} catch (error) {
vscode.window.showErrorMessage(`Error fetching content for ${mention}: ${error.message}`)
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Error fetching content for ${mention}: ${error.message}`,
}),
)
result = `Error fetching content: ${error.message}`
}
}
@@ -119,7 +132,7 @@ export async function parseMentions(
}
} else if (mention === "problems") {
try {
const problems = getWorkspaceProblems(cwd)
const problems = await getWorkspaceProblems()
parsedText += `\n\n<workspace_diagnostics>\n${problems}\n</workspace_diagnostics>`
} catch (error) {
parsedText += `\n\n<workspace_diagnostics>\nError fetching diagnostics: ${error.message}\n</workspace_diagnostics>`
@@ -215,13 +228,9 @@ async function getFileOrFolderContent(mentionPath: string, cwd: string): Promise
}
}
function getWorkspaceProblems(cwd: string): string {
async function getWorkspaceProblems(): Promise<string> {
const diagnostics = vscode.languages.getDiagnostics()
const result = diagnosticsToProblemsString(
diagnostics,
[vscode.DiagnosticSeverity.Error, vscode.DiagnosticSeverity.Warning],
cwd,
)
const result = diagnosticsToProblemsString(diagnostics, [vscode.DiagnosticSeverity.Error, vscode.DiagnosticSeverity.Warning])
if (!result) {
return "No errors or warnings detected."
}
+2 -1
View File
@@ -1,6 +1,6 @@
export type SecretKey =
| "apiKey"
| "clineApiKey"
| "clineAccountId"
| "openRouterApiKey"
| "awsAccessKey"
| "awsSecretKey"
@@ -80,6 +80,7 @@ export type GlobalStateKey =
| "claudeCodePath"
// Settings around plan/act and ephemeral model configuration
| "chatSettings"
| "mode"
// Current active model configuration (per workspace)
| "apiProvider"
| "apiModelId"
+1 -1
View File
@@ -202,7 +202,7 @@ export async function migrateWelcomeViewCompleted(context: vscode.ExtensionConte
config.doubaoApiKey,
config.mistralApiKey,
config.vsCodeLmModelSelector,
config.clineApiKey,
config.clineAccountId,
config.asksageApiKey,
config.xaiApiKey,
config.sambanovaApiKey,
+155 -85
View File
@@ -18,19 +18,66 @@ import { migrateEnableCheckpointsSetting, migrateMcpMarketplaceEnableSetting } f
https://www.eliostruyf.com/devhack-code-extension-storage-options/
*/
// global
const isTemporaryProfile = process.env.TEMP_PROFILE === "true"
// In-memory storage for temporary profiles
const inMemoryGlobalState = new Map<string, any>()
const inMemoryWorkspaceState = new Map<string, any>()
const inMemorySecrets = new Map<string, string>()
// global
export async function updateGlobalState(context: vscode.ExtensionContext, key: GlobalStateKey, value: any) {
if (isTemporaryProfile) {
inMemoryGlobalState.set(key, value)
return
}
await context.globalState.update(key, value)
}
export async function getGlobalState(context: vscode.ExtensionContext, key: GlobalStateKey) {
if (isTemporaryProfile) {
return inMemoryGlobalState.get(key)
}
return await context.globalState.get(key)
}
// secrets
// Batched operations for performance optimization
export async function updateGlobalStateBatch(context: vscode.ExtensionContext, updates: Record<string, any>) {
if (isTemporaryProfile) {
Object.entries(updates).forEach(([key, value]) => {
inMemoryGlobalState.set(key, value)
})
return
}
// Use Promise.all to batch the updates
await Promise.all(Object.entries(updates).map(([key, value]) => context.globalState.update(key as GlobalStateKey, value)))
}
export async function updateSecretsBatch(context: vscode.ExtensionContext, updates: Record<string, string | undefined>) {
if (isTemporaryProfile) {
Object.entries(updates).forEach(([key, value]) => {
if (value) {
inMemorySecrets.set(key, value)
} else {
inMemorySecrets.delete(key)
}
})
return
}
// Use Promise.all to batch the secret updates
await Promise.all(Object.entries(updates).map(([key, value]) => storeSecret(context, key as SecretKey, value)))
}
// secrets
export async function storeSecret(context: vscode.ExtensionContext, key: SecretKey, value?: string) {
if (isTemporaryProfile) {
if (value) {
inMemorySecrets.set(key, value)
} else {
inMemorySecrets.delete(key)
}
return
}
if (value) {
await context.secrets.store(key, value)
} else {
@@ -39,26 +86,36 @@ export async function storeSecret(context: vscode.ExtensionContext, key: SecretK
}
export async function getSecret(context: vscode.ExtensionContext, key: SecretKey) {
if (isTemporaryProfile) {
return inMemorySecrets.get(key)
}
return await context.secrets.get(key)
}
// workspace
export async function updateWorkspaceState(context: vscode.ExtensionContext, key: LocalStateKey, value: any) {
if (isTemporaryProfile) {
inMemoryWorkspaceState.set(key, value)
return
}
await context.workspaceState.update(key, value)
}
export async function getWorkspaceState(context: vscode.ExtensionContext, key: LocalStateKey) {
if (isTemporaryProfile) {
return inMemoryWorkspaceState.get(key)
}
return await context.workspaceState.get(key)
}
export async function getAllExtensionState(context: vscode.ExtensionContext) {
const firstBatchStart = performance.now()
const [
isNewUser,
welcomeViewCompleted,
apiKey,
openRouterApiKey,
clineApiKey,
clineAccountId,
awsAccessKey,
awsSecretKey,
awsSessionToken,
@@ -131,7 +188,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
getGlobalState(context, "welcomeViewCompleted") as Promise<boolean | undefined>,
getSecret(context, "apiKey") as Promise<string | undefined>,
getSecret(context, "openRouterApiKey") as Promise<string | undefined>,
getSecret(context, "clineApiKey") as Promise<string | undefined>,
getSecret(context, "clineAccountId") as Promise<string | undefined>,
getSecret(context, "awsAccessKey") as Promise<string | undefined>,
getSecret(context, "awsSecretKey") as Promise<string | undefined>,
getSecret(context, "awsSessionToken") as Promise<string | undefined>,
@@ -203,8 +260,10 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
const localClineRulesToggles = (await getWorkspaceState(context, "localClineRulesToggles")) as ClineRulesToggles
const secondBatchStart = performance.now()
const [
chatSettings,
currentMode,
storedApiProvider,
apiModelId,
thinkingBudgetTokens,
@@ -236,6 +295,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
sapAiCoreModelId,
] = await Promise.all([
getGlobalState(context, "chatSettings") as Promise<StoredChatSettings | undefined>,
getGlobalState(context, "mode") as Promise<"plan" | "act" | undefined>,
getGlobalState(context, "apiProvider") as Promise<ApiProvider | undefined>,
getGlobalState(context, "apiModelId") as Promise<string | undefined>,
getGlobalState(context, "thinkingBudgetTokens") as Promise<number | undefined>,
@@ -267,6 +327,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
getGlobalState(context, "sapAiCoreModelId") as Promise<string | undefined>,
])
const processingStart = performance.now()
let apiProvider: ApiProvider
if (storedApiProvider) {
apiProvider = storedApiProvider
@@ -309,7 +370,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
apiModelId,
apiKey,
openRouterApiKey,
clineApiKey,
clineAccountId,
claudeCodePath,
awsAccessKey,
awsSecretKey,
@@ -389,7 +450,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
browserSettings: { ...DEFAULT_BROWSER_SETTINGS, ...browserSettings }, // this will ensure that older versions of browserSettings (e.g. before remoteBrowserEnabled was added) are merged with the default values (false for remoteBrowserEnabled)
chatSettings: {
...DEFAULT_CHAT_SETTINGS, // Apply defaults first
...(chatSettings || {}), // Spread fetched chatSettings, which includes preferredLanguage, and openAIReasoningEffort
...(chatSettings || {}), // Spread fetched global chatSettings, which includes preferredLanguage, and openAIReasoningEffort
mode: currentMode || "act", // Merge mode from global state
},
userInfo,
previousModeApiProvider,
@@ -473,7 +535,7 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
xaiApiKey,
thinkingBudgetTokens,
reasoningEffort,
clineApiKey,
clineAccountId,
sambanovaApiKey,
cerebrasApiKey,
nebiusApiKey,
@@ -491,84 +553,92 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
claudeCodePath,
} = apiConfiguration
// Ephemeral model config updates
await updateGlobalState(context, "apiProvider", apiProvider)
await updateGlobalState(context, "apiModelId", apiModelId)
await updateGlobalState(context, "thinkingBudgetTokens", thinkingBudgetTokens)
await updateGlobalState(context, "reasoningEffort", reasoningEffort)
await updateGlobalState(context, "vsCodeLmModelSelector", vsCodeLmModelSelector)
await updateGlobalState(context, "awsBedrockCustomSelected", awsBedrockCustomSelected)
await updateGlobalState(context, "awsBedrockCustomModelBaseId", awsBedrockCustomModelBaseId)
await updateGlobalState(context, "openRouterModelId", openRouterModelId)
await updateGlobalState(context, "openRouterModelInfo", openRouterModelInfo)
await updateGlobalState(context, "openAiModelId", openAiModelId)
await updateGlobalState(context, "openAiModelInfo", openAiModelInfo)
await updateGlobalState(context, "ollamaModelId", ollamaModelId)
await updateGlobalState(context, "lmStudioModelId", lmStudioModelId)
await updateGlobalState(context, "liteLlmModelId", liteLlmModelId)
await updateGlobalState(context, "liteLlmModelInfo", liteLlmModelInfo)
await updateGlobalState(context, "requestyModelId", requestyModelId)
await updateGlobalState(context, "requestyModelInfo", requestyModelInfo)
await updateGlobalState(context, "togetherModelId", togetherModelId)
await updateGlobalState(context, "fireworksModelId", fireworksModelId)
await updateGlobalState(context, "sapAiCoreModelId", sapAiCoreModelId)
// OPTIMIZED: Batch all global state updates into 2 operations instead of 47
const batchedGlobalUpdates = {
// Ephemeral model config updates (20 keys)
apiProvider,
apiModelId,
thinkingBudgetTokens,
reasoningEffort,
vsCodeLmModelSelector,
awsBedrockCustomSelected,
awsBedrockCustomModelBaseId,
openRouterModelId,
openRouterModelInfo,
openAiModelId,
openAiModelInfo,
ollamaModelId,
lmStudioModelId,
liteLlmModelId,
liteLlmModelInfo,
requestyModelId,
requestyModelInfo,
togetherModelId,
fireworksModelId,
sapAiCoreModelId,
// Global state updates
await updateGlobalState(context, "awsRegion", awsRegion)
await updateGlobalState(context, "awsUseCrossRegionInference", awsUseCrossRegionInference)
await updateGlobalState(context, "awsBedrockUsePromptCache", awsBedrockUsePromptCache)
await updateGlobalState(context, "awsBedrockEndpoint", awsBedrockEndpoint)
await updateGlobalState(context, "awsProfile", awsProfile)
await updateGlobalState(context, "awsUseProfile", awsUseProfile)
await updateGlobalState(context, "vertexProjectId", vertexProjectId)
await updateGlobalState(context, "vertexRegion", vertexRegion)
await updateGlobalState(context, "openAiBaseUrl", openAiBaseUrl)
await updateGlobalState(context, "openAiHeaders", openAiHeaders || {})
await updateGlobalState(context, "ollamaBaseUrl", ollamaBaseUrl)
await updateGlobalState(context, "ollamaApiOptionsCtxNum", ollamaApiOptionsCtxNum)
await updateGlobalState(context, "lmStudioBaseUrl", lmStudioBaseUrl)
await updateGlobalState(context, "anthropicBaseUrl", anthropicBaseUrl)
await updateGlobalState(context, "geminiBaseUrl", geminiBaseUrl)
await updateGlobalState(context, "azureApiVersion", azureApiVersion)
await updateGlobalState(context, "openRouterProviderSorting", openRouterProviderSorting)
await updateGlobalState(context, "liteLlmBaseUrl", liteLlmBaseUrl)
await updateGlobalState(context, "liteLlmUsePromptCache", liteLlmUsePromptCache)
await updateGlobalState(context, "qwenApiLine", qwenApiLine)
await updateGlobalState(context, "asksageApiUrl", asksageApiUrl)
await updateGlobalState(context, "favoritedModelIds", favoritedModelIds)
await updateGlobalState(context, "requestTimeoutMs", apiConfiguration.requestTimeoutMs)
await updateGlobalState(context, "fireworksModelMaxCompletionTokens", fireworksModelMaxCompletionTokens)
await updateGlobalState(context, "fireworksModelMaxTokens", fireworksModelMaxTokens)
await updateGlobalState(context, "sapAiCoreBaseUrl", sapAiCoreBaseUrl)
await updateGlobalState(context, "sapAiCoreTokenUrl", sapAiCoreTokenUrl)
await updateGlobalState(context, "sapAiResourceGroup", sapAiResourceGroup)
await updateGlobalState(context, "claudeCodePath", claudeCodePath)
// Global state updates (27 keys)
awsRegion,
awsUseCrossRegionInference,
awsBedrockUsePromptCache,
awsBedrockEndpoint,
awsProfile,
awsUseProfile,
vertexProjectId,
vertexRegion,
openAiBaseUrl,
openAiHeaders: openAiHeaders || {},
ollamaBaseUrl,
ollamaApiOptionsCtxNum,
lmStudioBaseUrl,
anthropicBaseUrl,
geminiBaseUrl,
azureApiVersion,
openRouterProviderSorting,
liteLlmBaseUrl,
liteLlmUsePromptCache,
qwenApiLine,
asksageApiUrl,
favoritedModelIds,
requestTimeoutMs: apiConfiguration.requestTimeoutMs,
fireworksModelMaxCompletionTokens,
fireworksModelMaxTokens,
sapAiCoreBaseUrl,
sapAiCoreTokenUrl,
sapAiResourceGroup,
claudeCodePath,
}
// Secret updates
await storeSecret(context, "apiKey", apiKey)
await storeSecret(context, "openRouterApiKey", openRouterApiKey)
await storeSecret(context, "clineApiKey", clineApiKey)
await storeSecret(context, "awsAccessKey", awsAccessKey)
await storeSecret(context, "awsSecretKey", awsSecretKey)
await storeSecret(context, "awsSessionToken", awsSessionToken)
await storeSecret(context, "openAiApiKey", openAiApiKey)
await storeSecret(context, "geminiApiKey", geminiApiKey)
await storeSecret(context, "openAiNativeApiKey", openAiNativeApiKey)
await storeSecret(context, "deepSeekApiKey", deepSeekApiKey)
await storeSecret(context, "requestyApiKey", requestyApiKey)
await storeSecret(context, "togetherApiKey", togetherApiKey)
await storeSecret(context, "qwenApiKey", qwenApiKey)
await storeSecret(context, "doubaoApiKey", doubaoApiKey)
await storeSecret(context, "mistralApiKey", mistralApiKey)
await storeSecret(context, "liteLlmApiKey", liteLlmApiKey)
await storeSecret(context, "fireworksApiKey", fireworksApiKey)
await storeSecret(context, "asksageApiKey", asksageApiKey)
await storeSecret(context, "xaiApiKey", xaiApiKey)
await storeSecret(context, "sambanovaApiKey", sambanovaApiKey)
await storeSecret(context, "cerebrasApiKey", cerebrasApiKey)
await storeSecret(context, "nebiusApiKey", nebiusApiKey)
await storeSecret(context, "sapAiCoreClientId", sapAiCoreClientId)
await storeSecret(context, "sapAiCoreClientSecret", sapAiCoreClientSecret)
// OPTIMIZED: Batch all secret updates into 1 operation instead of 23
const batchedSecretUpdates = {
apiKey,
openRouterApiKey,
clineAccountId,
awsAccessKey,
awsSecretKey,
awsSessionToken,
openAiApiKey,
geminiApiKey,
openAiNativeApiKey,
deepSeekApiKey,
requestyApiKey,
togetherApiKey,
qwenApiKey,
doubaoApiKey,
mistralApiKey,
liteLlmApiKey,
fireworksApiKey,
asksageApiKey,
xaiApiKey,
sambanovaApiKey,
cerebrasApiKey,
nebiusApiKey,
sapAiCoreClientId,
sapAiCoreClientSecret,
}
// Execute batched operations in parallel for maximum performance
await Promise.all([updateGlobalStateBatch(context, batchedGlobalUpdates), updateSecretsBatch(context, batchedSecretUpdates)])
}
export async function resetWorkspaceState(context: vscode.ExtensionContext) {
@@ -597,7 +667,7 @@ export async function resetGlobalState(context: vscode.ExtensionContext) {
"qwenApiKey",
"doubaoApiKey",
"mistralApiKey",
"clineApiKey",
"clineAccountId",
"liteLlmApiKey",
"fireworksApiKey",
"asksageApiKey",
+42 -28
View File
@@ -26,16 +26,15 @@ import { getApiMetrics } from "@shared/getApiMetrics"
import { HistoryItem } from "@shared/HistoryItem"
import { DEFAULT_LANGUAGE_SETTINGS, getLanguageKey, LanguageDisplay } from "@shared/Languages"
import { ClineAskResponse, ClineCheckpointRestore } from "@shared/WebviewMessage"
import { arePathsEqual } from "@utils/path"
import { getGitRemoteUrls } from "@utils/git"
import { arePathsEqual, getDesktopDir } from "@utils/path"
import cloneDeep from "clone-deep"
import { execa } from "execa"
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
import os from "os"
import pTimeout from "p-timeout"
import pWaitFor from "p-wait-for"
import * as path from "path"
import * as vscode from "vscode"
import { getGitRemoteUrls } from "@utils/git"
import { parseAssistantMessageV2, parseAssistantMessageV3, ToolUseName } from "@core/assistant-message"
import {
@@ -70,7 +69,7 @@ import {
getSavedClineMessages,
GlobalFileNames,
} from "@core/storage/disk"
import { getGlobalState, getWorkspaceState } from "@core/storage/state"
import { getGlobalState } from "@core/storage/state"
import { processFilesIntoText } from "@integrations/misc/extract-text"
import WorkspaceTracker from "@integrations/workspace/WorkspaceTracker"
import { McpHub } from "@services/mcp/McpHub"
@@ -83,12 +82,10 @@ import { MessageStateHandler } from "./message-state"
import { TaskState } from "./TaskState"
import { ToolExecutor } from "./ToolExecutor"
import { formatErrorWithStatusCode, updateApiReqMsg } from "./utils"
import { createDiffViewProvider, getHostBridgeProvider } from "@/hosts/host-providers"
export const USE_EXPERIMENTAL_CLAUDE4_FEATURES = false
export const cwd =
vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution
export type ToolResponse = string | Array<Anthropic.TextBlockParam | Anthropic.ImageBlockParam>
type UserContent = Array<Anthropic.ContentBlockParam>
@@ -96,6 +93,7 @@ export class Task {
// Core task variables
readonly taskId: string
private taskIsFavorited?: boolean
private cwd: string
taskState: TaskState
@@ -152,6 +150,7 @@ export class Task {
terminalOutputLineLimit: number,
defaultTerminalProfile: string,
enableCheckpointsSetting: boolean,
cwd: string,
task?: string,
images?: string[],
files?: string[],
@@ -176,11 +175,12 @@ export class Task {
this.urlContentFetcher = new UrlContentFetcher(context)
this.browserSession = new BrowserSession(context, browserSettings)
this.contextManager = new ContextManager()
this.diffViewProvider = new DiffViewProvider(cwd)
this.diffViewProvider = createDiffViewProvider()
this.autoApprovalSettings = autoApprovalSettings
this.browserSettings = browserSettings
this.chatSettings = chatSettings
this.enableCheckpoints = enableCheckpointsSetting
this.cwd = cwd
// Set up MCP notification callback for real-time notifications
this.mcpHub.setNotificationCallback(async (serverName: string, level: string, message: string) => {
@@ -1097,7 +1097,7 @@ export class Task {
const [taskResumptionMessage, userResponseMessage] = formatResponse.taskResumption(
this.chatSettings?.mode === "plan" ? "plan" : "act",
agoText,
cwd,
this.cwd,
wasRecent,
responseText,
hasPendingFileContextWarnings,
@@ -1328,7 +1328,7 @@ export class Task {
// Create a child process
const childProcess = execa(command, {
shell: true,
cwd,
cwd: this.cwd,
reject: false,
all: true, // Merge stdout and stderr
})
@@ -1405,7 +1405,7 @@ export class Task {
}
Logger.info("Executing command in VS code terminal: " + command)
const terminalInfo = await this.terminalManager.getOrCreateTerminal(cwd)
const terminalInfo = await this.terminalManager.getOrCreateTerminal(this.cwd)
terminalInfo.terminal.show() // weird visual bug when creating new terminals (even manually) where there's an empty space at the top.
const process = this.terminalManager.runCommand(terminalInfo, command)
@@ -1581,7 +1581,7 @@ export class Task {
const supportsBrowserUse = modelSupportsBrowserUse && !disableBrowserTool // only enable browser use if the model supports it and the user hasn't disabled it
const isNextGenModel = isClaude4ModelFamily(this.api) || isGemini2dot5ModelFamily(this.api)
let systemPrompt = await SYSTEM_PROMPT(cwd, supportsBrowserUse, this.mcpHub, this.browserSettings, isNextGenModel)
let systemPrompt = await SYSTEM_PROMPT(this.cwd, supportsBrowserUse, this.mcpHub, this.browserSettings, isNextGenModel)
await this.migratePreferredLanguageToolSetting()
const preferredLanguage = getLanguageKey(this.chatSettings.preferredLanguage as LanguageDisplay)
@@ -1590,18 +1590,18 @@ export class Task {
? `# Preferred Language\n\nSpeak in ${preferredLanguage}.`
: ""
const { globalToggles, localToggles } = await refreshClineRulesToggles(this.getContext(), cwd)
const { windsurfLocalToggles, cursorLocalToggles } = await refreshExternalRulesToggles(this.getContext(), cwd)
const { globalToggles, localToggles } = await refreshClineRulesToggles(this.getContext(), this.cwd)
const { windsurfLocalToggles, cursorLocalToggles } = await refreshExternalRulesToggles(this.getContext(), this.cwd)
const globalClineRulesFilePath = await ensureRulesDirectoryExists()
const globalClineRulesFileInstructions = await getGlobalClineRules(globalClineRulesFilePath, globalToggles)
const localClineRulesFileInstructions = await getLocalClineRules(cwd, localToggles)
const localClineRulesFileInstructions = await getLocalClineRules(this.cwd, localToggles)
const [localCursorRulesFileInstructions, localCursorRulesDirInstructions] = await getLocalCursorRules(
cwd,
this.cwd,
cursorLocalToggles,
)
const localWindsurfRulesFileInstructions = await getLocalWindsurfRules(cwd, windsurfLocalToggles)
const localWindsurfRulesFileInstructions = await getLocalWindsurfRules(this.cwd, windsurfLocalToggles)
const clineIgnoreContent = this.clineIgnoreController.clineIgnoreContent
let clineIgnoreInstructions: string | undefined
@@ -1724,7 +1724,7 @@ export class Task {
await this.messageStateHandler.updateClineMessage(lastApiReqStartedIndex, {
text: JSON.stringify({
...currentApiReqInfo, // Spread the modified info (with retryStatus removed)
cancelReason: "retries_exhausted", // Indicate that automatic retries failed
// cancelReason: "retries_exhausted", // Indicate that automatic retries failed
streamingFailedMessage: errorMessage,
} satisfies ClineApiReqInfo),
})
@@ -2125,6 +2125,13 @@ export class Task {
this.api.getModel().id,
"assistant",
true,
{
tokensIn: inputTokens,
tokensOut: outputTokens,
cacheWriteTokens,
cacheReadTokens,
totalCost,
},
)
// signals to provider that it can retrieve the saved messages from disk, as abortTask can not be awaited on in nature
@@ -2298,6 +2305,13 @@ export class Task {
this.api.getModel().id,
"assistant",
true,
{
tokensIn: inputTokens,
tokensOut: outputTokens,
cacheWriteTokens,
cacheReadTokens,
totalCost,
},
)
await this.messageStateHandler.addToApiConversationHistory({
@@ -2357,7 +2371,7 @@ export class Task {
// Track if we need to check clinerulesFile
let needsClinerulesFileCheck = false
const { localWorkflowToggles, globalWorkflowToggles } = await refreshWorkflowToggles(this.getContext(), cwd)
const { localWorkflowToggles, globalWorkflowToggles } = await refreshWorkflowToggles(this.getContext(), this.cwd)
const processUserContent = async () => {
// This is a temporary solution to dynamically load context mentions from tool results. It checks for the presence of tags that indicate that the tool was rejected and feedback was provided (see formatToolDeniedFeedback, attemptCompletion, executeCommand, and consecutiveMistakeCount >= 3) or "<answer>" (see askFollowupQuestion), we place all user generated content in these tags so they can effectively be used as markers for when we should parse mentions). However if we allow multiple tools responses in the future, we will need to parse mentions specifically within the user content tags.
@@ -2375,7 +2389,7 @@ export class Task {
) {
const parsedText = await parseMentions(
block.text,
cwd,
this.cwd,
this.urlContentFetcher,
this.fileContextTracker,
)
@@ -2411,7 +2425,7 @@ export class Task {
// After processing content, check clinerulesData if needed
let clinerulesError = false
if (needsClinerulesFileCheck) {
clinerulesError = await ensureLocalClineDirExists(cwd, GlobalFileNames.clineRules)
clinerulesError = await ensureLocalClineDirExists(this.cwd, GlobalFileNames.clineRules)
}
// Return all results
@@ -2426,7 +2440,7 @@ export class Task {
const visibleFilePaths = vscode.window.visibleTextEditors
?.map((editor) => editor.document?.uri?.fsPath)
.filter(Boolean)
.map((absolutePath) => path.relative(cwd, absolutePath))
.map((absolutePath) => path.relative(this.cwd, absolutePath))
// Filter paths through clineIgnoreController
const allowedVisibleFiles = this.clineIgnoreController
@@ -2445,7 +2459,7 @@ export class Task {
.flatMap((group) => group.tabs)
.map((tab) => (tab.input as vscode.TabInputText)?.uri?.fsPath)
.filter(Boolean)
.map((absolutePath) => path.relative(cwd, absolutePath))
.map((absolutePath) => path.relative(this.cwd, absolutePath))
// Filter paths through clineIgnoreController
const allowedOpenTabs = this.clineIgnoreController
@@ -2571,19 +2585,19 @@ export class Task {
details += `\n\n# Current Time\n${formatter.format(now)} (${timeZone}, UTC${timeZoneOffsetStr})`
if (includeFileDetails) {
details += `\n\n# Current Working Directory (${cwd.toPosix()}) Files\n`
const isDesktop = arePathsEqual(cwd, path.join(os.homedir(), "Desktop"))
details += `\n\n# Current Working Directory (${this.cwd.toPosix()}) Files\n`
const isDesktop = arePathsEqual(this.cwd, getDesktopDir())
if (isDesktop) {
// don't want to immediately access desktop since it would show permission popup
details += "(Desktop files not shown automatically. Use list_files to explore if needed.)"
} else {
const [files, didHitLimit] = await listFiles(cwd, true, 200)
const result = formatResponse.formatFilesList(cwd, files, didHitLimit, this.clineIgnoreController)
const [files, didHitLimit] = await listFiles(this.cwd, true, 200)
const result = formatResponse.formatFilesList(this.cwd, files, didHitLimit, this.clineIgnoreController)
details += result
}
// Add git remote URLs section
const gitRemotes = await getGitRemoteUrls(cwd)
const gitRemotes = await getGitRemoteUrls(this.cwd)
if (gitRemotes.length > 0) {
details += `\n\n# Git Remote URLs\n${gitRemotes.join("\n")}`
}
+2 -2
View File
@@ -12,7 +12,7 @@ import CheckpointTracker from "@integrations/checkpoints/CheckpointTracker"
import { HistoryItem } from "@/shared/HistoryItem"
import Anthropic from "@anthropic-ai/sdk"
import { TaskState } from "./TaskState"
import { getCwd } from "@/utils/path"
import { getCwd, getDesktopDir } from "@/utils/path"
interface MessageStateHandlerParams {
context: vscode.ExtensionContext
@@ -83,7 +83,7 @@ export class MessageStateHandler {
} catch (error) {
console.error("Failed to get task directory size:", taskDir, error)
}
const cwd = await getCwd(path.join(os.homedir(), "Desktop"))
const cwd = await getCwd(getDesktopDir())
await this.updateTaskHistory({
id: this.taskId,
ts: lastRelevantMessage.ts,
+2 -2
View File
@@ -2,7 +2,7 @@ import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
import { ToolUseName } from "@core/assistant-message"
import * as path from "path"
import os from "os"
import { getCwd } from "@/utils/path"
import { getCwd, getDesktopDir } from "@/utils/path"
export class AutoApprove {
autoApprovalSettings: AutoApprovalSettings
@@ -54,7 +54,7 @@ export class AutoApprove {
async shouldAutoApproveToolWithPath(blockname: ToolUseName, autoApproveActionpath: string | undefined): Promise<boolean> {
let isLocalRead: boolean = false
if (autoApproveActionpath) {
const cwd = await getCwd(path.join(os.homedir(), "Desktop"))
const cwd = await getCwd(getDesktopDir())
const absolutePath = path.resolve(cwd, autoApproveActionpath)
isLocalRead = absolutePath.startsWith(cwd)
} else {
+8 -2
View File
@@ -10,6 +10,8 @@ import path from "node:path"
import { v4 as uuidv4 } from "uuid"
import { Uri } from "vscode"
import { ExtensionMessage } from "@/shared/ExtensionMessage"
import { getHostBridgeProvider } from "@/hosts/host-providers"
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
export abstract class WebviewProvider {
public static readonly sideBarId = "claude-dev.SidebarProvider" // used in package.json as the view's id. This value cannot be changed due to how vscode caches views based on their id, and updating the id would break existing instances of the extension.
@@ -260,8 +262,12 @@ export abstract class WebviewProvider {
try {
await axios.get(`http://${localServerUrl}`)
} catch (error) {
vscode.window.showErrorMessage(
"Cline: Local webview dev server is not running, HMR will not work. Please run 'npm run dev:webview' before launching the extension to enable HMR. Using bundled assets.",
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message:
"Cline: Local webview dev server is not running, HMR will not work. Please run 'npm run dev:webview' before launching the extension to enable HMR. Using bundled assets.",
}),
)
return this.getHtmlContent()
+9 -1
View File
@@ -4,6 +4,8 @@ import * as path from "path"
import { Controller } from "@core/controller"
import { HistoryItem } from "@shared/HistoryItem"
import { ClineMessage } from "@shared/ExtensionMessage"
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
import { getHostBridgeProvider } from "@/hosts/host-providers"
/**
* Registers development-only commands for task manipulation.
@@ -96,7 +98,13 @@ export function registerTaskCommands(context: vscode.ExtensionContext, controlle
// Update the UI to show the new tasks
await controller.postStateToWebview()
vscode.window.showInformationMessage(`Created ${tasksCount} test tasks`)
const message = `Created ${tasksCount} test tasks`
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message,
}),
)
},
)
}),
+69 -17
View File
@@ -35,8 +35,11 @@ import * as hostProviders from "@hosts/host-providers"
import { vscodeHostBridgeClient } from "@/hosts/vscode/client/host-grpc-client"
import { VscodeWebviewProvider } from "./core/webview/VscodeWebviewProvider"
import { ExtensionContext } from "vscode"
import { AuthService } from "./services/auth/AuthService"
import { writeTextToClipboard, readTextFromClipboard } from "@/utils/env"
import { VscodeDiffViewProvider } from "./integrations/editor/VscodeDiffViewProvider"
import { getHostBridgeProvider } from "@hosts/host-providers"
import { ShowMessageRequest, ShowMessageType } from "./shared/proto/host/window"
/*
Built using https://github.com/microsoft/vscode-webview-ui-toolkit
@@ -103,7 +106,12 @@ export async function activate(context: vscode.ExtensionContext) {
const message = `Cline has been updated to v${currentVersion}`
await vscode.commands.executeCommand("claude-dev.SidebarProvider.focus")
await new Promise((resolve) => setTimeout(resolve, 200))
vscode.window.showInformationMessage(message)
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message,
}),
)
// Record that we've shown the popup for this version.
await context.globalState.update("clineLastPopupNotificationVersion", currentVersion)
}
@@ -293,24 +301,39 @@ export async function activate(context: vscode.ExtensionContext) {
break
}
case "/auth": {
const token = query.get("token")
const authService = AuthService.getInstance()
console.log("Auth callback received:", uri.toString())
const token = query.get("idToken")
const state = query.get("state")
const apiKey = query.get("apiKey")
const provider = query.get("provider")
console.log("Auth callback received:", {
token: token,
state: state,
apiKey: apiKey,
provider: provider,
})
// Validate state parameter
if (!(await visibleWebview?.controller.validateAuthState(state))) {
vscode.window.showErrorMessage("Invalid auth state")
return
// Ask user to confirm on state mismatch. This enables signins initiated from
// outside the extension (e.g. Cline web) to be handled correctly.
if (authService.authNonce !== state) {
const userConfirmation = (
await getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: "Invalid auth state",
}),
)
)?.selectedOption
if (userConfirmation === "Cancel") {
console.log("User declined to continue with auth callback due to state mismatch")
return
}
}
if (token && apiKey) {
await visibleWebview?.controller.handleAuthCallback(token, apiKey)
if (token) {
await visibleWebview?.controller.handleAuthCallback(token, provider)
// await authService.handleAuthCallback(token)
}
break
}
@@ -413,7 +436,12 @@ export async function activate(context: vscode.ExtensionContext) {
// Ensure clipboard is restored even if an error occurs
await writeTextToClipboard(tempCopyBuffer)
console.error("Error getting terminal contents:", error)
vscode.window.showErrorMessage("Failed to get terminal contents")
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: "Failed to get terminal contents",
}),
)
}
}),
)
@@ -549,7 +577,12 @@ export async function activate(context: vscode.ExtensionContext) {
}
const selectedText = editor.document.getText(range)
if (!selectedText.trim()) {
vscode.window.showInformationMessage("Please select some code to explain.")
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: "Please select some code to explain.",
}),
)
return
}
const filePath = editor.document.uri.fsPath
@@ -571,7 +604,12 @@ export async function activate(context: vscode.ExtensionContext) {
}
const selectedText = editor.document.getText(range)
if (!selectedText.trim()) {
vscode.window.showInformationMessage("Please select some code to improve.")
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: "Please select some code to improve.",
}),
)
return
}
const filePath = editor.document.uri.fsPath
@@ -636,8 +674,11 @@ export async function activate(context: vscode.ExtensionContext) {
sendFocusChatInputEvent(clientId)
} else {
console.error("FocusChatInput: Could not find or activate a Cline webview to focus.")
vscode.window.showErrorMessage(
"Could not activate Cline view. Please try opening it manually from the Activity Bar.",
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: "Could not activate Cline view. Please try opening it manually from the Activity Bar.",
}),
)
}
telemetryService.captureButtonClick("command_focusChatInput", activeWebviewProvider?.controller.task?.taskId, true)
@@ -672,6 +713,14 @@ export async function activate(context: vscode.ExtensionContext) {
}),
)
context.subscriptions.push(
context.secrets.onDidChange((event) => {
if (event.key === "clineAccountId") {
AuthService.getInstance(context)?.restoreRefreshTokenAndRetrieveAuthInfo()
}
}),
)
return createClineAPI(outputChannel, sidebarWebview.controller)
}
@@ -681,7 +730,10 @@ function maybeSetupHostProviders(context: ExtensionContext) {
const createWebview = function (type: WebviewProviderType) {
return new VscodeWebviewProvider(context, outputChannel, type)
}
hostProviders.initializeHostProviders(createWebview, vscodeHostBridgeClient)
const createDiffView = function () {
return new VscodeDiffViewProvider()
}
hostProviders.initializeHostProviders(createWebview, createDiffView, vscodeHostBridgeClient)
}
}
+2 -2
View File
@@ -1,20 +1,20 @@
import {
UriServiceClientInterface,
WatchServiceClientInterface,
WorkspaceServiceClientInterface,
EnvServiceClientInterface,
WindowServiceClientInterface,
DiffServiceClientInterface,
} from "@generated/hosts/host-bridge-client-types"
/**
* Interface for host bridge client providers
*/
export interface HostBridgeClientProvider {
uriServiceClient: UriServiceClientInterface
watchServiceClient: WatchServiceClientInterface
workspaceClient: WorkspaceServiceClientInterface
envClient: EnvServiceClientInterface
windowClient: WindowServiceClientInterface
diffClient: DiffServiceClientInterface
}
/**
+13 -1
View File
@@ -1,23 +1,28 @@
import { WebviewProvider } from "@core/webview"
import { HostBridgeClientProvider } from "./host-provider-types"
import { WebviewProviderType } from "@/shared/webview/types"
import * as vscode from "vscode"
import { DiffViewProvider } from "@/integrations/editor/DiffViewProvider"
/**
* A function that creates WebviewProvider instances
*/
export type WebviewProviderCreator = (providerType: WebviewProviderType) => WebviewProvider
export type DiffViewProviderCreator = () => DiffViewProvider
let _webviewProviderCreator: WebviewProviderCreator | undefined
let _diffViewProviderCreator: DiffViewProviderCreator | undefined
let _hostBridgeProvider: HostBridgeClientProvider | undefined
export var isSetup: boolean = false
export function initializeHostProviders(
webviewProviderCreator: WebviewProviderCreator,
diffViewProviderCreator: DiffViewProviderCreator,
hostBridgeProvider: HostBridgeClientProvider,
) {
_webviewProviderCreator = webviewProviderCreator
_diffViewProviderCreator = diffViewProviderCreator
_hostBridgeProvider = hostBridgeProvider
isSetup = true
}
@@ -29,6 +34,13 @@ export function createWebviewProvider(providerType: WebviewProviderType): Webvie
return _webviewProviderCreator(providerType)
}
export function createDiffViewProvider(): DiffViewProvider {
if (!_diffViewProviderCreator) {
throw Error("Host providers not initialized")
}
return _diffViewProviderCreator()
}
export function getHostBridgeProvider(): HostBridgeClientProvider {
if (!_hostBridgeProvider) {
throw Error("Host providers not initialized")
+1 -1
View File
@@ -3,9 +3,9 @@ import { HostBridgeClientProvider } from "@/hosts/host-provider-types"
import * as host from "@shared/proto/index.host"
export const vscodeHostBridgeClient: HostBridgeClientProvider = {
uriServiceClient: createGrpcClient(host.UriServiceDefinition),
watchServiceClient: createGrpcClient(host.WatchServiceDefinition),
workspaceClient: createGrpcClient(host.WorkspaceServiceDefinition),
envClient: createGrpcClient(host.EnvServiceDefinition),
windowClient: createGrpcClient(host.WindowServiceDefinition),
diffClient: createGrpcClient(host.DiffServiceDefinition),
}
+5
View File
@@ -0,0 +1,5 @@
import { OpenDiffRequest, OpenDiffResponse } from "@/shared/proto/index.host"
export async function openDiff(_request: OpenDiffRequest): Promise<OpenDiffResponse> {
throw new Error("diffService.openDiff is not supported. Use the VscodeDiffViewProvider.")
}
+8
View File
@@ -0,0 +1,8 @@
import { Empty, StringRequest } from "@/shared/proto/common"
import * as vscode from "vscode"
export async function openExternal(request: StringRequest): Promise<Empty> {
console.log("openExternal called with request:", request)
await vscode.env.openExternal(vscode.Uri.parse(request.value))
return Empty.create()
}
-20
View File
@@ -1,20 +0,0 @@
import * as vscode from "vscode"
import { Uri } from "@shared/proto/host/uri"
import { StringRequest } from "@shared/proto/common"
/**
* Creates a file URI from a file path
* @param request The request containing the file path
* @returns A URI object representing the file
*/
export async function file(request: StringRequest): Promise<Uri> {
const uri = vscode.Uri.file(request.value)
return Uri.create({
scheme: uri.scheme,
authority: uri.authority,
path: uri.path,
query: uri.query,
fragment: uri.fragment,
fsPath: uri.fsPath,
})
}

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