Compare commits

...

55 Commits

Author SHA1 Message Date
abeatrix 9dbf850627 changeset 2025-07-15 17:59:01 -07:00
abeatrix e042ba5a03 Fix: pass items to showMessage in VS Code host bridge
This change allows the `showMessage` host bridge method to pass an array of `items` to the VS Code `window.show*Message` methods. This enables the display of quick pick options in the message box.
2025-07-15 17:48:05 -07:00
Nick Baumann b5e2916bd6 docs: Update Claude Code documentation to include Pro plans alongside Max plans (#4948) 2025-07-15 17:00:45 -07:00
Sarah Fortune 371db77007 Move the vscode specific classes into the hosts/vscode package. (#4947)
* Move the vscode specific classes into the `hosts/vscode` package.

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

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

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

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

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

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

* Update import path

* Move the hostbridge handlers in the a hostbridge packge.

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

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

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

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

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

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

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

The following changes were made:

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

* clean up

---------

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

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

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

* changeset

---------

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

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

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

* remove authNonce

* changeset

---------

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

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

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

* Updating CHANGELOG.md format

* Update CHANGELOG.md

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-07-15 03:57:26 -07:00
Saoud Rizwan cb9a339442 Add ability to choose chinese endpoint for Moonshot provider (#4924)
* Add ability to choose chinese endpoint for Moonshot provider

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

* Updating CHANGELOG.md format

* Update CHANGELOG.md

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-07-15 01:32:06 -07:00
Saoud Rizwan 4ecbecb1e2 Add Moonshot AI provider (#4913)
* Add Moonshot AI provider

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

* Updating CHANGELOG.md format

* Update CHANGELOG.md

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-07-14 22:19:20 -07:00
Bee 575cfd48cc Includes request ID in error returned by Cline API (#4909)
* Includes request ID in error returned by Cline API

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

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

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

* Create nasty-parents-play.md

---------

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

* improved clarity in the local development instructions

* Update CONTRIBUTING.md

* Create wise-hairs-grow.md

---------

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

* Updating CHANGELOG.md format

* Update CHANGELOG.md

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-07-14 21:01:12 -07:00
Saoud Rizwan 3069e27413 Add groq to kimi providers (#4906)
* Add groq to kimi providers

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

* add markdown parsing to mcp response

* add display mode to global state; simplify state flow

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

* changeset

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

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

* changeset

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

* More resiliency
2025-07-13 17:36:41 -07:00
Sarah Fortune 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
github-actions[bot] 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
github-actions[bot] 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 Rizwan 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
Bee 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
Bee 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
109 changed files with 2918 additions and 1870 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Display user role in organization UI
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fix missing options from window messages.
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Remove state parameter from auth callback link that allows redirect to work when multiple windows are opened
@@ -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
-4
View File
@@ -34,8 +34,4 @@ src/shared/proto/host/*.ts
# Webview
webview-ui/src/services/grpc-client.ts
# Host bridge
src/hosts/vscode/*/methods.ts
src/hosts/vscode/*/index.ts
src/hosts/vscode/client/host-grpc-client.ts
src/hosts/vscode/host-grpc-service-config.ts
src/standalone/server-setup.ts
+30 -1
View File
@@ -1,5 +1,34 @@
# Changelog
## [3.19.4]
- Add ability to choose Chinese endpoint for Moonshot provider
## [3.19.3]
- Add Moonshot AI provider
## [3.19.2]
- Show request ID in error messages returned by Cline Accounts API to help debug user reported issues
## [3.19.1]
- Fix documentation
## [3.19.0]
- Add Kimi-K2 as a recommended model in the Cline Provider, and route to Together/Groq for 131k context window and high throughput
- Added API Key support for Bedrock integration
## [3.18.14]
- Fix bug where Cline account users logged in with invalid token would not be shown as logged out in webview presentation layer
## [3.18.13]
- Fix authentication issue where Cline accounts users would keep getting logged out or seeing 'Unexpected API response' errors
## [3.18.12]
- Fix flaky organization switching behavior in Cline provider that caused UI inconsistencies and double loading
@@ -32,7 +61,7 @@
## [3.18.6]
- Update request header to include `"ai-client-type": "Cline"` to SAP Api Provider
- Add organization organization accounts
- Add organization accounts
## [3.18.5]
+1
View File
@@ -147,6 +147,7 @@
"pages": [
"provider-config/anthropic",
"provider-config/claude-code",
"provider-config/aws-bedrock-with-apikey-authentication",
"provider-config/aws-bedrock-with-credentials-authentication",
"provider-config/aws-bedrock-with-profile-authentication",
"provider-config/gcp-vertex-ai",
@@ -14,6 +14,8 @@ Certain scenarios may warrant using local models, including handling highly sens
#### [IAM Security Best Practices](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html) (For administrators)
#### [AWS Bedrock setup for API Keys](/provider-config/aws-bedrock-with-apikey-authentication)
#### [AWS Bedrock setup for Legacy IAM (AWS Credentials)](/provider-config/aws-bedrock-with-credentials-authentication)
#### [AWS Bedrock setup for SSO token (AWS Profile)](/provider-config/aws-bedrock-with-profile-authentication)
@@ -0,0 +1,135 @@
---
title: "AWS Bedrock"
description: "Learn how to set up AWS Bedrock with Cline using credentials authentication. This guide covers AWS environment setup, regional access verification, and secure integration with the Cline VS Code extension."
---
### Overview
- **AWS Bedrock:** A fully managed service that offers access to leading generative AI models (e.g., Anthropic Claude, Amazon Nova) through AWS.\
[Learn more about AWS Bedrock](https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html).
- **Cline:** A VS Code extension that acts as a coding assistant by integrating with AI models—empowering developers to generate code, debug, and analyze data.
- **Developer Focus:** This guide is tailored for individual developers that want to enable access to frontier models via AWS Bedrock with a simplified setup using API Keys.
---
### Step 1: Prepare Your AWS Environment
#### 1.1 Individual user setup - Create a Bedrock API Key
For more detailed instructions check the [documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/api-keys.html).
1. **Sign in to the AWS Management Console:**\
[AWS Console](https://aws.amazon.com/console/)
2. **Access Bedrock Console:**
- [Bedrock Console](https://console.aws.amazon.com/bedrock)
- Create a new Long Lived API Key. This API Key will have by default the `AmazonBedrockLimitedAccess` IAM policy
[View AmazonBedrockLimitedAccess Policy Details](https://docs.aws.amazon.com/bedrock/latest/userguide/security-iam.html)
#### 1.2 Create or Modify the Policy
To ensure Cline can interact with AWS Bedrock, your IAM user or role needs specific permissions. While the `AmazonBedrockLimitedAccess` managed policy provides comprehensive access, for a more restricted and secure setup adhering to the principle of least privilege, the following minimal permissions are sufficient for Cline's core model invocation functionality:
- `bedrock:InvokeModel`
- `bedrock:InvokeModelWithResponseStream`
- `bedrock:CallWithBearerToken`
You can create a custom IAM policy with these permissions and attach it to your IAM user or role.
1. In the AWS IAM console, create a new policy.
2. Use the JSON editor to add the following policy document:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream", "bedrock:CallWithBearerToken"],
"Resource": "*" // For enhanced security, scope this to specific model ARNs if possible.
}
]
}
```
3. Name the policy (e.g., `ClineBedrockInvokeAccess`) and attach it to the IAM user associated with the key you created. The IAM user and the API key have the same prefix.
**Important Considerations:**
- **Model Listing in Cline:** The minimal permissions (`bedrock:InvokeModel`, `bedrock:InvokeModelWithResponseStream`) are sufficient for Cline to _use_ a model if you specify the model ID directly in Cline's settings. If you rely on Cline to dynamically list available Bedrock models, you might need additional permissions like `bedrock:ListFoundationModels`.
- **AWS Marketplace Subscriptions:** For third-party models (e.g., Anthropic Claude), the **`AmazonBedrockLimitedAccess`** policy grants you the necessary permissions to subscribe via the AWS Marketplace. There is no explicit access to be enabled. For Anthropic models you are still required to submit a First Time Use (FTU) form via the Console. If you get the following message in the Cline chat `[ERROR] Failed to process response: Model use case details have not been submitted for this account. Fill out the Anthropic use case details form before using the model.` then open the [Playground in the AWS Bedrock Console](https://console.aws.amazon.com/bedrock/home?#/text-generation-playground), select any Anthropic model and fill in the form (you might need to send a prompt first)
---
### Step 2: Verify Regional and Model Access
#### 2.1 Choose and Confirm a Region
1. **Select a Region:**\
AWS Bedrock is available in multiple regions (e.g., US East, Europe, Asia Pacific). Choose the region that meets your latency and compliance needs.\
[AWS Global Infrastructure](https://aws.amazon.com/about-aws/global-infrastructure/regions_az/)
2. **Verify Model Access:**
- **Note:** Some models are only accessible via an [Inference Profile](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html). In such case check the box "Cross Region Inference".
---
### Step 3: Configure the Cline VS Code Extension
#### 3.1 Install and Open Cline
1. **Install VS Code:**\
Download from the [VS Code website](https://code.visualstudio.com/).
2. **Install the Cline Extension:**
- Open VS Code.
- Go to the Extensions Marketplace (`Ctrl+Shift+X` or `Cmd+Shift+X`).
- Search for **Cline** and install it.
#### 3.2 Configure Cline Settings
1. **Open Cline Settings:**
- Click on the settings ⚙️ to select your API Provider.
2. **Select AWS Bedrock as the API Provider:**
- From the API Provider dropdown, choose **AWS Bedrock**.
3. **Enter Your AWS API Key:**
- Input your **API Key**
- Specify the correct **AWS Region** (e.g., `us-east-1` or your enterprise-approved region).
4. **Select a Model:**
- Choose an on-demand model (e.g., **anthropic.claude-3-5-sonnet-20241022-v2:0**).
5. **Save and Test:**
- Click **Done/Save** to apply your settings.
- Test the integration by sending a simple prompt (e.g., "Generate a Python function to check if a number is prime.").
---
### Step 4: Security, Monitoring, and Best Practices
1. **Secure Access:**
- Prefer AWS SSO/federated roles over long-lived API Key when possible.
- [AWS IAM Best Practices](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html)
2. **Enhance Network Security:**
- Consider setting up [AWS PrivateLink](https://docs.aws.amazon.com/vpc/latest/userguide/endpoint-services-overview.html) to securely connect to Bedrock.
3. **Monitor and Log Activity:**
- Enable AWS CloudTrail to log Bedrock API calls.
- Use CloudWatch to monitor metrics like invocation count, latency, and token usage.
- Set up alerts for abnormal activity.
4. **Handle Errors and Manage Costs:**
- Implement exponential backoff for throttling errors.
- Use AWS Cost Explorer and set billing alerts to track usage.\
[AWS Cost Management](https://docs.aws.amazon.com/cost-management/latest/userguide/what-is-aws-cost-management.html)
5. **Regular Audits and Compliance:**
- Periodically review IAM roles and CloudTrail logs.
- Follow internal data privacy and governance policies.
---
### Conclusion
By following these steps, your enterprise team can securely integrate AWS Bedrock with the Cline VS Code extension to accelerate development:
1. **Prepare Your AWS Environment:** Create or use a secure IAM role/user, attach the `AmazonBedrockLimitedAccess` policy, and ensure necessary permissions.
2. **Verify Region and Model Access:** Confirm that your selected region supports your required models.
3. **Configure Cline in VS Code:** Install and set up Cline with your AWS credentials and choose an appropriate model.
4. **Implement Security and Monitoring:** Use best practices for IAM, network security, monitoring, and cost management.
For further details, consult the [AWS Bedrock Documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html) and coordinate with your internal cloud team. Happy coding!
---
_This guide will be updated as AWS Bedrock and Cline evolve. Always refer to the latest documentation and internal policies for up-to-date practices._
@@ -5,7 +5,7 @@ description: "Learn how to set up AWS Bedrock with Cline using credentials authe
### Overview
- **AWS Bedrock:** A fully managed service that offers access to leading generative AI models (e.g., Anthropic Claude, Amazon Titan) through AWS.\
- **AWS Bedrock:** A fully managed service that offers access to leading generative AI models (e.g., Anthropic Claude, Amazon Nova) through AWS.\
[Learn more about AWS Bedrock](https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html).
- **Cline:** A VS Code extension that acts as a coding assistant by integrating with AI models—empowering developers to generate code, debug, and analyze data.
- **Enterprise Focus:** This guide is tailored for organizations with established AWS environments (using IAM roles, AWS SSO, AWS Organizations, etc.) to ensure secure and compliant usage.
@@ -25,7 +25,7 @@ description: "Learn how to set up AWS Bedrock with Cline using credentials authe
#### 1.2 Attach the Required Policies
To ensure Cline can interact with AWS Bedrock, your IAM user or role needs specific permissions. While the `AmazonBedrockFullAccess` managed policy provides comprehensive access, for a more restricted and secure setup adhering to the principle of least privilege, the following minimal permissions are sufficient for Cline's core model invocation functionality:
To ensure Cline can interact with AWS Bedrock, your IAM user or role needs specific permissions. While the `AmazonBedrockLimitedAccess` managed policy provides comprehensive access, for a more restricted and secure setup adhering to the principle of least privilege, the following minimal permissions are sufficient for Cline's core model invocation functionality:
- `bedrock:InvokeModel`
- `bedrock:InvokeModelWithResponseStream`
@@ -52,8 +52,8 @@ You can create a custom IAM policy with these permissions and attach it to your
**Option 2: Using a Managed Policy (Simpler Initial Setup)**
- Alternatively, you can attach the AWS managed policy **`AmazonBedrockFullAccess`**. This grants broader permissions, including the ability to list models, manage provisioning, and other Bedrock features. This might be simpler for initial setup or if you require these wider capabilities.
[View AmazonBedrockFullAccess Policy Details](https://docs.aws.amazon.com/bedrock/latest/userguide/security-iam.html)
- Alternatively, you can attach the AWS managed policy **`AmazonBedrockLimitedAccess`**. This grants broader permissions, including the ability to list models, manage provisioning, and other Bedrock features. This might be simpler for initial setup or if you require these wider capabilities.
[View AmazonBedrockLimitedAccess Policy Details](https://docs.aws.amazon.com/bedrock/latest/userguide/security-iam.html)
**Important Considerations:**
@@ -71,8 +71,8 @@ You can create a custom IAM policy with these permissions and attach it to your
AWS Bedrock is available in multiple regions (e.g., US East, Europe, Asia Pacific). Choose the region that meets your latency and compliance needs.\
[AWS Global Infrastructure](https://aws.amazon.com/about-aws/global-infrastructure/regions_az/)
2. **Verify Model Access:**
- In the AWS Bedrock console, confirm that the models your team requires (e.g., Anthropic Claude, Amazon Titan) are marked as "Access granted."
- **Note:** Some advanced models might require an [Inference Profile](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-prereq.html) if not available on-demand.
- In the AWS Bedrock console, confirm that the models your team requires (e.g., Anthropic Claude, Amazon Nova) are marked as "Access granted."
- **Note:** Some advanced models might require an [Inference Profile](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html) if not available on-demand.
#### 2.2 Set Up AWS Marketplace Subscriptions (if needed)
@@ -138,7 +138,7 @@ You can create a custom IAM policy with these permissions and attach it to your
By following these steps, your enterprise team can securely integrate AWS Bedrock with the Cline VS Code extension to accelerate development:
1. **Prepare Your AWS Environment:** Create or use a secure IAM role/user, attach the `AmazonBedrockFullAccess` policy, and ensure necessary permissions.
1. **Prepare Your AWS Environment:** Create or use a secure IAM role/user, attach the `AmazonBedrockLimitedAccess` policy, and ensure necessary permissions.
2. **Verify Region and Model Access:** Confirm that your selected region supports your required models and subscribe via AWS Marketplace if needed.
3. **Configure Cline in VS Code:** Install and set up Cline with your AWS credentials and choose an appropriate model.
4. **Implement Security and Monitoring:** Use best practices for IAM, network security, monitoring, and cost management.
+2 -2
View File
@@ -1,11 +1,11 @@
---
title: "Claude Code"
description: "Use your Claude Max subscription with Cline instead of paying per token. Learn how to set up and configure the Claude Code provider."
description: "Use your Claude Max or Pro subscription with Cline instead of paying per token. Learn how to set up and configure the Claude Code provider."
---
**Website:** [https://docs.anthropic.com/en/docs/claude-code/setup](https://docs.anthropic.com/en/docs/claude-code/setup)
The Claude Code provider lets you use your existing Claude subscription with Cline. If you have Claude Max, this means you can use Claude in Cline without paying extra API costs.
The Claude Code provider lets you use your existing Claude subscription with Cline. If you have Claude Max or Pro, this means you can use Claude in Cline without paying extra API costs.
<Frame>
<img
+2 -2
View File
@@ -153,8 +153,8 @@ const extensionConfig = {
// Standalone-specific configuration
const standaloneConfig = {
...baseConfig,
entryPoints: ["src/standalone/standalone.ts"],
outfile: `${destDir}/standalone.js`,
entryPoints: ["src/standalone/cline-core.ts"],
outfile: `${destDir}/cline-core.js`,
// These gRPC protos need to load files from the module directory at runtime,
// so they cannot be bundled.
external: ["vscode", "@grpc/reflection", "grpc-health-check"],
+1 -1
View File
@@ -36,7 +36,7 @@ It starts with our test cases. Each one is a JSON file in `./cases` that has the
Then, for every test run, we set up a specific configuration. This includes which LLM we're testing, which system prompt it gets, which function we use to parse the model's raw output, and which function we use to actually apply the diff. Here's the command I've been using:
```bash
npm run diff-eval -- --model-ids "anthropic/claude-3-5-sonnet-20241022,x-ai/grok-3-beta" --max-cases 4 --valid-attempts-per-case 2 --verbose --parallel
npm run diff-eval -- --model-ids "anthropic/claude-3-5-sonnet,x-ai/grok-3-beta,anthropic/claude-3.7-sonnet,anthropic/claude-sonnet-4,google/gemini-2.5-pro-preview,google/gemini-2.5-flash" --max-cases 5 --valid-attempts-per-case 5 --parallel --diff-edit-function diff-06-26-25 --verbose
```
This will build the eval script, run it, and then open the streamlit dashboard to show the results.
+584 -836
View File
File diff suppressed because it is too large Load Diff
+6 -5
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.12",
"version": "3.19.4",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -330,12 +330,12 @@
"watch:esbuild": "node esbuild.js --watch",
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
"package": "npm run check-types && npm run build:webview && npm run lint && node esbuild.js --production",
"protos": "node proto/build-proto.js && node scripts/generate-server-setup.mjs && node scripts/generate-host-bridge-client.mjs",
"protos": "node scripts/build-proto.mjs && node scripts/generate-server-setup.mjs && node scripts/generate-host-bridge-client.mjs",
"postprotos": "prettier src/shared/proto src/core/controller src/hosts/ webview-ui/src/services src/generated --write --log-level warn",
"clean": "rimraf dist dist-standalone webview-ui/build src/generated out/",
"compile-tests": "node ./scripts/build-tests.js",
"watch-tests": "tsc -p . -w --outDir out",
"check-types": "npm run protos && tsc --noEmit",
"check-types": "npm run protos && npx tsc --noEmit && cd webview-ui && npx tsc -b --noEmit",
"lint": "eslint src --ext ts && eslint webview-ui/src --ext ts && buf lint && cd webview-ui && npm run lint",
"format": "prettier . --check",
"format:fix": "prettier . --write",
@@ -409,8 +409,8 @@
"dependencies": {
"@anthropic-ai/sdk": "^0.37.0",
"@anthropic-ai/vertex-sdk": "^0.6.4",
"@aws-sdk/client-bedrock-runtime": "^3.826.0",
"@aws-sdk/credential-providers": "^3.826.0",
"@aws-sdk/client-bedrock-runtime": "^3.840.0",
"@aws-sdk/credential-providers": "^3.840.0",
"@bufbuild/protobuf": "^2.2.5",
"@cerebras/cerebras_cloud_sdk": "^1.35.0",
"@google-cloud/vertexai": "^1.9.3",
@@ -449,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",
+39
View File
@@ -0,0 +1,39 @@
syntax = "proto3";
package host;
option java_package = "bot.cline.host.proto";
option java_multiple_files = true;
import "common.proto";
// Provides methods for diff views.
service DiffService {
// Open the diff view/editor.
rpc openDiff(OpenDiffRequest) returns (OpenDiffResponse);
rpc replaceText(ReplaceTextRequest) returns (ReplaceTextResponse);
}
message OpenDiffRequest {
optional cline.Metadata metadata = 1;
// The absolute path of the document being edited.
optional string path = 2;
// The new content for the file.
optional string content = 3;
}
message OpenDiffResponse {
// A unique identifier for the diff view that was opened.
optional string diff_id = 1;
}
message ReplaceTextRequest {
optional cline.Metadata metadata = 1;
optional string diff_id = 2;
optional string content = 3;
optional int32 start_line = 4;
optional int32 end_line = 5;
}
message ReplaceTextResponse {
// TBD
}
+25
View File
@@ -11,6 +11,7 @@ service WindowService {
// Opens a text document in the editor and returns editor information.
rpc showTextDocument(ShowTextDocumentRequest) returns (TextEditorInfo);
rpc showOpenDialogue(ShowOpenDialogueRequest) returns (SelectedResources);
rpc showMessage(ShowMessageRequest) returns (SelectedResponse);
}
message ShowTextDocumentRequest {
@@ -46,3 +47,27 @@ message ShowOpenDialogueFilterOption {
message SelectedResources {
repeated string paths = 1;
}
enum ShowMessageType {
ERROR = 0;
INFORMATION = 1;
WARNING = 2;
}
message ShowMessageRequest {
cline.Metadata metadata = 1;
ShowMessageType type = 2;
string message = 3;
optional ShowMessageRequestOptions options = 4;
}
message ShowMessageRequestOptions {
repeated string items = 1;
optional bool modal = 2;
optional string detail = 3;
}
message SelectedResponse {
optional string selected_option = 1;
}
+6 -1
View File
@@ -122,6 +122,7 @@ enum ApiProvider {
CEREBRAS = 23;
SAPAICORE = 24;
CLAUDE_CODE = 25;
MOONSHOT = 26;
}
// Model info for OpenAI-compatible models
@@ -235,4 +236,8 @@ message ModelsApiConfiguration {
optional string sap_ai_core_token_url = 71;
optional string sap_ai_core_base_url = 72;
optional string claude_code_path = 73;
}
optional string aws_authentication = 74;
optional string aws_bedrock_api_key = 75;
optional string moonshot_api_key = 76;
optional string moonshot_api_line = 77;
}
-3
View File
@@ -1,3 +0,0 @@
{
"type": "module"
}
+9 -1
View File
@@ -112,7 +112,7 @@ message UpdateSettingsRequest {
optional int64 shell_integration_timeout = 8;
optional bool terminal_reuse_enabled = 9;
optional bool mcp_responses_collapsed = 10;
optional bool mcp_rich_display_enabled = 11;
optional string mcp_display_mode = 11;
optional int64 terminal_output_line_limit = 12;
}
@@ -232,4 +232,12 @@ message ApiConfiguration {
// Claude Code specific
optional string claude_code_path = 77;
// Extension fields for Bedrock Api Keys
optional string aws_authentication = 78;
optional string aws_bedrock_api_key = 79;
// Moonshot
optional string moonshot_api_key = 80;
optional string moonshot_api_line = 81;
}
@@ -27,5 +27,6 @@ export const hostServiceNameMap = {
workspace: "host.WorkspaceService",
env: "host.EnvService",
window: "host.WindowService",
diff: "host.DiffService",
// Add new host services here
}
@@ -9,22 +9,22 @@ import chalk from "chalk"
import os from "os"
import { createRequire } from "module"
import { serviceNameMap, hostServiceNameMap } from "./build-proto-config.js"
import { serviceNameMap, hostServiceNameMap } from "./build-proto-config.mjs"
const require = createRequire(import.meta.url)
const PROTOC = path.join(require.resolve("grpc-tools"), "../bin/protoc")
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url))
const ROOT_DIR = path.resolve(SCRIPT_DIR, "..")
const SCRIPT_NAME = path.relative(process.cwd(), fileURLToPath(import.meta.url))
const TS_OUT_DIR = path.join(ROOT_DIR, "src", "shared", "proto")
const GRPC_JS_OUT_DIR = path.join(ROOT_DIR, "src", "generated", "grpc-js")
const NICE_JS_OUT_DIR = path.join(ROOT_DIR, "src", "generated", "nice-grpc")
const DESCRIPTOR_OUT_DIR = path.join(ROOT_DIR, "dist-standalone", "proto")
const PROTO_DIR = path.resolve("proto")
const TS_OUT_DIR = path.resolve("src/shared/proto")
const GRPC_JS_OUT_DIR = path.resolve("src/generated/grpc-js")
const NICE_JS_OUT_DIR = path.resolve("src/generated/nice-grpc")
const DESCRIPTOR_OUT_DIR = path.resolve("dist-standalone/proto")
const isWindows = process.platform === "win32"
const TS_PROTO_PLUGIN = isWindows
? path.join(ROOT_DIR, "node_modules", ".bin", "protoc-gen-ts_proto.cmd") // Use the .bin directory path for Windows
? path.resolve("node_modules/.bin/protoc-gen-ts_proto.cmd") // Use the .bin directory path for Windows
: require.resolve("ts-proto/protoc-gen-ts_proto")
const TS_PROTO_OPTIONS = [
@@ -37,12 +37,7 @@ const TS_PROTO_OPTIONS = [
]
// Service directories derived from imported serviceNameMap
const serviceDirs = Object.keys(serviceNameMap).map((serviceKey) => path.join(ROOT_DIR, "src", "core", "controller", serviceKey))
// Host service directories derived from imported hostServiceNameMap
const hostServiceDirs = Object.keys(hostServiceNameMap).map((serviceKey) =>
path.join(ROOT_DIR, "src", "hosts", "vscode", serviceKey),
)
const serviceDirs = Object.keys(serviceNameMap).map((serviceKey) => path.join("src/core/controller", serviceKey))
async function main() {
console.log(chalk.bold.blue("Starting Protocol Buffer code generation..."))
@@ -61,8 +56,8 @@ async function main() {
await ensureProtoFilesExist()
// Process all proto files
const protoFiles = await globby("**/*.proto", { cwd: SCRIPT_DIR, realpath: true })
console.log(chalk.cyan(`Processing ${protoFiles.length} proto files from`), SCRIPT_DIR)
const protoFiles = await globby("**/*.proto", { cwd: PROTO_DIR, realpath: true })
console.log(chalk.cyan(`Processing ${protoFiles.length} proto files from`), PROTO_DIR)
tsProtoc(TS_OUT_DIR, protoFiles, TS_PROTO_OPTIONS)
// grpc-js is used to generate service impls for the ProtoBus service.
@@ -73,7 +68,7 @@ async function main() {
const descriptorFile = path.join(DESCRIPTOR_OUT_DIR, "descriptor_set.pb")
const descriptorProtocCommand = [
PROTOC,
`--proto_path="${SCRIPT_DIR}"`,
`--proto_path="${PROTO_DIR}"`,
`--descriptor_set_out="${descriptorFile}"`,
"--include_imports",
...protoFiles,
@@ -89,11 +84,12 @@ async function main() {
log_verbose(chalk.green("Protocol Buffer code generation completed successfully."))
log_verbose(chalk.green(`TypeScript files generated in: ${TS_OUT_DIR}`))
await generateMethodRegistrations()
await generateHostMethodRegistrations()
await generateServiceConfig()
await generateHostServiceConfig()
await generateGrpcClientConfig()
await generateProtoBusServiceConfig()
await generateProtoBusMethodRegistrations()
await generateProtoBusGrpcClientConfig()
await generateHostBridgeServiceConfig()
await generateHostBridgeMethodRegistrations()
console.log(chalk.bold.blue("Finished Protocol Buffer code generation."))
}
@@ -102,7 +98,7 @@ async function tsProtoc(outDir, protoFiles, protoOptions) {
// Build the protoc command with proper path handling for cross-platform
const command = [
PROTOC,
`--proto_path="${SCRIPT_DIR}"`,
`--proto_path="${PROTO_DIR}"`,
`--plugin=protoc-gen-ts_proto="${TS_PROTO_PLUGIN}"`,
`--ts_proto_out="${outDir}"`,
`--ts_proto_opt=${protoOptions.join(",")} `,
@@ -122,7 +118,7 @@ async function tsProtoc(outDir, protoFiles, protoOptions) {
* Generate a gRPC client configuration file for the webview
* This eliminates the need for manual imports and client creation in grpc-client.ts
*/
async function generateGrpcClientConfig() {
async function generateProtoBusGrpcClientConfig() {
log_verbose(chalk.cyan("Generating gRPC client configuration..."))
const serviceImports = []
@@ -147,7 +143,7 @@ async function generateGrpcClientConfig() {
// Generate the file content
const content = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Generated by ${SCRIPT_NAME}
import { createGrpcClient } from "./grpc-client-base"
${serviceImports.join("\n")}
@@ -158,7 +154,7 @@ export {
${serviceExports.join(",\n\t")}
}`
const filePath = path.join(ROOT_DIR, "webview-ui", "src", "services", "grpc-client.ts")
const filePath = path.resolve("webview-ui/src/services/grpc-client.ts")
await writeFileWithMkdirs(filePath, content)
log_verbose(chalk.green(`Generated gRPC client at ${filePath}`))
}
@@ -221,12 +217,12 @@ async function parseProtoForStreamingMethods(protoFiles, scriptDir) {
return streamingMethodsMap
}
async function generateMethodRegistrations() {
async function generateProtoBusMethodRegistrations() {
log_verbose(chalk.cyan("Generating method registration files..."))
// Parse proto files for streaming methods
const protoFiles = await globby("*.proto", { cwd: SCRIPT_DIR })
const streamingMethodsMap = await parseProtoForStreamingMethods(protoFiles, SCRIPT_DIR)
const protoFiles = await globby("*.proto", { cwd: PROTO_DIR })
const streamingMethodsMap = await parseProtoForStreamingMethods(protoFiles, PROTO_DIR)
for (const serviceDir of serviceDirs) {
const serviceName = path.basename(serviceDir)
@@ -243,7 +239,7 @@ async function generateMethodRegistrations() {
// Create the methods.ts file with header
let methodsContent = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Generated by ${SCRIPT_NAME}
// Import all method implementations
import { registerMethod } from "./index"\n`
@@ -292,7 +288,7 @@ export function registerAllMethods(): void {
// Generate index.ts file
const capitalizedServiceName = serviceName.charAt(0).toUpperCase() + serviceName.slice(1)
const indexContent = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Generated by ${SCRIPT_NAME}
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../grpc-service"
import { StreamingResponseHandler } from "../grpc-handler"
@@ -327,7 +323,7 @@ registerAllMethods()`
* Generate a service configuration file that maps service names to their handlers
* This eliminates the need for manual switch/case statements in grpc-handler.ts
*/
async function generateServiceConfig() {
async function generateProtoBusServiceConfig() {
log_verbose(chalk.cyan("Generating service configuration file..."))
const serviceImports = []
@@ -347,7 +343,7 @@ async function generateServiceConfig() {
}
const content = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Generated by ${SCRIPT_NAME}
import { Controller } from "./index"
import { StreamingResponseHandler } from "./grpc-handler"
@@ -367,7 +363,7 @@ export interface ServiceHandlerConfig {
export const serviceHandlers: Record<string, ServiceHandlerConfig> = {${serviceConfigs.join(",")}
};`
const configPath = path.join(ROOT_DIR, "src", "core", "controller", "grpc-service-config.ts")
const configPath = path.resolve("src/core/controller/grpc-service-config.ts")
await writeFileWithMkdirs(configPath, content)
log_verbose(chalk.green(`Generated service configuration at ${configPath}`))
}
@@ -380,7 +376,7 @@ async function ensureProtoFilesExist() {
log_verbose(chalk.cyan("Checking for missing proto files..."))
// Get existing proto files
const existingProtoFiles = await globby("*.proto", { cwd: SCRIPT_DIR })
const existingProtoFiles = await globby("*.proto", { cwd: PROTO_DIR })
const existingProtoServices = existingProtoFiles.map((file) => path.basename(file, ".proto"))
// Check each service in serviceNameMap
@@ -417,7 +413,7 @@ service ${serviceClassName} {
`
// Write the template proto file
const protoFilePath = path.join(SCRIPT_DIR, `${serviceName}.proto`)
const protoFilePath = path.join(PROTO_DIR, `${serviceName}.proto`)
await fs.writeFile(protoFilePath, protoContent)
log_verbose(chalk.green(`Created template proto file at ${protoFilePath}`))
}
@@ -427,17 +423,22 @@ service ${serviceClassName} {
/**
* Generate method registration files for host services
*/
async function generateHostMethodRegistrations() {
async function generateHostBridgeMethodRegistrations() {
log_verbose(chalk.cyan("Generating host method registration files..."))
// Host service directories derived from imported hostServiceNameMap
const hostServiceDirs = Object.keys(hostServiceNameMap).map((serviceKey) =>
path.join("src/hosts/vscode/hostbridge", serviceKey),
)
// Parse proto files for streaming methods
const hostProtoFiles = await globby("*.proto", { cwd: path.join(SCRIPT_DIR, "host") })
const streamingMethodsMap = await parseProtoForStreamingMethods(hostProtoFiles, path.join(SCRIPT_DIR, "host"))
const hostProtoFiles = await globby("*.proto", { cwd: path.join(PROTO_DIR, "host") })
const streamingMethodsMap = await parseProtoForStreamingMethods(hostProtoFiles, path.join(PROTO_DIR, "host"))
for (const serviceDir of hostServiceDirs) {
const serviceName = path.basename(serviceDir)
const fullServiceName = hostServiceNameMap[serviceName]
const streamingMethods = streamingMethodsMap.get(fullServiceName) || []
const outputDir = path.join("src/generated/hosts/vscode/hostbridge", serviceName)
log_verbose(chalk.cyan(`Generating method registrations for host ${serviceName}...`))
@@ -449,7 +450,7 @@ async function generateHostMethodRegistrations() {
// Create the methods.ts file with header
let methodsContent = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Generated ${SCRIPT_NAME}
// Import all method implementations
import { registerMethod } from "./index"\n`
@@ -457,7 +458,7 @@ import { registerMethod } from "./index"\n`
// Import implementations directly
for (const file of implementationFiles) {
const baseName = path.basename(file, ".ts")
methodsContent += `import { ${baseName} } from "./${baseName}"\n`
methodsContent += `import { ${baseName} } from "@hosts/vscode/hostbridge/${serviceName}/${baseName}"\n`
}
// Add streaming methods information
@@ -491,17 +492,17 @@ export function registerAllMethods(): void {
methodsContent += `}`
// Write the methods.ts file
const registryFile = path.join(serviceDir, "methods.ts")
const registryFile = path.join(outputDir, "methods.ts")
await writeFileWithMkdirs(registryFile, methodsContent)
log_verbose(chalk.green(`Generated ${registryFile}`))
// Generate index.ts file
const capitalizedServiceName = serviceName.charAt(0).toUpperCase() + serviceName.slice(1)
const indexContent = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Generated by ${SCRIPT_NAME}
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../host-grpc-service"
import { StreamingResponseHandler } from "../host-grpc-handler"
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "@hosts/vscode/hostbridge-grpc-service"
import { StreamingResponseHandler } from "@hosts/vscode/hostbridge-grpc-handler"
import { registerAllMethods } from "./methods"
// Create ${serviceName} service registry
@@ -521,7 +522,7 @@ export const isStreamingMethod = ${serviceName}Service.isStreamingMethod
registerAllMethods()`
// Write the index.ts file
const indexFile = path.join(serviceDir, "index.ts")
const indexFile = path.join(outputDir, "index.ts")
await writeFileWithMkdirs(indexFile, indexContent)
log_verbose(chalk.green(`Generated ${indexFile}`))
}
@@ -532,7 +533,7 @@ registerAllMethods()`
/**
* Generate a service configuration file for host services
*/
async function generateHostServiceConfig() {
async function generateHostBridgeServiceConfig() {
log_verbose(chalk.cyan("Generating host service configuration file..."))
const serviceImports = []
@@ -542,7 +543,7 @@ async function generateHostServiceConfig() {
for (const [dirName, fullServiceName] of Object.entries(hostServiceNameMap)) {
const capitalizedName = dirName.charAt(0).toUpperCase() + dirName.slice(1)
serviceImports.push(
`import { handle${capitalizedName}ServiceRequest, handle${capitalizedName}ServiceStreamingRequest } from "./${dirName}/index"`,
`import { handle${capitalizedName}ServiceRequest, handle${capitalizedName}ServiceStreamingRequest } from "@generated/hosts/vscode/hostbridge/${dirName}/index"`,
)
serviceConfigs.push(`
"${fullServiceName}": {
@@ -552,9 +553,9 @@ async function generateHostServiceConfig() {
}
const content = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Generated by ${SCRIPT_NAME}
import { StreamingResponseHandler } from "./host-grpc-handler"
import { StreamingResponseHandler } from "@/hosts/vscode/hostbridge-grpc-handler"
${serviceImports.join("\n")}
/**
@@ -571,7 +572,7 @@ export interface HostServiceHandlerConfig {
export const hostServiceHandlers: Record<string, HostServiceHandlerConfig> = {${serviceConfigs.join(",")}
};`
const filePath = path.join(ROOT_DIR, "src/hosts/vscode/host-grpc-service-config.ts")
const filePath = "src/generated/hosts/vscode/hostbridge-grpc-service-config.ts"
await writeFileWithMkdirs(filePath, content)
log_verbose(chalk.green(`Generated host service configuration at ${filePath}`))
}
@@ -583,15 +584,33 @@ async function cleanup() {
for (const file of existingFiles) {
await fs.unlink(path.join(TS_OUT_DIR, file))
}
await rmdir(path.join(ROOT_DIR, "src", "generated"))
await rmdir("src/generated")
// Clean up generated files that were moved.
await fs.rm(path.join(ROOT_DIR, "src", "standalone", "services", "host-grpc-client.ts"), { force: true })
await rmdir(path.join(ROOT_DIR, "src", "standalone", "services"))
await fs.rm(path.join(ROOT_DIR, "hosts", "vscode"), { force: true, recursive: true })
await rmdir(path.join(ROOT_DIR, "hosts"))
await fs.rm("src/standalone/services/host-grpc-client.ts", { force: true })
await rmdir("src/standalone/services")
await fs.rm("hosts/vscode", { force: true, recursive: true })
await rmdir("hosts")
await fs.rm(path.join(ROOT_DIR, "src/standalone/server-setup.ts"), { force: true })
await fs.rm("src/standalone/server-setup.ts", { force: true })
await fs.rm("src/hosts/vscode/host-grpc-service-config.ts", { force: true })
const oldhostbridgefiles = [
"src/hosts/vscode/workspace/methods.ts",
"src/hosts/vscode/workspace/index.ts",
"src/hosts/vscode/diff/methods.ts",
"src/hosts/vscode/diff/index.ts",
"src/hosts/vscode/env/methods.ts",
"src/hosts/vscode/env/index.ts",
"src/hosts/vscode/window/methods.ts",
"src/hosts/vscode/window/index.ts",
"src/hosts/vscode/watch/methods.ts",
"src/hosts/vscode/watch/index.ts",
"src/hosts/vscode/uri/methods.ts",
"src/hosts/vscode/uri/index.ts",
]
for (const file of oldhostbridgefiles) {
await fs.rm(file, { force: true })
}
}
/**
+16 -2
View File
@@ -5,17 +5,31 @@ DIR=${1:-src/}
DEST_DIR=dist-standalone
SDK_DEST=$DEST_DIR/vscode-sdk-uses.txt
CSS_DEST=$DEST_DIR/vscode-css-uses.txt
TMP=/tmp/vscode-sdk-uses.txt.tmp
mkdir -p $DEST_DIR
{
git grep -h 'vscode\.' $DIR |
grep -Ev '//.*vscode' | # remove commented out code
grep -v vscode.commands.executeCommand | # executeCommand is handled separately
grep -Ev '"vscode' | # remove command strings that get included because they start with vscode
sed 's|.*vscode\.|vscode.|'| # remove everything before vscode.
sed 's/[^a-zA-Z0-9_.].*$//' | # remove everything after last identifier
grep -E '\.[a-z][^.]+$' | # remove types (last part of identifier should be lowercase)
sort | uniq -c | sort -n | # Count occurrences
cat > $SDK_DEST
cat > $TMP
}
{
grep -rh vscode.commands.executeCommand $DIR |
perl -ne 'print if /["\x27"]/' | # Remove occurrences where the command is not on the same line (line doesnt contain quote chars) :(
sed -n 's|.*\(vscode.commands.executeCommand[^,]*\).*|\1|p'| # Remove all params after the first one
sed 's|\(".*"\).*|\1)|'| # Close the parantheses
cat >> $TMP
}
# Count occurrences
cat $TMP | sort | uniq -c | sort -n > $SDK_DEST
rm $TMP
echo Wrote uses of the vscode SDK to $(realpath $SDK_DEST)
{
+9
View File
@@ -27,6 +27,7 @@ import { SambanovaHandler } from "./providers/sambanova"
import { CerebrasHandler } from "./providers/cerebras"
import { SapAiCoreHandler } from "./providers/sapaicore"
import { ClaudeCodeHandler } from "./providers/claude-code"
import { MoonshotHandler } from "./providers/moonshot"
export interface ApiHandler {
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream
@@ -63,6 +64,8 @@ function createHandlerForProvider(apiProvider: string | undefined, options: Omit
awsSecretKey: options.awsSecretKey,
awsSessionToken: options.awsSessionToken,
awsRegion: options.awsRegion,
awsAuthentication: options.awsAuthentication,
awsBedrockApiKey: options.awsBedrockApiKey,
awsUseCrossRegionInference: options.awsUseCrossRegionInference,
awsBedrockUsePromptCache: options.awsBedrockUsePromptCache,
awsUseProfile: options.awsUseProfile,
@@ -185,6 +188,12 @@ function createHandlerForProvider(apiProvider: string | undefined, options: Omit
liteLlmUsePromptCache: options.liteLlmUsePromptCache,
taskId: options.taskId,
})
case "moonshot":
return new MoonshotHandler({
moonshotApiKey: options.moonshotApiKey,
moonshotApiLine: options.moonshotApiLine,
apiModelId: options.apiModelId,
})
case "nebius":
return new NebiusHandler({
nebiusApiKey: options.nebiusApiKey,
@@ -182,6 +182,24 @@ describe("AwsBedrockHandler", () => {
process.env["AWS_PROFILE"]!.should.equal(preAWSProfile)
})
it("should work with AWS_BEARER_TOKEN_BEDROCK", async () => {
process.env["AWS_BEARER_TOKEN_BEDROCK"] = "test-key"
const preAWSProfile = process.env["AWS_BEARER_TOKEN_BEDROCK"]
await AwsBedrockHandler["withTempEnv"](
() => {
delete process.env["AWS_BEARER_TOKEN_BEDROCK"]
},
async () => {
should.not.exist(process.env["AWS_BEARER_TOKEN_BEDROCK"])
return "test"
},
)
process.env["AWS_BEARER_TOKEN_BEDROCK"]!.should.equal(preAWSProfile)
})
})
const mockOptions: ApiHandlerOptions = {
@@ -192,6 +210,7 @@ describe("AwsBedrockHandler", () => {
awsSessionToken: "",
awsUseProfile: false,
awsProfile: "",
awsBedrockApiKey: "",
awsBedrockUsePromptCache: false,
awsUseCrossRegionInference: false,
awsBedrockEndpoint: "",
+24 -8
View File
@@ -22,6 +22,8 @@ interface AwsBedrockHandlerOptions {
awsSecretKey?: string
awsSessionToken?: string
awsRegion?: string
awsAuthentication?: string
awsBedrockApiKey?: string
awsUseCrossRegionInference?: boolean
awsBedrockUsePromptCache?: boolean
awsUseProfile?: boolean
@@ -186,7 +188,10 @@ export class AwsBedrockHandler implements ApiHandler {
}> {
// Configure provider options
const providerOptions: ProviderChainOptions = {}
if (this.options.awsUseProfile) {
const useProfile =
(this.options.awsAuthentication === undefined && this.options.awsUseProfile) ||
this.options.awsAuthentication === "profile"
if (useProfile) {
// For profile-based auth, always use ignoreCache to detect credential file changes
// This solves the AWS Identity Manager issue where credential files change externally
providerOptions.ignoreCache = true
@@ -200,7 +205,7 @@ export class AwsBedrockHandler implements ApiHandler {
return await AwsBedrockHandler.withTempEnv(
() => {
AwsBedrockHandler.setEnv("AWS_REGION", this.options.awsRegion)
if (this.options.awsUseProfile) {
if (useProfile) {
AwsBedrockHandler.setEnv("AWS_PROFILE", this.options.awsProfile)
} else {
delete process.env["AWS_PROFILE"]
@@ -224,15 +229,26 @@ export class AwsBedrockHandler implements ApiHandler {
* Creates a BedrockRuntimeClient with the appropriate credentials
*/
private async getBedrockClient(): Promise<BedrockRuntimeClient> {
const credentials = await this.getAwsCredentials()
let auth: any
if (this.options.awsAuthentication === "apikey") {
auth = {
token: { token: this.options.awsBedrockApiKey },
authSchemePreference: ["httpBearerAuth"],
}
} else {
const credentials = await this.getAwsCredentials()
auth = {
credentials: {
accessKeyId: credentials.accessKeyId,
secretAccessKey: credentials.secretAccessKey,
sessionToken: credentials.sessionToken,
},
}
}
return new BedrockRuntimeClient({
region: this.getRegion(),
credentials: {
accessKeyId: credentials.accessKeyId,
secretAccessKey: credentials.secretAccessKey,
sessionToken: credentials.sessionToken,
},
...auth,
...(this.options.awsBedrockEndpoint && { endpoint: this.options.awsBedrockEndpoint }),
})
}
+19 -14
View File
@@ -9,6 +9,9 @@ import { OpenRouterErrorResponse } from "./types"
import { withRetry } from "../retry"
import { AuthService } from "@/services/auth/AuthService"
import OpenAI from "openai"
import { version as extensionVersion } from "../../../package.json"
import { shouldSkipReasoningForModel } from "@utils/model-utils"
import { CLINE_ACCOUNT_AUTH_ERROR_MESSAGE } from "@/shared/ClineAccount"
interface ClineHandlerOptions {
taskId?: string
@@ -40,7 +43,7 @@ export class ClineHandler implements ApiHandler {
private async ensureClient(): Promise<OpenAI> {
const clineAccountAuthToken = await this._authService.getAuthToken()
if (!clineAccountAuthToken) {
throw new Error("Cline account authentication token is required")
throw new Error(CLINE_ACCOUNT_AUTH_ERROR_MESSAGE)
}
if (!this.client) {
try {
@@ -51,6 +54,7 @@ export class ClineHandler implements ApiHandler {
"HTTP-Referer": "https://cline.bot",
"X-Title": "Cline",
"X-Task-ID": this.options.taskId || "",
"X-Cline-Version": extensionVersion,
},
})
} catch (error: any) {
@@ -65,12 +69,6 @@ export class ClineHandler implements ApiHandler {
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
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 client = await this.ensureClient()
this.lastGenerationId = undefined
@@ -125,7 +123,8 @@ export class ClineHandler 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
@@ -179,13 +178,19 @@ export class ClineHandler implements ApiHandler {
}
}
} catch (error) {
if (error.code === "ERR_BAD_REQUEST" || error.status === 401) {
throw new Error("Unauthorized: Please sign in to Cline before trying again.")
} 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))
const requestId = error?.request_id ? ` (Request ID: ${error.request_id})` : ""
if (error.code === "ERR_BAD_REQUEST" || error.status === 401) {
throw new Error(CLINE_ACCOUNT_AUTH_ERROR_MESSAGE + requestId)
} else if (error.code === "insufficient_credits" || error.status === 402) {
if (error.error) {
error.error.message = error.error.message + requestId
throw new Error(JSON.stringify(error.error))
}
}
const _error = error instanceof Error ? error : new Error(String(error))
_error.message = _error.message + requestId
throw _error
}
}
+90
View File
@@ -0,0 +1,90 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { withRetry } from "../retry"
import { ApiHandler } from "../index"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { ModelInfo, MoonshotModelId, moonshotModels, moonshotDefaultModelId } from "@/shared/api"
interface MoonshotHandlerOptions {
moonshotApiKey?: string
moonshotApiLine?: string
apiModelId?: string
}
export class MoonshotHandler implements ApiHandler {
private client: OpenAI | undefined
constructor(private readonly options: MoonshotHandlerOptions) {}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.moonshotApiKey) {
throw new Error("Moonshot API key is required")
}
try {
this.client = new OpenAI({
baseURL:
this.options.moonshotApiLine === "china" ? "https://api.moonshot.cn/v1" : "https://api.moonshot.ai/v1",
apiKey: this.options.moonshotApiKey,
})
} catch (error) {
throw new Error(`Error creating Moonshot client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
const stream = await client.chat.completions.create({
model: model.id,
messages: openAiMessages,
temperature: 0,
stream: true,
stream_options: { include_usage: true },
})
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
if (delta?.content) {
yield {
type: "text",
text: delta.content,
}
}
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
yield {
type: "reasoning",
reasoning: (delta.reasoning_content as string | undefined) || "",
}
}
if (chunk.usage) {
yield {
type: "usage",
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
}
}
}
}
getModel(): { id: MoonshotModelId; info: ModelInfo } {
const modelId = this.options.apiModelId
if (modelId && modelId in moonshotModels) {
const id = modelId as MoonshotModelId
return { id, info: moonshotModels[id] }
}
return { id: moonshotDefaultModelId, info: moonshotModels[moonshotDefaultModelId] }
}
}
+3 -1
View File
@@ -8,6 +8,7 @@ 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
@@ -112,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
+8 -4
View File
@@ -6,6 +6,7 @@ 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
@@ -70,10 +71,13 @@ export class XAIHandler implements ApiHandler {
}
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
yield {
type: "reasoning",
// @ts-ignore-next-line
reasoning: delta.reasoning_content,
// Skip reasoning content for Grok 4 models since it only displays "thinking" without providing useful information
if (!shouldSkipReasoningForModel(modelId)) {
yield {
type: "reasoning",
// @ts-ignore-next-line
reasoning: delta.reasoning_content,
}
}
}
+6
View File
@@ -139,6 +139,10 @@ export async function createOpenRouterStream(
shouldApplyMiddleOutTransform = true
}
// hardcoded provider sorting for kimi-k2
const isKimiK2 = model.id.startsWith("moonshotai/kimi-k2")
openRouterProviderSorting = isKimiK2 ? undefined : openRouterProviderSorting
// @ts-ignore-next-line
const stream = await client.chat.completions.create({
model: model.id,
@@ -153,6 +157,8 @@ export async function createOpenRouterStream(
...(model.id.startsWith("openai/o") ? { reasoning_effort: reasoningEffort || "medium" } : {}),
...(reasoning ? { reasoning } : {}),
...(openRouterProviderSorting ? { provider: { sort: openRouterProviderSorting } } : {}),
// limit providers to only those that support the 131k context window
...(isKimiK2 ? { provider: { order: ["groq", "together"], allow_fallbacks: false } } : {}),
})
return stream
@@ -6,9 +6,9 @@ import * as path from "path"
import { FileContextTracker } from "./FileContextTracker"
import * as diskModule from "@core/storage/disk"
import type { TaskMetadata, FileMetadataEntry } from "./ContextTrackerTypes"
import type { WebviewProviderCreator } from "@/hosts/host-providers"
import type { DiffViewProviderCreator, WebviewProviderCreator } from "@/hosts/host-providers"
import * as hostProviders from "@hosts/host-providers"
import { vscodeHostBridgeClient } from "@/hosts/vscode/client/host-grpc-client"
import { vscodeHostBridgeClient } from "@/hosts/vscode/hostbridge/client/host-grpc-client"
describe("FileContextTracker", () => {
let sandbox: sinon.SinonSandbox
@@ -53,7 +53,11 @@ describe("FileContextTracker", () => {
mockTaskMetadata = { files_in_context: [], model_usage: [] }
getTaskMetadataStub = sandbox.stub(diskModule, "getTaskMetadata").resolves(mockTaskMetadata)
saveTaskMetadataStub = sandbox.stub(diskModule, "saveTaskMetadata").resolves()
hostProviders.initializeHostProviders(((_) => {}) as WebviewProviderCreator, vscodeHostBridgeClient)
hostProviders.initializeHostProviders(
((_) => {}) as WebviewProviderCreator,
(() => {}) as DiffViewProviderCreator,
vscodeHostBridgeClient,
)
// Create tracker instance
taskId = "test-task-id"
+15 -4
View File
@@ -3,11 +3,12 @@ import { RuleFileRequest, RuleFile } from "@shared/proto/file"
import { FileMethodHandler } from "./index"
import { refreshClineRulesToggles } from "@core/context/instructions/user-instructions/cline-rules"
import { createRuleFile as createRuleFileImpl } from "@core/context/instructions/user-instructions/rule-helpers"
import * as vscode from "vscode"
import * as path from "path"
import { handleFileServiceRequest } from "./index"
import { refreshWorkflowToggles } from "@/core/context/instructions/user-instructions/workflows"
import { getCwd, getDesktopDir } from "@/utils/path"
import { getHostBridgeProvider } from "@/hosts/host-providers"
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
/**
* Creates a rule file in either global or workspace rules directory
@@ -42,7 +43,13 @@ export const createRuleFile: FileMethodHandler = async (controller: Controller,
const fileTypeName = request.type === "workflow" ? "workflow" : "rule"
if (fileExists) {
vscode.window.showWarningMessage(`${fileTypeName} file "${request.filename}" already exists.`)
const message = `${fileTypeName} file "${request.filename}" already exists.`
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.WARNING,
message,
}),
)
// Still open it for editing
await handleFileServiceRequest(controller, "openFile", { value: filePath })
} else {
@@ -55,8 +62,12 @@ export const createRuleFile: FileMethodHandler = async (controller: Controller,
await handleFileServiceRequest(controller, "openFile", { value: filePath })
vscode.window.showInformationMessage(
`Created new ${request.isGlobal ? "global" : "workspace"} ${fileTypeName} file: ${request.filename}`,
const message = `Created new ${request.isGlobal ? "global" : "workspace"} ${fileTypeName} file: ${request.filename}`
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message,
}),
)
}
+9 -2
View File
@@ -1,9 +1,10 @@
import { deleteRuleFile as deleteRuleFileImpl } from "@core/context/instructions/user-instructions/rule-helpers"
import { RuleFile, RuleFileRequest } from "@shared/proto/file"
import * as path from "path"
import * as vscode from "vscode"
import { Controller } from ".."
import { FileMethodHandler } from "./index"
import { getHostBridgeProvider } from "@/hosts/host-providers"
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
/**
* Deletes a rule file from either global or workspace rules directory
@@ -44,7 +45,13 @@ export const deleteRuleFile: FileMethodHandler = async (controller: Controller,
const fileTypeName = request.type === "workflow" ? "workflow" : "rule"
vscode.window.showInformationMessage(`${fileTypeName} file "${fileName}" deleted successfully`)
const message = `${fileTypeName} file "${fileName}" deleted successfully`
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message,
}),
)
return RuleFile.create({
filePath: request.rulePath,
+88 -29
View File
@@ -27,20 +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,
getWorkspaceState,
storeSecret,
updateGlobalState,
updateWorkspaceState,
} from "../storage/state"
import { getAllExtensionState, getGlobalState, getWorkspaceState, storeSecret, updateGlobalState } from "../storage/state"
import { Task } from "../task"
import { handleGrpcRequest, handleGrpcRequestCancel } from "./grpc-handler"
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
@@ -79,7 +74,7 @@ export class Controller {
)
this.accountService = ClineAccountService.getInstance()
this.authService = AuthService.getInstance(context)
this.authService.restoreAuthToken()
this.authService.restoreRefreshTokenAndRetrieveAuthInfo()
// Clean up legacy checkpoints
cleanupLegacyCheckpoints(this.context.globalStorageUri.fsPath, this.outputChannel).catch((error) => {
@@ -118,9 +113,19 @@ export class Controller {
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",
}),
)
}
}
@@ -456,11 +461,6 @@ export class Controller {
}
}
// Auth
public async validateAuthState(state: string | null): Promise<boolean> {
return state === this.authService.authNonce
}
async handleAuthCallback(customToken: string, provider: string | null = null) {
try {
await this.authService.handleAuthCallback(customToken, provider ? provider : "google")
@@ -484,7 +484,12 @@ export class Controller {
await this.postStateToWebview()
} 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
}
@@ -519,7 +524,12 @@ export class Controller {
console.error("Failed to fetch MCP marketplace:", error)
if (!silent) {
const errorMessage = error instanceof Error ? error.message : "Failed to fetch MCP marketplace"
vscode.window.showErrorMessage(errorMessage)
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: errorMessage,
}),
)
}
return undefined
}
@@ -603,7 +613,12 @@ export class Controller {
} catch (error) {
console.error("Failed to handle cached MCP marketplace:", error)
const errorMessage = error instanceof Error ? error.message : "Failed to handle cached MCP marketplace"
vscode.window.showErrorMessage(errorMessage)
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: errorMessage,
}),
)
}
}
@@ -814,7 +829,7 @@ export class Controller {
chatSettings: storedChatSettings,
userInfo,
mcpMarketplaceEnabled,
mcpRichDisplayEnabled,
mcpDisplayMode,
telemetrySetting,
planActSeparateModelsSetting,
enableCheckpointsSetting,
@@ -867,7 +882,7 @@ export class Controller {
chatSettings,
userInfo,
mcpMarketplaceEnabled,
mcpRichDisplayEnabled,
mcpDisplayMode,
telemetrySetting,
planActSeparateModelsSetting,
enableCheckpointsSetting: enableCheckpointsSetting ?? true,
@@ -967,14 +982,24 @@ export class Controller {
// Check if there's a workspace folder open
const cwd = await getCwd()
if (!cwd) {
vscode.window.showErrorMessage("No workspace folder open")
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: "No workspace folder open",
}),
)
return
}
// Get the git diff
const gitDiff = await getWorkingState(cwd)
if (gitDiff === "No changes in working directory") {
vscode.window.showInformationMessage("No changes in workspace for commit message")
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: "No changes in workspace for commit message",
}),
)
return
}
@@ -1041,25 +1066,59 @@ Commit message:`
if (api && api.repositories.length > 0) {
const repo = api.repositories[0]
repo.inputBox.value = commitMessage
vscode.window.showInformationMessage("Commit message generated and applied")
const message = "Commit message generated and applied"
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message,
}),
)
} else {
vscode.window.showErrorMessage("No Git repositories found")
const message = "No Git repositories found"
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message,
}),
)
}
} else {
vscode.window.showErrorMessage("Git extension not found")
const message = "Git extension not found"
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message,
}),
)
}
} else {
vscode.window.showErrorMessage("Failed to generate commit message")
const message = "Failed to generate commit message"
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message,
}),
)
}
} catch (innerError) {
const innerErrorMessage = innerError instanceof Error ? innerError.message : String(innerError)
vscode.window.showErrorMessage(`Failed to generate commit message: ${innerErrorMessage}`)
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Failed to generate commit message: ${innerErrorMessage}`,
}),
)
}
},
)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
vscode.window.showErrorMessage(`Failed to generate commit message: ${errorMessage}`)
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Failed to generate commit message: ${errorMessage}`,
}),
)
}
}
}
@@ -104,6 +104,12 @@ export async function refreshOpenRouterModels(
modelInfo.cacheWritesPrice = 0.75
modelInfo.cacheReadsPrice = 0
break
case "moonshotai/kimi-k2":
// forcing kimi-k2 to use the together provider for full context and best throughput
modelInfo.inputPrice = 1
modelInfo.outputPrice = 3
modelInfo.contextWindow = 131_000
break
default:
if (rawModel.id.startsWith("openai/")) {
modelInfo.cacheReadsPrice = parsePrice(rawModel.pricing?.input_cache_read)
+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,
}),
)
}
}
+3 -3
View File
@@ -50,9 +50,9 @@ export async function updateSettings(controller: Controller, request: UpdateSett
await controller.context.globalState.update("mcpResponsesCollapsed", request.mcpResponsesCollapsed)
}
// Update MCP responses collapsed setting
if (request.mcpRichDisplayEnabled !== undefined) {
await controller.context.globalState.update("mcpRichDisplayEnabled", request.mcpRichDisplayEnabled)
// Update MCP display mode setting
if (request.mcpDisplayMode !== undefined) {
await controller.context.globalState.update("mcpDisplayMode", request.mcpDisplayMode)
}
// Update chat settings
@@ -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()
+14 -2
View File
@@ -13,6 +13,8 @@ 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) {
@@ -76,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}`,
}),
)
}
}
@@ -93,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}`
}
}
+6 -1
View File
@@ -5,6 +5,7 @@ export type SecretKey =
| "awsAccessKey"
| "awsSecretKey"
| "awsSessionToken"
| "awsBedrockApiKey"
| "openAiApiKey"
| "geminiApiKey"
| "openAiNativeApiKey"
@@ -19,6 +20,7 @@ export type SecretKey =
| "authNonce"
| "asksageApiKey"
| "xaiApiKey"
| "moonshotApiKey"
| "nebiusApiKey"
| "sambanovaApiKey"
| "cerebrasApiKey"
@@ -31,6 +33,8 @@ export type GlobalStateKey =
| "awsBedrockUsePromptCache"
| "awsBedrockEndpoint"
| "awsProfile"
| "awsBedrockApiKey"
| "awsAuthentication"
| "awsUseProfile"
| "vertexProjectId"
| "vertexRegion"
@@ -58,6 +62,7 @@ export type GlobalStateKey =
| "fireworksModelMaxCompletionTokens"
| "fireworksModelMaxTokens"
| "qwenApiLine"
| "moonshotApiLine"
| "mcpMarketplaceCatalog"
| "telemetrySetting"
| "asksageApiUrl"
@@ -73,7 +78,7 @@ export type GlobalStateKey =
| "isNewUser"
| "welcomeViewCompleted"
| "terminalOutputLineLimit"
| "mcpRichDisplayEnabled"
| "mcpDisplayMode"
| "sapAiCoreTokenUrl"
| "sapAiCoreBaseUrl"
| "sapAiResourceGroup"
+26 -3
View File
@@ -11,6 +11,7 @@ import { StoredChatSettings } from "@shared/ChatSettings"
import { TelemetrySetting } from "@shared/TelemetrySetting"
import { UserInfo } from "@shared/UserInfo"
import { ClineRulesToggles } from "@shared/cline-rules"
import { DEFAULT_MCP_DISPLAY_MODE, McpDisplayMode } from "@shared/McpDisplayMode"
import { migrateEnableCheckpointsSetting, migrateMcpMarketplaceEnableSetting } from "./state-migrations"
/*
Storage
@@ -124,7 +125,9 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
awsBedrockUsePromptCache,
awsBedrockEndpoint,
awsProfile,
awsBedrockApiKey,
awsUseProfile,
awsAuthentication,
vertexProjectId,
vertexRegion,
openAiBaseUrl,
@@ -156,6 +159,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
fireworksModelMaxTokens,
userInfo,
qwenApiLine,
moonshotApiLine,
liteLlmApiKey,
telemetrySetting,
asksageApiKey,
@@ -163,6 +167,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
xaiApiKey,
sambanovaApiKey,
cerebrasApiKey,
moonshotApiKey,
nebiusApiKey,
planActSeparateModelsSettingRaw,
favoritedModelIds,
@@ -171,7 +176,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
shellIntegrationTimeout,
enableCheckpointsSettingRaw,
mcpMarketplaceEnabledRaw,
mcpRichDisplayEnabled,
mcpDisplayMode,
mcpResponsesCollapsedRaw,
globalWorkflowToggles,
terminalReuseEnabled,
@@ -197,7 +202,9 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
getGlobalState(context, "awsBedrockUsePromptCache") as Promise<boolean | undefined>,
getGlobalState(context, "awsBedrockEndpoint") as Promise<string | undefined>,
getGlobalState(context, "awsProfile") as Promise<string | undefined>,
getSecret(context, "awsBedrockApiKey") as Promise<string | undefined>,
getGlobalState(context, "awsUseProfile") as Promise<boolean | undefined>,
getGlobalState(context, "awsAuthentication") as Promise<string | undefined>,
getGlobalState(context, "vertexProjectId") as Promise<string | undefined>,
getGlobalState(context, "vertexRegion") as Promise<string | undefined>,
getGlobalState(context, "openAiBaseUrl") as Promise<string | undefined>,
@@ -229,6 +236,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
getGlobalState(context, "fireworksModelMaxTokens") as Promise<number | undefined>,
getGlobalState(context, "userInfo") as Promise<UserInfo | undefined>,
getGlobalState(context, "qwenApiLine") as Promise<string | undefined>,
getGlobalState(context, "moonshotApiLine") as Promise<string | undefined>,
getSecret(context, "liteLlmApiKey") as Promise<string | undefined>,
getGlobalState(context, "telemetrySetting") as Promise<TelemetrySetting | undefined>,
getSecret(context, "asksageApiKey") as Promise<string | undefined>,
@@ -236,6 +244,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
getSecret(context, "xaiApiKey") as Promise<string | undefined>,
getSecret(context, "sambanovaApiKey") as Promise<string | undefined>,
getSecret(context, "cerebrasApiKey") as Promise<string | undefined>,
getSecret(context, "moonshotApiKey") as Promise<string | undefined>,
getSecret(context, "nebiusApiKey") as Promise<string | undefined>,
getGlobalState(context, "planActSeparateModelsSetting") as Promise<boolean | undefined>,
getGlobalState(context, "favoritedModelIds") as Promise<string[] | undefined>,
@@ -244,7 +253,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
getGlobalState(context, "shellIntegrationTimeout") as Promise<number | undefined>,
getGlobalState(context, "enableCheckpointsSetting") as Promise<boolean | undefined>,
getGlobalState(context, "mcpMarketplaceEnabled") as Promise<boolean | undefined>,
getGlobalState(context, "mcpRichDisplayEnabled") as Promise<boolean | undefined>,
getGlobalState(context, "mcpDisplayMode") as Promise<McpDisplayMode | undefined>,
getGlobalState(context, "mcpResponsesCollapsed") as Promise<boolean | undefined>,
getGlobalState(context, "globalWorkflowToggles") as Promise<ClineRulesToggles | undefined>,
getGlobalState(context, "terminalReuseEnabled") as Promise<boolean | undefined>,
@@ -380,7 +389,9 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
awsBedrockUsePromptCache,
awsBedrockEndpoint,
awsProfile,
awsBedrockApiKey,
awsUseProfile,
awsAuthentication,
awsBedrockCustomSelected,
awsBedrockCustomModelBaseId,
vertexProjectId,
@@ -407,6 +418,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
togetherModelId,
qwenApiKey,
qwenApiLine,
moonshotApiLine,
doubaoApiKey,
mistralApiKey,
azureApiVersion,
@@ -430,6 +442,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
xaiApiKey,
sambanovaApiKey,
cerebrasApiKey,
moonshotApiKey,
nebiusApiKey,
favoritedModelIds,
requestTimeoutMs,
@@ -464,7 +477,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
previousModeAwsBedrockCustomModelBaseId,
previousModeSapAiCoreModelId,
mcpMarketplaceEnabled: mcpMarketplaceEnabled,
mcpRichDisplayEnabled: mcpRichDisplayEnabled ?? true,
mcpDisplayMode: mcpDisplayMode ?? DEFAULT_MCP_DISPLAY_MODE,
mcpResponsesCollapsed: mcpResponsesCollapsed,
telemetrySetting: telemetrySetting || "unset",
planActSeparateModelsSetting,
@@ -490,8 +503,10 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
awsUseCrossRegionInference,
awsBedrockUsePromptCache,
awsBedrockEndpoint,
awsBedrockApiKey,
awsProfile,
awsUseProfile,
awsAuthentication,
awsBedrockCustomSelected,
awsBedrockCustomModelBaseId,
vertexProjectId,
@@ -530,6 +545,7 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
liteLlmApiKey,
liteLlmUsePromptCache,
qwenApiLine,
moonshotApiLine,
asksageApiKey,
asksageApiUrl,
xaiApiKey,
@@ -538,6 +554,7 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
clineAccountId,
sambanovaApiKey,
cerebrasApiKey,
moonshotApiKey,
nebiusApiKey,
favoritedModelIds,
fireworksApiKey,
@@ -584,6 +601,7 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
awsBedrockEndpoint,
awsProfile,
awsUseProfile,
awsAuthentication,
vertexProjectId,
vertexRegion,
openAiBaseUrl,
@@ -598,6 +616,7 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
liteLlmBaseUrl,
liteLlmUsePromptCache,
qwenApiLine,
moonshotApiLine,
asksageApiUrl,
favoritedModelIds,
requestTimeoutMs: apiConfiguration.requestTimeoutMs,
@@ -617,6 +636,7 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
awsAccessKey,
awsSecretKey,
awsSessionToken,
awsBedrockApiKey,
openAiApiKey,
geminiApiKey,
openAiNativeApiKey,
@@ -632,6 +652,7 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
xaiApiKey,
sambanovaApiKey,
cerebrasApiKey,
moonshotApiKey,
nebiusApiKey,
sapAiCoreClientId,
sapAiCoreClientSecret,
@@ -658,6 +679,7 @@ export async function resetGlobalState(context: vscode.ExtensionContext) {
"awsAccessKey",
"awsSecretKey",
"awsSessionToken",
"awsBedrockApiKey",
"openAiApiKey",
"geminiApiKey",
"openAiNativeApiKey",
@@ -674,6 +696,7 @@ export async function resetGlobalState(context: vscode.ExtensionContext) {
"xaiApiKey",
"sambanovaApiKey",
"cerebrasApiKey",
"moonshotApiKey",
"nebiusApiKey",
]
for (const key of secretKeys) {
+17 -4
View File
@@ -81,7 +81,8 @@ import { refreshWorkflowToggles } from "../context/instructions/user-instruction
import { MessageStateHandler } from "./message-state"
import { TaskState } from "./TaskState"
import { ToolExecutor } from "./ToolExecutor"
import { formatErrorWithStatusCode, updateApiReqMsg } from "./utils"
import { extractErrorDetails, formatErrorWithStatusCode, updateApiReqMsg } from "./utils"
import { createDiffViewProvider, getHostBridgeProvider } from "@/hosts/host-providers"
export const USE_EXPERIMENTAL_CLAUDE4_FEATURES = false
@@ -174,7 +175,7 @@ export class Task {
this.urlContentFetcher = new UrlContentFetcher(context)
this.browserSession = new BrowserSession(context, browserSettings)
this.contextManager = new ContextManager()
this.diffViewProvider = new DiffViewProvider()
this.diffViewProvider = createDiffViewProvider()
this.autoApprovalSettings = autoApprovalSettings
this.browserSettings = browserSettings
this.chatSettings = chatSettings
@@ -1574,8 +1575,9 @@ export class Task {
await this.migrateDisableBrowserToolSetting()
const disableBrowserTool = this.browserSettings.disableToolUse ?? false
const modelInfo = this.api.getModel()
// cline browser tool uses image recognition for navigation (requires model image support).
const modelSupportsBrowserUse = this.api.getModel().info.supportsImages ?? false
const modelSupportsBrowserUse = modelInfo.info.supportsImages ?? false
const supportsBrowserUse = modelSupportsBrowserUse && !disableBrowserTool // only enable browser use if the model supports it and the user hasn't disabled it
@@ -1660,6 +1662,17 @@ export class Task {
const isOpenRouterContextWindowError = checkIsOpenRouterContextWindowError(error) && isOpenRouter
const isAnthropicContextWindowError = checkIsAnthropicContextWindowError(error) && isAnthropic
const { statusCode, message, requestId } = extractErrorDetails(error)
// Capture provider failure telemetry
telemetryService.captureProviderApiError({
taskId: this.taskId,
model: modelInfo.id,
errorMessage: message,
errorStatus: statusCode,
requestId,
})
if (isAnthropic && isAnthropicContextWindowError && !this.taskState.didAutomaticallyRetryFailedApiRequest) {
this.taskState.conversationHistoryDeletedRange = this.contextManager.getNextTruncationRange(
this.messageStateHandler.getApiConversationHistory(),
@@ -1723,7 +1736,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),
})
+9 -2
View File
@@ -6,13 +6,20 @@ import { calculateApiCostAnthropic } from "@/utils/cost"
import { ApiHandler } from "@/api"
export function formatErrorWithStatusCode(error: any): string {
const statusCode = error.status || error.statusCode || (error.response && error.response.status)
const message = error.message ?? JSON.stringify(serializeError(error), null, 2)
const { statusCode, message } = extractErrorDetails(error)
// Only prepend the statusCode if it's not already part of the message
return statusCode && !message.includes(statusCode.toString()) ? `${statusCode} - ${message}` : message
}
export function extractErrorDetails(error: any): { message: string; statusCode?: number; requestId?: string } {
const statusCode = error.status || error.statusCode || (error.response && error.response?.status)
const message = error.message ?? JSON.stringify(serializeError(error), null, 2)
const requestId = error.request_id || error.response?.request_id || undefined
return { message, statusCode, requestId }
}
export const showNotificationForApprovalIfAutoApprovalEnabled = (
message: string,
autoApprovalSettingsEnabled: boolean,
+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,
}),
)
},
)
}),
+47 -31
View File
@@ -32,12 +32,14 @@ import {
import { sendFocusChatInputEvent } from "./core/controller/ui/subscribeToFocusChatInput"
import { FileContextTracker } from "./core/context/context-tracking/FileContextTracker"
import * as hostProviders from "@hosts/host-providers"
import { vscodeHostBridgeClient } from "@/hosts/vscode/client/host-grpc-client"
import { VscodeWebviewProvider } from "./core/webview/VscodeWebviewProvider"
import { vscodeHostBridgeClient } from "@/hosts/vscode/hostbridge/client/host-grpc-client"
import { VscodeWebviewProvider } from "./hosts/vscode/VscodeWebviewProvider"
import { ExtensionContext } from "vscode"
import { AuthService } from "./services/auth/AuthService"
import { writeTextToClipboard, readTextFromClipboard } from "@/utils/env"
import { VscodeDiffViewProvider } from "./hosts/vscode/VscodeDiffViewProvider"
import { getHostBridgeProvider } from "@hosts/host-providers"
import { ShowMessageRequest, ShowMessageType } from "./shared/proto/host/window"
/*
Built using https://github.com/microsoft/vscode-webview-ui-toolkit
@@ -104,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)
}
@@ -294,32 +301,12 @@ export async function activate(context: vscode.ExtensionContext) {
break
}
case "/auth": {
const authService = AuthService.getInstance()
console.log("Auth callback received:", uri.toString())
const token = query.get("idToken")
const state = query.get("state")
const provider = query.get("provider")
console.log("Auth callback received:", {
token: token,
state: state,
provider: provider,
})
// 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 vscode.window.showWarningMessage(
`Store token returned from ${uri.path}`,
"Store",
"Cancel",
)
if (userConfirmation === "Cancel") {
console.log("User declined to continue with auth callback due to state mismatch")
return
}
}
console.log("Auth callback received:", { provider })
if (token) {
await visibleWebview?.controller.handleAuthCallback(token, provider)
@@ -426,7 +413,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",
}),
)
}
}),
)
@@ -562,7 +554,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
@@ -584,7 +581,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
@@ -649,8 +651,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)
@@ -685,6 +690,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)
}
@@ -694,7 +707,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
View File
@@ -3,6 +3,7 @@ import {
WorkspaceServiceClientInterface,
EnvServiceClientInterface,
WindowServiceClientInterface,
DiffServiceClientInterface,
} from "@generated/hosts/host-bridge-client-types"
/**
@@ -13,6 +14,7 @@ export interface HostBridgeClientProvider {
workspaceClient: WorkspaceServiceClientInterface
envClient: EnvServiceClientInterface
windowClient: WindowServiceClientInterface
diffClient: DiffServiceClientInterface
}
/**
+13
View File
@@ -1,22 +1,28 @@
import { WebviewProvider } from "@core/webview"
import { HostBridgeClientProvider } from "./host-provider-types"
import { WebviewProviderType } from "@/shared/webview/types"
import { DiffViewProvider } from "@/integrations/editor/DiffViewProvider"
/**
* A function that creates WebviewProvider instances
*/
export type WebviewProviderCreator = (providerType: WebviewProviderType) => WebviewProvider
export type DiffViewProviderCreator = () => DiffViewProvider
let _webviewProviderCreator: WebviewProviderCreator | undefined
let _diffViewProviderCreator: DiffViewProviderCreator | undefined
let _hostBridgeProvider: HostBridgeClientProvider | undefined
export var isSetup: boolean = false
export function initializeHostProviders(
webviewProviderCreator: WebviewProviderCreator,
diffViewProviderCreator: DiffViewProviderCreator,
hostBridgeProvider: HostBridgeClientProvider,
) {
_webviewProviderCreator = webviewProviderCreator
_diffViewProviderCreator = diffViewProviderCreator
_hostBridgeProvider = hostBridgeProvider
isSetup = true
}
@@ -28,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")
+100
View File
@@ -0,0 +1,100 @@
import { arePathsEqual } from "@/utils/path"
import * as path from "path"
import * as vscode from "vscode"
import { DecorationController } from "@integrations/editor/DecorationController"
import { DIFF_VIEW_URI_SCHEME, DiffViewProvider } from "@integrations/editor/DiffViewProvider"
export class VscodeDiffViewProvider extends DiffViewProvider {
override async openDiffEditor(): Promise<void> {
if (!this.absolutePath) {
throw new Error("No file path set")
}
// get diagnostics before editing the file, we'll compare to diagnostics after editing to see if cline needs to fix anything
this.preDiagnostics = vscode.languages.getDiagnostics()
// if the file was already open, close it (must happen after showing the diff view since if it's the only tab the column will close)
this.documentWasOpen = false
// close the tab if it's open (it's already been saved)
const tabs = vscode.window.tabGroups.all
.map((tg) => tg.tabs)
.flat()
.filter((tab) => tab.input instanceof vscode.TabInputText && arePathsEqual(tab.input.uri.fsPath, this.absolutePath))
for (const tab of tabs) {
if (!tab.isDirty) {
await vscode.window.tabGroups.close(tab)
}
this.documentWasOpen = true
}
const uri = vscode.Uri.file(this.absolutePath)
// If this diff editor is already open (ie if a previous write file was interrupted) then we should activate that instead of opening a new diff
const diffTab = vscode.window.tabGroups.all
.flatMap((group) => group.tabs)
.find(
(tab) =>
tab.input instanceof vscode.TabInputTextDiff &&
tab.input?.original?.scheme === DIFF_VIEW_URI_SCHEME &&
arePathsEqual(tab.input.modified.fsPath, uri.fsPath),
)
if (diffTab && diffTab.input instanceof vscode.TabInputTextDiff) {
// Use already open diff editor.
this.activeDiffEditor = await vscode.window.showTextDocument(diffTab.input.modified, {
preserveFocus: true,
})
} else {
// Open new diff editor.
this.activeDiffEditor = await new Promise<vscode.TextEditor>((resolve, reject) => {
const fileName = path.basename(uri.fsPath)
const fileExists = this.editType === "modify"
const disposable = vscode.window.onDidChangeActiveTextEditor((editor) => {
if (editor && arePathsEqual(editor.document.uri.fsPath, uri.fsPath)) {
disposable.dispose()
resolve(editor)
}
})
vscode.commands.executeCommand(
"vscode.diff",
vscode.Uri.parse(`${DIFF_VIEW_URI_SCHEME}:${fileName}`).with({
query: Buffer.from(this.originalContent ?? "").toString("base64"),
}),
uri,
`${fileName}: ${fileExists ? "Original ↔ Cline's Changes" : "New File"} (Editable)`,
{
preserveFocus: true,
},
)
// This may happen on very slow machines ie project idx
setTimeout(() => {
disposable.dispose()
reject(new Error("Failed to open diff editor, please try again..."))
}, 10_000)
})
}
this.fadedOverlayController = new DecorationController("fadedOverlay", this.activeDiffEditor)
this.activeLineController = new DecorationController("activeLine", this.activeDiffEditor)
// Apply faded overlay to all lines initially
this.fadedOverlayController.addLines(0, this.activeDiffEditor.document.lineCount)
}
override async replaceText(
content: string,
rangeToReplace: { startLine: number; endLine: number },
currentLine: number,
): Promise<void> {
const document = this.activeDiffEditor?.document
if (!document) {
throw new Error("User closed text editor, unable to edit file...")
}
const edit = new vscode.WorkspaceEdit()
const range = new vscode.Range(rangeToReplace.startLine, 0, rangeToReplace.endLine, 0)
edit.replace(document.uri, range, content)
await vscode.workspace.applyEdit(edit)
// Update decorations for the entire changed section
this.activeLineController?.setActiveLine(currentLine)
this.fadedOverlayController?.updateOverlayAfterLine(currentLine, document.lineCount)
}
}
@@ -4,8 +4,8 @@ import { sendThemeEvent } from "@core/controller/ui/subscribeToTheme"
import { getTheme } from "@integrations/theme/getTheme"
import * as vscode from "vscode"
import { Uri } from "vscode"
import { WebviewProvider } from "."
import { sendDidBecomeVisibleEvent } from "../controller/ui/subscribeToDidBecomeVisible"
import { WebviewProvider } from "@core/webview"
import { sendDidBecomeVisibleEvent } from "@core/controller/ui/subscribeToDidBecomeVisible"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -1,5 +1,5 @@
import { StreamingCallbacks } from "@/hosts/host-provider-types"
import { HostServiceHandlerConfig, hostServiceHandlers } from "./host-grpc-service-config"
import { HostServiceHandlerConfig, hostServiceHandlers } from "@generated/hosts/vscode/hostbridge-grpc-service-config"
import { GrpcRequestRegistry } from "@core/controller/grpc-request-registry"
/**
@@ -1,4 +1,4 @@
import { StreamingResponseHandler } from "./host-grpc-handler"
import { StreamingResponseHandler } from "./hostbridge-grpc-handler"
/**
* Generic type for service method handlers
@@ -1,5 +1,5 @@
import { v4 as uuidv4 } from "uuid"
import { GrpcHandler } from "../host-grpc-handler"
import { GrpcHandler } from "@/hosts/vscode/hostbridge-grpc-handler"
import { StreamingCallbacks } from "@/hosts/host-provider-types"
// Generic type for any protobuf service definition
@@ -1,4 +1,4 @@
import { createGrpcClient } from "@hosts/vscode/client/host-grpc-client-base"
import { createGrpcClient } from "@hosts/vscode/hostbridge/client/host-grpc-client-base"
import { HostBridgeClientProvider } from "@/hosts/host-provider-types"
import * as host from "@shared/proto/index.host"
@@ -7,4 +7,5 @@ export const vscodeHostBridgeClient: HostBridgeClientProvider = {
workspaceClient: createGrpcClient(host.WorkspaceServiceDefinition),
envClient: createGrpcClient(host.EnvServiceDefinition),
windowClient: createGrpcClient(host.WindowServiceDefinition),
diffClient: createGrpcClient(host.DiffServiceDefinition),
}
@@ -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.")
}
@@ -0,0 +1,5 @@
import { ReplaceTextRequest, ReplaceTextResponse } from "@/shared/proto/index.host"
export async function replaceText(_request: ReplaceTextRequest): Promise<ReplaceTextResponse> {
throw new Error("diffService.replaceText is not supported. Use the VscodeDiffViewProvider.")
}
@@ -1,7 +1,7 @@
import * as fs from "fs/promises"
import * as fsSync from "fs"
import { SubscribeToFileRequest, FileChangeEvent, FileChangeEvent_ChangeType } from "@shared/proto/host/watch"
import { StreamingResponseHandler, getRequestRegistry } from "../host-grpc-handler"
import { SubscribeToFileRequest, FileChangeEvent_ChangeType } from "@shared/proto/host/watch"
import { StreamingResponseHandler, getRequestRegistry } from "@/hosts/vscode/hostbridge-grpc-handler"
// Debounce configuration
const DEBOUNCE_DELAY = 100 // ms
@@ -0,0 +1,26 @@
import { window } from "vscode"
import { SelectedResponse, ShowMessageRequest, ShowMessageType } from "@/shared/proto/index.host"
const DEFAULT_OPTIONS = { modal: false, items: [] } as const
export async function showMessage(request: ShowMessageRequest): Promise<SelectedResponse | undefined> {
const { message, type, options } = request
const { modal, detail, items } = { ...DEFAULT_OPTIONS, ...options }
const option = { modal, detail }
let selectedOption: string | undefined = undefined
switch (type) {
case ShowMessageType.ERROR:
selectedOption = await window.showErrorMessage(message, option, ...items)
break
case ShowMessageType.WARNING:
selectedOption = await window.showWarningMessage(message, option, ...items)
break
default:
selectedOption = await window.showInformationMessage(message, option, ...items)
break
}
return SelectedResponse.create({ selectedOption })
}
+48 -102
View File
@@ -10,29 +10,30 @@ import { diagnosticsToProblemsString, getNewDiagnostics } from "../diagnostics"
import { detectEncoding } from "../misc/extract-text"
import * as iconv from "iconv-lite"
import { getHostBridgeProvider } from "@/hosts/host-providers"
import { ShowTextDocumentRequest, ShowTextDocumentOptions, TextEditorInfo } from "@/shared/proto/host/window"
import { ShowTextDocumentRequest, ShowTextDocumentOptions } from "@/shared/proto/host/window"
export const DIFF_VIEW_URI_SCHEME = "cline-diff"
export class DiffViewProvider {
export abstract class DiffViewProvider {
editType?: "create" | "modify"
isEditing = false
originalContent: string | undefined
private createdDirs: string[] = []
private documentWasOpen = false
private relPath?: string
private absolutePath?: string
private newContent?: string
private activeDiffEditor?: vscode.TextEditor
private fadedOverlayController?: DecorationController
private activeLineController?: DecorationController
protected documentWasOpen = false
protected relPath?: string
protected absolutePath?: string
protected fileEncoding: string = "utf8"
private streamedLines: string[] = []
private preDiagnostics: [vscode.Uri, vscode.Diagnostic[]][] = []
private fileEncoding: string = "utf8"
private newContent?: string
protected activeDiffEditor?: vscode.TextEditor
protected fadedOverlayController?: DecorationController
protected activeLineController?: DecorationController
protected preDiagnostics: [vscode.Uri, vscode.Diagnostic[]][] = []
constructor() {}
async open(relPath: string): Promise<void> {
public async open(relPath: string): Promise<void> {
this.isEditing = true
this.relPath = relPath
this.absolutePath = path.resolve(await getCwd(), relPath)
@@ -46,12 +47,7 @@ export class DiffViewProvider {
if (existingDocument && existingDocument.isDirty) {
await existingDocument.save()
}
}
// get diagnostics before editing the file, we'll compare to diagnostics after editing to see if cline needs to fix anything
this.preDiagnostics = vscode.languages.getDiagnostics()
if (fileExists) {
const fileBuffer = await fs.readFile(this.absolutePath)
this.fileEncoding = await detectEncoding(fileBuffer)
this.originalContent = iconv.decode(fileBuffer, this.fileEncoding)
@@ -65,28 +61,25 @@ export class DiffViewProvider {
if (!fileExists) {
await fs.writeFile(this.absolutePath, "")
}
// if the file was already open, close it (must happen after showing the diff view since if it's the only tab the column will close)
this.documentWasOpen = false
// close the tab if it's open (it's already saved above)
const tabs = vscode.window.tabGroups.all
.map((tg) => tg.tabs)
.flat()
.filter((tab) => tab.input instanceof vscode.TabInputText && arePathsEqual(tab.input.uri.fsPath, this.absolutePath))
for (const tab of tabs) {
if (!tab.isDirty) {
await vscode.window.tabGroups.close(tab)
}
this.documentWasOpen = true
}
this.activeDiffEditor = await this.openDiffEditor()
this.fadedOverlayController = new DecorationController("fadedOverlay", this.activeDiffEditor)
this.activeLineController = new DecorationController("activeLine", this.activeDiffEditor)
// Apply faded overlay to all lines initially
this.fadedOverlayController.addLines(0, this.activeDiffEditor.document.lineCount)
await this.openDiffEditor()
this.scrollEditorToLine(0) // will this crash for new files?
this.streamedLines = []
}
/**
* Opens a diff editor or viewer for the current file.
*
* This abstract method must be implemented by subclasses to create and display
* a diff editor or viewer that shows the difference between the original and
* modified content.
*
* Called automatically by the `open` method after ensuring the file exists and
* creating any necessary directories.
*
* @returns A promise that resolves when the diff editor is open and ready
*/
protected abstract openDiffEditor(): Promise<void>
async update(
accumulatedContent: string,
isFinal: boolean,
@@ -128,15 +121,9 @@ export class DiffViewProvider {
// Replace all content up to the current line with accumulated lines
// This is necessary (as compared to inserting one line at a time) to handle cases where html tags on previous lines are auto closed for example
const edit = new vscode.WorkspaceEdit()
const rangeToReplace = new vscode.Range(0, 0, currentLine + 1, 0)
const contentToReplace = accumulatedLines.slice(0, currentLine + 1).join("\n") + "\n"
edit.replace(document.uri, rangeToReplace, contentToReplace)
await vscode.workspace.applyEdit(edit)
// Update decorations for the entire changed section
this.activeLineController.setActiveLine(currentLine)
this.fadedOverlayController.updateOverlayAfterLine(currentLine, document.lineCount)
const rangeToReplace = { startLine: 0, endLine: currentLine + 1 }
await this.replaceText(contentToReplace, rangeToReplace, currentLine)
// Scroll to the actual change location if provided.
if (changeLocation) {
@@ -193,6 +180,24 @@ export class DiffViewProvider {
}
}
/**
* Replaces text in the diff editor with the specified content.
*
* This abstract method must be implemented by subclasses to handle the actual
* text replacement in their specific diff editor implementation. It's called
* during the streaming update process to progressively show changes.
*
* @param content The new content to insert into the document
* @param rangeToReplace An object specifying the line range to replace
* @param currentLine The current line number being edited, used for scroll positioning
* @returns A promise that resolves when the text replacement is complete
*/
abstract replaceText(
content: string,
rangeToReplace: { startLine: number; endLine: number },
currentLine: number,
): Promise<void>
async saveChanges(): Promise<{
newProblemsMessage: string | undefined
userEdits: string | undefined
@@ -349,65 +354,6 @@ export class DiffViewProvider {
}
}
private async openDiffEditor(): Promise<vscode.TextEditor> {
if (!this.absolutePath) {
throw new Error("No file path set")
}
const uri = vscode.Uri.file(this.absolutePath)
// If this diff editor is already open (ie if a previous write file was interrupted) then we should activate that instead of opening a new diff
const diffTab = vscode.window.tabGroups.all
.flatMap((group) => group.tabs)
.find(
(tab) =>
tab.input instanceof vscode.TabInputTextDiff &&
tab.input?.original?.scheme === DIFF_VIEW_URI_SCHEME &&
arePathsEqual(tab.input.modified.fsPath, uri.fsPath),
)
if (diffTab && diffTab.input instanceof vscode.TabInputTextDiff) {
const editorInfo = await getHostBridgeProvider().windowClient.showTextDocument(
ShowTextDocumentRequest.create({
path: diffTab.input.modified.fsPath,
options: ShowTextDocumentOptions.create({
preserveFocus: true,
}),
}),
)
// Find the editor that matches the returned path
const editor = vscode.window.visibleTextEditors.find((e) => e.document.uri.fsPath === editorInfo.documentPath)
if (!editor) {
throw new Error("Failed to find opened text editor")
}
return editor
}
// Open new diff editor
return new Promise<vscode.TextEditor>((resolve, reject) => {
const fileName = path.basename(uri.fsPath)
const fileExists = this.editType === "modify"
const disposable = vscode.window.onDidChangeActiveTextEditor((editor) => {
if (editor && arePathsEqual(editor.document.uri.fsPath, uri.fsPath)) {
disposable.dispose()
resolve(editor)
}
})
vscode.commands.executeCommand(
"vscode.diff",
vscode.Uri.parse(`${DIFF_VIEW_URI_SCHEME}:${fileName}`).with({
query: Buffer.from(this.originalContent ?? "").toString("base64"),
}),
uri,
`${fileName}: ${fileExists ? "Original ↔ Cline's Changes" : "New File"} (Editable)`,
{
preserveFocus: true,
},
)
// This may happen on very slow machines ie project idx
setTimeout(() => {
disposable.dispose()
reject(new Error("Failed to open diff editor, please try again..."))
}, 10_000)
})
}
private scrollEditorToLine(line: number) {
if (this.activeDiffEditor) {
const scrollLine = line + 4
@@ -1,9 +1,7 @@
import * as vscode from "vscode"
import { getWorkingState } from "@utils/git"
import { writeTextToClipboard } from "@utils/env"
import { getHostBridgeProvider } from "@/hosts/host-providers"
import { ShowTextDocumentRequest } from "@/shared/proto/host/window"
import { ShowMessageType, ShowTextDocumentRequest, ShowMessageRequest } from "@/shared/proto/host/window"
/**
* Formats the git diff into a prompt for the AI
* @param gitDiff The git diff to format
@@ -61,7 +59,12 @@ export function extractCommitMessage(aiResponse: string): string {
*/
export async function copyCommitMessageToClipboard(message: string): Promise<void> {
await writeTextToClipboard(message)
vscode.window.showInformationMessage("Commit message copied to clipboard")
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: "Commit message copied to clipboard",
}),
)
}
/**
@@ -73,13 +76,19 @@ export async function showCommitMessageOptions(message: string): Promise<void> {
const applyAction = "Apply to Git Input"
const editAction = "Edit Message"
const selectedAction = await vscode.window.showInformationMessage(
"Commit message generated",
{ modal: false, detail: message },
copyAction,
applyAction,
editAction,
)
const selectedAction = (
await getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: "Commit message generated",
options: {
modal: false,
detail: message,
items: [copyAction, applyAction, editAction],
},
}),
)
)?.selectedOption
// Handle user dismissing the dialog (selectedAction is undefined)
if (!selectedAction) {
@@ -111,13 +120,28 @@ async function applyCommitMessageToGitInput(message: string): Promise<void> {
if (api && api.repositories.length > 0) {
const repo = api.repositories[0]
repo.inputBox.value = message
vscode.window.showInformationMessage("Commit message applied to Git input")
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: "Commit message applied to Git input",
}),
)
} else {
vscode.window.showErrorMessage("No Git repositories found")
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: "No Git repositories found",
}),
)
await copyCommitMessageToClipboard(message)
}
} else {
vscode.window.showErrorMessage("Git extension not found")
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: "Git extension not found",
}),
)
await copyCommitMessageToClipboard(message)
}
}
@@ -137,5 +161,10 @@ async function editCommitMessage(message: string): Promise<void> {
path: document.uri.fsPath,
}),
)
vscode.window.showInformationMessage("Edit the commit message and copy when ready")
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: "Edit the commit message and copy when ready",
}),
)
}
+6 -3
View File
@@ -3,7 +3,7 @@ import os from "os"
import * as path from "path"
import * as vscode from "vscode"
import { getHostBridgeProvider } from "@/hosts/host-providers"
import { ShowTextDocumentRequest, ShowTextDocumentOptions } from "@/shared/proto/host/window"
import { ShowTextDocumentRequest, ShowTextDocumentOptions, ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
import { writeFile } from "@utils/fs"
export async function downloadTask(dateTs: number, conversationHistory: Anthropic.MessageParam[]) {
@@ -48,8 +48,11 @@ export async function downloadTask(dateTs: number, conversationHistory: Anthropi
}),
)
} catch (error) {
vscode.window.showErrorMessage(
`Failed to save markdown file: ${error instanceof Error ? error.message : String(error)}`,
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Failed to save markdown file: ${error instanceof Error ? error.message : String(error)}`,
}),
)
}
}
+19 -4
View File
@@ -3,13 +3,18 @@ import * as os from "os"
import * as vscode from "vscode"
import { arePathsEqual } from "@utils/path"
import { getHostBridgeProvider } from "@/hosts/host-providers"
import { ShowTextDocumentRequest, ShowTextDocumentOptions } from "@/shared/proto/host/window"
import { ShowTextDocumentRequest, ShowTextDocumentOptions, ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
import { writeFile } from "@utils/fs"
export async function openImage(dataUri: string) {
const matches = dataUri.match(/^data:image\/([a-zA-Z]+);base64,(.+)$/)
if (!matches) {
vscode.window.showErrorMessage("Invalid data URI format")
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: "Invalid data URI format",
}),
)
return
}
const [, format, base64Data] = matches
@@ -19,7 +24,12 @@ export async function openImage(dataUri: string) {
await writeFile(tempFilePath, new Uint8Array(imageBuffer))
await vscode.commands.executeCommand("vscode.open", vscode.Uri.file(tempFilePath))
} catch (error) {
vscode.window.showErrorMessage(`Error opening image: ${error}`)
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Error opening image: ${error}`,
}),
)
}
}
@@ -52,6 +62,11 @@ export async function openFile(absolutePath: string) {
}),
)
} catch (error) {
vscode.window.showErrorMessage(`Could not open file!`)
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Could not open file!`,
}),
)
}
}
+24 -6
View File
@@ -3,7 +3,7 @@ import fs from "fs/promises"
import * as path from "path"
import sizeOf from "image-size"
import { getHostBridgeProvider } from "@/hosts/host-providers"
import { ShowOpenDialogueRequest } from "@/shared/proto/host/window"
import { ShowMessageRequest, ShowMessageType, ShowOpenDialogueRequest } from "@/shared/proto/host/window"
/**
* Supports processing of images and other file types
@@ -46,14 +46,22 @@ export async function selectFiles(imagesAllowed: boolean): Promise<{ images: str
const dimensions = sizeOf(uint8Array) // Get dimensions from Uint8Array
if (dimensions.width! > 7500 || dimensions.height! > 7500) {
console.warn(`Image dimensions exceed 7500px, skipping: ${filePath}`)
vscode.window.showErrorMessage(
`Image too large: ${path.basename(filePath)} was skipped (dimensions exceed 7500px).`,
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Image too large: ${path.basename(filePath)} was skipped (dimensions exceed 7500px).`,
}),
)
return null
}
} catch (error) {
console.error(`Error reading file or getting dimensions for ${filePath}:`, error)
vscode.window.showErrorMessage(`Could not read dimensions for ${path.basename(filePath)}, skipping.`)
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Could not read dimensions for ${path.basename(filePath)}, skipping.`,
}),
)
return null
}
@@ -68,12 +76,22 @@ export async function selectFiles(imagesAllowed: boolean): Promise<{ images: str
const stats = await fs.stat(filePath)
if (stats.size > 20 * 1000 * 1024) {
console.warn(`File too large, skipping: ${filePath}`)
vscode.window.showErrorMessage(`File too large: ${path.basename(filePath)} was skipped (size exceeds 20MB).`)
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `File too large: ${path.basename(filePath)} was skipped (size exceeds 20MB).`,
}),
)
return null
}
} catch (error) {
console.error(`Error checking file size for ${filePath}:`, error)
vscode.window.showErrorMessage(`Could not check file size for ${path.basename(filePath)}, skipping.`)
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Could not check file size for ${path.basename(filePath)}, skipping.`,
}),
)
return null
}
return { type: "file", data: filePath }
+2 -38
View File
@@ -82,42 +82,6 @@ export class ClineAccountService {
}
}
/**
* Validates if the user has sufficient credits to make API requests.
* This checks the user's balance and throws an error if the balance is insufficient or if the request fails.
* @throws Error if the user has insufficient credits or if the request fails
* @returns {Promise<void>} A promise that resolves if the user has sufficient credits.
*/
async validateRequest(): Promise<void> {
try {
const { organizations, id } = await this.authenticatedRequest<UserResponse>(`/api/v1/users/me`)
const activeOrganization = organizations.find((org) => org.active)
console.log("SwitchAuthToken: Active Organization", activeOrganization?.name || "No active organization")
// Skip balance check for active organizations
if (activeOrganization) {
return
}
const balance = await this.authenticatedRequest<BalanceResponse>(`/api/v1/users/${id}/balance`)
const currentBalance = Number(balance?.balance) || 0
// Throw error if insufficient credits (balance <= 0)
if (currentBalance <= 0) {
throw new Error(
JSON.stringify({
code: "insufficient_credits",
current_balance: currentBalance,
message: "Not enough credits available",
}),
)
}
} catch (error) {
console.error("Invalid Cline API request:", error)
throw error instanceof Error ? error : new Error(`Invalid Request: ${error}`)
}
}
/**
* RPC variant that fetches the user's current credit balance without posting to webview
* @returns Balance data or undefined if failed
@@ -274,8 +238,8 @@ export class ClineAccountService {
console.error("Error switching account:", error)
throw error
} finally {
// Request a new authentication token
await this._authService.refreshAuth()
// After user switches account, we will force a refresh of the id token by calling this function that restores the refresh token and retrieves new auth info
await this._authService.restoreRefreshTokenAndRetrieveAuthInfo()
}
}
}
+56 -59
View File
@@ -1,7 +1,6 @@
import vscode from "vscode"
import crypto from "crypto"
import { EmptyRequest, String } from "../../shared/proto/common"
import { AuthState } from "../../shared/proto/account"
import { AuthState, UserInfo } from "../../shared/proto/account"
import { StreamingResponseHandler, getRequestRegistry } from "@/core/controller/grpc-handler"
import { FirebaseAuthProvider } from "./providers/FirebaseAuthProvider"
import { Controller } from "@/core/controller"
@@ -22,15 +21,35 @@ const availableAuthProviders = {
// Add other providers here as needed
}
export interface ClineAuthInfo {
idToken: string
userInfo: ClineAccountUserInfo
}
export interface ClineAccountUserInfo {
createdAt: string
displayName: string
email: string
id: string
organizations: ClineAccountOrganization[]
}
export interface ClineAccountOrganization {
active: boolean
memberId: string
name: string
organizationId: string
roles: string[]
}
// TODO: Add logic to handle multiple webviews getting auth updates.
export class AuthService {
private static instance: AuthService | null = null
private _config: ServiceConfig
private _authenticated: boolean = false
private _user: any = null
private _provider: any = null
private readonly _authNonce = crypto.randomBytes(32).toString("hex")
private _clineAuthInfo: ClineAuthInfo | null = null
private _provider: { provider: FirebaseAuthProvider } | null = null
private _activeAuthStatusUpdateSubscriptions = new Set<[Controller, StreamingResponseHandler]>()
private _context: vscode.ExtensionContext
@@ -100,6 +119,7 @@ export class AuthService {
})
this._setProvider(authProviders.find((authProvider) => authProvider.name === providerName).name)
this._context = context
}
@@ -118,7 +138,7 @@ export class AuthService {
}
AuthService.instance = new AuthService(context, config || {}, authProvider)
}
if (context) {
if (context !== undefined) {
AuthService.instance.context = context
}
return AuthService.instance
@@ -136,18 +156,20 @@ export class AuthService {
this._setProvider(providerName)
}
get authNonce(): string {
return this._authNonce
}
async getAuthToken(): Promise<string | null> {
if (!this._user) {
if (!this._clineAuthInfo) {
return null
}
// TODO: This may need to be dependant on the auth provider
// Return the ID token from the user object
return this._provider.provider.getAuthToken(this._user)
const idToken = this._clineAuthInfo.idToken
const shouldRefreshIdToken = await this._provider?.provider.shouldRefreshIdToken(idToken)
if (shouldRefreshIdToken) {
// Retrieves the stored id token and refreshes it, then updates this._clineAuthInfo
await this.restoreRefreshTokenAndRetrieveAuthInfo()
if (!this._clineAuthInfo) {
return null
}
}
return this._clineAuthInfo.idToken
}
private _setProvider(providerName: string): void {
@@ -160,9 +182,17 @@ export class AuthService {
}
getInfo(): AuthState {
let user = null
if (this._user && this._authenticated) {
user = this._provider.provider.convertUserData(this._user)
// TODO: this logic should be cleaner, but this will determine the authentication state for the webview -- if a user object is returned then the webview assumes authenticated, otherwise it assumes logged out (we previously returned a UserInfo object with empty fields, and this represented a broken logged in state)
let user: any = null
if (this._clineAuthInfo && this._authenticated) {
const userInfo = this._clineAuthInfo.userInfo
user = UserInfo.create({
// TODO: create proto for new user info type
uid: userInfo?.id,
displayName: userInfo?.displayName,
email: userInfo?.email,
photoUrl: undefined,
})
}
return AuthState.create({
@@ -184,7 +214,6 @@ export class AuthService {
// Use URL object for more graceful query construction
const authUrl = new URL(this._config.URI)
authUrl.searchParams.set("state", this._authNonce)
authUrl.searchParams.set("callback_url", callbackUrl)
const authUrlString = authUrl.toString()
@@ -199,8 +228,7 @@ export class AuthService {
}
try {
await this._provider.provider.signOut()
this._user = null
this._clineAuthInfo = null
this._authenticated = false
this.sendAuthStatusUpdate()
} catch (error) {
@@ -215,12 +243,11 @@ export class AuthService {
}
try {
this._user = await this._provider.provider.signIn(this._context, token, provider)
this._clineAuthInfo = await this._provider.provider.signIn(this._context, token, provider)
this._authenticated = true
await this.sendAuthStatusUpdate()
this.setupAutoRefreshAuth()
return this._user
// return this._clineAuthInfo
} catch (error) {
console.error("Error signing in with custom token:", error)
throw error
@@ -239,59 +266,29 @@ export class AuthService {
* Restores the authentication token from the extension's storage.
* This is typically called when the extension is activated.
*/
async restoreAuthToken(): Promise<void> {
async restoreRefreshTokenAndRetrieveAuthInfo(): Promise<void> {
if (!this._provider || !this._provider.provider) {
throw new Error("Auth provider is not set")
}
try {
this._user = await this._provider.provider.restoreAuthCredential(this._context)
if (this._user) {
this._clineAuthInfo = await this._provider.provider.retrieveClineAuthInfo(this._context)
if (this._clineAuthInfo) {
this._authenticated = true
await this.sendAuthStatusUpdate()
this.setupAutoRefreshAuth()
// Setup auto-refresh for the auth token
} else {
console.warn("No user found after restoring auth token")
this._authenticated = false
this._user = null
this._clineAuthInfo = null
}
} catch (error) {
console.error("Error restoring auth token:", error)
this._authenticated = false
this._user = null
this._clineAuthInfo = null
return
}
}
/**
* Refreshes the authentication status and sends an update to all subscribers.
*/
async refreshAuth(): Promise<void> {
if (!this._user) {
console.warn("No user is authenticated, skipping auth refresh")
return
}
await this._provider.provider.refreshAuthToken()
this.sendAuthStatusUpdate()
}
private setupAutoRefreshAuth(): void {
// Set timeoutDuration to refresh the auth token 5 minutes before it expires
const timeoutDuration = Math.floor(this._user.stsTokenManager.expirationTime - 5 * 60000 - Date.now()) // Milliseconds until 5 minutes before expiration
setTimeout(() => this._autoRefreshAuth(), timeoutDuration)
}
private async _autoRefreshAuth(): Promise<void> {
if (!this._user) {
console.warn("No user is authenticated, skipping auth refresh")
return
}
await this.refreshAuth()
this.setupAutoRefreshAuth() // Reschedule the next auto-refresh
}
/**
* Subscribe to authStatusUpdate events
* @param controller The controller instance
@@ -1,18 +1,11 @@
import { getSecret, storeSecret } from "@/core/storage/state"
import { ErrorService } from "@/services/error/ErrorService"
import axios from "axios"
import { initializeApp } from "firebase/app"
import {
AuthCredential,
GoogleAuthProvider,
GithubAuthProvider,
OAuthCredential,
User,
UserCredential,
getAuth,
signInWithCredential,
signOut,
} from "firebase/auth"
import { GithubAuthProvider, GoogleAuthProvider, User, getAuth, signInWithCredential } from "firebase/auth"
import { ExtensionContext } from "vscode"
import { ClineAccountUserInfo, ClineAuthInfo } from "../AuthService"
import { jwtDecode } from "jwt-decode"
export class FirebaseAuthProvider {
private _config: any
@@ -29,80 +22,16 @@ export class FirebaseAuthProvider {
this._config = value
}
/**
* Gets the authentication token of the current user.
* @returns {Promise<string | null>} A promise that resolves to the authentication token of the current user, or null if no user is signed in.
*/
async getAuthToken(): Promise<string | null> {
const user = getAuth().currentUser
const idToken = user ? await user.getIdToken() : null
return idToken
}
/**
* Gets the refresh token of the current user.
* @returns {Promise<string | null>} A promise that resolves to the refresh token of the current user, or null if no user is signed in.
*/
async getRefreshToken(): Promise<string | null> {
const user = getAuth().currentUser
const refreshToken = user ? user.refreshToken : null
return refreshToken
}
/**
* Refreshes the authentication token of the current user.
* @returns {Promise<string | null>} A promise that resolves to the refreshed authentication token of the current user, or null if no user is signed in.
*/
async refreshAuthToken(): Promise<string | null> {
const user = getAuth().currentUser
const idToken = user ? await user.getIdToken(true) : null
return idToken
}
/**
* Converts Firebase User object to a generic user object.
* @param user - The Firebase User object.
* @returns {User} A generic user object.
*/
convertUserData(user: User) {
return {
uid: user.uid,
email: user.email,
displayName: user.displayName,
photoUrl: user.photoURL,
}
}
/**
* Signs out the current user from Firebase.
* @returns {Promise<void>} A promise that resolves when the user is signed out.
*/
async signOut(): Promise<void> {
signOut(getAuth(initializeApp(Object.assign({}, this._config))))
.then(() => {
console.log("User signed out successfully.")
})
.catch((error) => {
ErrorService.logMessage("Firebase sign-out error", "error")
ErrorService.logException(error)
throw error
})
}
/**
* Stores the authentication token using a provided token.
* @param token - The authentication token to store.
* @returns {Promise<User>} A promise that resolves with the authenticated user.
* @throws {Error} Throws an error if the storage fails.
*/
private async _storeAuthCredential(context: ExtensionContext, credential: AuthCredential): Promise<void> {
try {
await storeSecret(context, "clineAccountId", JSON.stringify(credential.toJSON()))
} catch (error) {
ErrorService.logMessage("Firebase store token error", "error")
ErrorService.logException(error)
throw error
async shouldRefreshIdToken(existingIdToken: string): Promise<boolean> {
const decodedToken = jwtDecode(existingIdToken)
const exp = decodedToken.exp || 0 // 1752297633
const expirationTime = exp * 1000
const currentTime = Date.now()
const fiveMinutesInMs = 5 * 60 * 1000
if (currentTime > expirationTime - fiveMinutesInMs) {
return true // id token is expired or about to be expired
}
return false
}
/**
@@ -111,31 +40,55 @@ export class FirebaseAuthProvider {
* @returns {Promise<User>} A promise that resolves with the authenticated user.
* @throws {Error} Throws an error if the restoration fails.
*/
async restoreAuthCredential(context: ExtensionContext): Promise<User | null> {
const credentialJSON = await getSecret(context, "clineAccountId")
if (!credentialJSON) {
async retrieveClineAuthInfo(context: ExtensionContext): Promise<ClineAuthInfo | null> {
const userRefreshToken = await getSecret(context, "clineAccountId")
if (!userRefreshToken) {
console.error("No stored authentication credential found.")
return null
}
try {
const credentialData: AuthCredential = OAuthCredential.fromJSON(credentialJSON) as AuthCredential
const userCredential = await this._signInWithCredential(credentialData)
return userCredential.user
} catch (error) {
ErrorService.logMessage("Firebase restore token error", "error")
ErrorService.logException(error)
throw error
}
}
// Exchange refresh token for new access token using Firebase's secure token endpoint
// https://stackoverflow.com/questions/38233687/how-to-use-the-firebase-refreshtoken-to-reauthenticate/57119131#57119131
const firebaseApiKey = this._config.apiKey
const googleAccessTokenResponse = await axios.post(
`https://securetoken.googleapis.com/v1/token?key=${firebaseApiKey}`,
`grant_type=refresh_token&refresh_token=${encodeURIComponent(userRefreshToken)}`,
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
},
)
async _signInWithCredential(credential: AuthCredential): Promise<UserCredential> {
const firebaseConfig = Object.assign({}, this._config)
const app = initializeApp(firebaseConfig)
const auth = getAuth(app)
try {
return await signInWithCredential(auth, credential)
// console.log("googleAccessTokenResponse", googleAccessTokenResponse)
// This returns an object with access_token, expires_in (3600), id_token (can be used as bearer token to authenticate requests, we'll use this in the future instead of firebase but need to be aware of how we use firebase sdk for e.g. user info like the profile image), project_id, refresh_token, token_type (always Bearer), and user_id
const idToken = googleAccessTokenResponse.data.id_token
// const idTokenExpirationDate = new Date(Date.now() + googleAccessTokenResponse.data.expires_in * 1000)
// Now retrieve the user info from the backend (this was an easy solution to keep providing user profile details like name and email, but we should move to using the fetchMe() function instead)
// Fetch user info from Cline API
// TODO: consolidate with fetchMe() instead of making the call directly here
const userResponse = await axios.get("https://api.cline.bot/api/v1/users/me", {
headers: {
Authorization: `Bearer ${idToken}`,
},
})
// Store user data
const userInfo: ClineAccountUserInfo = userResponse.data.data
return { idToken, userInfo }
// let userObject = JSON.parse(credentialJSON)
// let user = User.
// userObject = User.constructor._fromJSON(auth, user2);
// const credentialData: AuthCredential = OAuthCredential.fromJSON(credentialJSON) as AuthCredential
// const userCredential = await this._signInWithCredential(context, credentialData)
// return userCredential.user
} catch (error) {
ErrorService.logMessage("Firebase sign-in with credential error", "error")
console.error("Firebase restore token error", error)
ErrorService.logMessage("Firebase restore token error", "error")
ErrorService.logException(error)
throw error
}
@@ -146,10 +99,9 @@ export class FirebaseAuthProvider {
* @returns {Promise<User>} A promise that resolves with the authenticated user.
* @throws {Error} Throws an error if the sign-in fails.
*/
async signIn(context: ExtensionContext, token: string, provider: string): Promise<User> {
async signIn(context: ExtensionContext, token: string, provider: string): Promise<ClineAuthInfo | null> {
try {
let credential
let userCredential
switch (provider) {
case "google":
credential = GoogleAuthProvider.credential(token)
@@ -160,9 +112,25 @@ export class FirebaseAuthProvider {
default:
throw new Error(`Unsupported provider: ${provider}`)
}
this._storeAuthCredential(context, credential)
userCredential = await this._signInWithCredential(credential)
return userCredential.user
// we've received the short-lived tokens from google/github, now we need to sign in to firebase with them
const firebaseConfig = Object.assign({}, this._config)
const app = initializeApp(firebaseConfig)
const auth = getAuth(app)
// this signs the user into firebase sdk internally
const userCredential = (await signInWithCredential(auth, credential)).user
// const userRefreshToken = await userCredential.getIdToken()
// store the long-lived refresh token in secret storage
try {
await storeSecret(context, "clineAccountId", userCredential.refreshToken)
} catch (error) {
ErrorService.logMessage("Firebase store token error", "error")
ErrorService.logException(error)
throw error
}
// userCredential = await this._signInWithCredential(context, credential)
return await this.retrieveClineAuthInfo(context)
} catch (error) {
ErrorService.logMessage("Firebase sign-in error", "error")
ErrorService.logException(error)
+58 -19
View File
@@ -20,10 +20,8 @@ import * as path from "path"
import * as vscode from "vscode"
import { z } from "zod"
import { FileChangeEvent_ChangeType, SubscribeToFileRequest } from "../../shared/proto/host/watch"
import { Metadata } from "../../shared/proto/common"
import {
DEFAULT_MCP_TIMEOUT_SECONDS,
McpMode,
McpResource,
McpResourceResponse,
McpResourceTemplate,
@@ -33,15 +31,14 @@ import {
MIN_MCP_TIMEOUT_SECONDS,
} from "@shared/mcp"
import { fileExistsAtPath } from "@utils/fs"
import { arePathsEqual } from "@utils/path"
import { secondsToMs } from "@utils/time"
import { GlobalFileNames } from "@core/storage/disk"
import { ExtensionMessage } from "@shared/ExtensionMessage"
import { DEFAULT_REQUEST_TIMEOUT_MS } from "./constants"
import { Transport, McpConnection, McpTransportType, McpServerConfig } from "./types"
import { McpConnection, McpServerConfig } from "./types"
import { BaseConfigSchema, ServerConfigSchema, McpSettingsSchema } from "./schemas"
import { getHostBridgeProvider } from "@/hosts/host-providers"
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
export class McpHub {
getMcpServersPath: () => Promise<string>
private getSettingsDirectoryPath: () => Promise<string>
@@ -112,8 +109,11 @@ export class McpHub {
try {
config = JSON.parse(content)
} catch (error) {
vscode.window.showErrorMessage(
"Invalid MCP settings format. Please ensure your settings follow the correct JSON format.",
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: "Invalid MCP settings format. Please ensure your settings follow the correct JSON format.",
}),
)
return undefined
}
@@ -121,7 +121,12 @@ export class McpHub {
// Validate against schema
const result = McpSettingsSchema.safeParse(config)
if (!result.success) {
vscode.window.showErrorMessage("Invalid MCP settings schema.")
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: "Invalid MCP settings schema.",
}),
)
return undefined
}
@@ -153,7 +158,12 @@ export class McpHub {
if (settings) {
try {
await this.updateServerConnections(settings.mcpServers)
vscode.window.showInformationMessage("MCP servers updated")
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: "MCP servers updated",
}),
)
} catch (error) {
console.error("Failed to process MCP settings change:", error)
}
@@ -403,8 +413,11 @@ export class McpHub {
console.log(`[MCP Fallback Notification] ${name}:`, JSON.stringify(notification, null, 2))
// Show in VS Code for visibility
vscode.window.showInformationMessage(
`MCP ${name}: ${notification.method || "unknown"} - ${JSON.stringify(notification.params || {})}`,
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: `MCP ${name}: ${notification.method || "unknown"} - ${JSON.stringify(notification.params || {})}`,
}),
)
}
console.log(`[MCP Debug] Successfully set fallback notification handler for ${name}`)
@@ -658,7 +671,12 @@ export class McpHub {
const connection = this.connections.find((conn) => conn.server.name === serverName)
const config = connection?.server.config
if (config) {
vscode.window.showInformationMessage(`Restarting ${serverName} MCP server...`)
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: `Restarting ${serverName} MCP server...`,
}),
)
connection.server.status = "connecting"
connection.server.error = ""
await this.notifyWebviewOfServerChanges()
@@ -667,10 +685,20 @@ export class McpHub {
await this.deleteConnection(serverName)
// Try to connect again using existing config
await this.connectToServer(serverName, JSON.parse(config), "internal")
vscode.window.showInformationMessage(`${serverName} MCP server connected`)
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.INFORMATION,
message: `${serverName} MCP server connected`,
}),
)
} catch (error) {
console.error(`Failed to restart connection for ${serverName}:`, error)
vscode.window.showErrorMessage(`Failed to connect to ${serverName} MCP server`)
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Failed to connect to ${serverName} MCP server`,
}),
)
}
}
@@ -756,8 +784,11 @@ export class McpHub {
if (error instanceof Error) {
console.error("Error details:", error.message, error.stack)
}
vscode.window.showErrorMessage(
`Failed to update server state: ${error instanceof Error ? error.message : String(error)}`,
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Failed to update server state: ${error instanceof Error ? error.message : String(error)}`,
}),
)
throw error
}
@@ -915,7 +946,12 @@ export class McpHub {
}
} catch (error) {
console.error("Failed to update autoApprove settings:", error)
vscode.window.showErrorMessage("Failed to update autoApprove settings")
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: "Failed to update autoApprove settings",
}),
)
throw error // Re-throw to ensure the error is properly handled
}
}
@@ -1033,8 +1069,11 @@ export class McpHub {
if (error instanceof Error) {
console.error("Error details:", error.message, error.stack)
}
vscode.window.showErrorMessage(
`Failed to update server timeout: ${error instanceof Error ? error.message : String(error)}`,
getHostBridgeProvider().windowClient.showMessage(
ShowMessageRequest.create({
type: ShowMessageType.ERROR,
message: `Failed to update server timeout: ${error instanceof Error ? error.message : String(error)}`,
}),
)
throw error
}
@@ -29,6 +29,11 @@ interface Collection {
*/
type TelemetryCategory = "checkpoints" | "browser"
/**
* Maximum length for error messages to prevent excessive data
*/
const MAX_ERROR_MESSAGE_LENGTH = 500
class TelemetryService {
// Map to control specific telemetry categories (event types)
private telemetryCategoryEnabled: Map<TelemetryCategory, boolean> = new Map([
@@ -83,6 +88,8 @@ class TelemetryService {
BROWSER_ERROR: "task.browser_error",
// Tracks Gemini API specific performance metrics
GEMINI_API_PERFORMANCE: "task.gemini_api_performance",
// Tracks when API providers return errors
PROVIDER_API_ERROR: "task.provider_api_error",
// Collection of all task events
TASK_COLLECTION: "task.collection",
},
@@ -720,6 +727,38 @@ class TelemetryService {
)
}
/**
* Records telemetry when an API provider returns an error
* @param taskId Unique identifier for the task
* @param model Identifier of the model used
* @param requestId Unique identifier for the specific API request
* @param errorMessage Detailed error message from the API provider
* @param errorStatus HTTP status code of the error response, if available
* @param collect Optional flag to determine if the event should be collected for batch sending
*/
public captureProviderApiError(
args: {
taskId: string
model: string
errorMessage: string
errorStatus?: number | undefined
requestId?: string | undefined
},
collect: boolean = true,
) {
this.capture(
{
event: TelemetryService.EVENTS.TASK.PROVIDER_API_ERROR,
properties: {
...args,
errorMessage: args.errorMessage.substring(0, MAX_ERROR_MESSAGE_LENGTH), // Truncate long error messages
timestamp: new Date().toISOString(),
},
},
collect,
)
}
/**
* Checks if telemetry is enabled
* @returns Boolean indicating whether telemetry is enabled
+3
View File
@@ -76,3 +76,6 @@ export interface OrganizationUsageTransaction {
totalTokens: number
userId: string
}
// Used in cline.ts provider and in webview-ui/src/components/chat/ChatRow.tsx to display the login button
export const CLINE_ACCOUNT_AUTH_ERROR_MESSAGE = "Unauthorized: Please sign in to Cline before trying again."
+2 -1
View File
@@ -7,6 +7,7 @@ import { HistoryItem } from "./HistoryItem"
import { TelemetrySetting } from "./TelemetrySetting"
import { ClineRulesToggles } from "./cline-rules"
import { UserInfo } from "./UserInfo"
import { McpDisplayMode, DEFAULT_MCP_DISPLAY_MODE } from "./McpDisplayMode"
// webview will hold state
export interface ExtensionMessage {
@@ -37,7 +38,7 @@ export interface ExtensionState {
clineMessages: ClineMessage[]
currentTaskItem?: HistoryItem
mcpMarketplaceEnabled?: boolean
mcpRichDisplayEnabled: boolean
mcpDisplayMode: McpDisplayMode
planActSeparateModelsSetting: boolean
enableCheckpointsSetting?: boolean
platform: Platform
+9
View File
@@ -0,0 +1,9 @@
/**
* Represents the different display modes available for MCP responses
*/
export type McpDisplayMode = "rich" | "plain" | "markdown"
/**
* Default display mode for MCP responses
*/
export const DEFAULT_MCP_DISPLAY_MODE: McpDisplayMode = "plain"
+38 -1
View File
@@ -20,6 +20,7 @@ export type ApiProvider =
| "vscode-lm"
| "cline"
| "litellm"
| "moonshot"
| "nebius"
| "fireworks"
| "asksage"
@@ -50,8 +51,10 @@ export interface ApiHandlerOptions {
awsRegion?: string
awsUseCrossRegionInference?: boolean
awsBedrockUsePromptCache?: boolean
awsAuthentication?: string
awsUseProfile?: boolean
awsProfile?: string
awsBedrockApiKey?: string
awsBedrockEndpoint?: string
awsBedrockCustomSelected?: boolean
awsBedrockCustomModelBaseId?: BedrockModelId
@@ -86,6 +89,8 @@ export interface ApiHandlerOptions {
azureApiVersion?: string
vsCodeLmModelSelector?: LanguageModelChatSelector
qwenApiLine?: string
moonshotApiLine?: string
moonshotApiKey?: string
nebiusApiKey?: string
asksageApiUrl?: string
asksageApiKey?: string
@@ -2094,7 +2099,8 @@ export const xaiModels = {
supportsImages: true,
supportsPromptCache: true,
inputPrice: 3.0, // will have different pricing for long context vs short context
outputPrice: 6.0,
cacheReadsPrice: 0.75,
outputPrice: 15.0,
},
"grok-3-beta": {
maxTokens: 8192,
@@ -2549,3 +2555,34 @@ export const sapAiCoreModels = {
description: sapAiCoreModelDescription,
},
} as const satisfies Record<string, ModelInfo>
// Moonshot AI Studio
// https://platform.moonshot.ai/docs/pricing/chat
export const moonshotModels = {
"kimi-k2-0711-preview": {
maxTokens: 131_072,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.6,
outputPrice: 2.5,
},
"moonshot-v1-128k-vision-preview": {
maxTokens: 131_072,
contextWindow: 131_072,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 2,
outputPrice: 5,
},
"kimi-thinking-preview": {
maxTokens: 131_072,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 30,
outputPrice: 30,
},
} as const satisfies Record<string, ModelInfo>
export type MoonshotModelId = keyof typeof moonshotModels
export const moonshotDefaultModelId = "kimi-k2-0711-preview" satisfies MoonshotModelId
@@ -222,6 +222,8 @@ function convertApiProviderToProto(provider: string | undefined): ProtoApiProvid
return ProtoApiProvider.CLINE
case "litellm":
return ProtoApiProvider.LITELLM
case "moonshot":
return ProtoApiProvider.MOONSHOT
case "nebius":
return ProtoApiProvider.NEBIUS
case "fireworks":
@@ -282,6 +284,8 @@ function convertProtoToApiProvider(provider: ProtoApiProvider): ApiProvider {
return "cline"
case ProtoApiProvider.LITELLM:
return "litellm"
case ProtoApiProvider.MOONSHOT:
return "moonshot"
case ProtoApiProvider.NEBIUS:
return "nebius"
case ProtoApiProvider.FIREWORKS:
@@ -328,7 +332,9 @@ export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoA
awsUseCrossRegionInference: config.awsUseCrossRegionInference,
awsBedrockUsePromptCache: config.awsBedrockUsePromptCache,
awsUseProfile: config.awsUseProfile,
awsAuthentication: config.awsAuthentication,
awsProfile: config.awsProfile,
awsBedrockApiKey: config.awsBedrockApiKey,
awsBedrockEndpoint: config.awsBedrockEndpoint,
awsBedrockCustomSelected: config.awsBedrockCustomSelected,
awsBedrockCustomModelBaseId: config.awsBedrockCustomModelBaseId as string | undefined,
@@ -362,6 +368,8 @@ export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoA
azureApiVersion: config.azureApiVersion,
vsCodeLmModelSelector: config.vsCodeLmModelSelector,
qwenApiLine: config.qwenApiLine,
moonshotApiLine: config.moonshotApiLine,
moonshotApiKey: config.moonshotApiKey,
nebiusApiKey: config.nebiusApiKey,
asksageApiUrl: config.asksageApiUrl,
asksageApiKey: config.asksageApiKey,
@@ -407,7 +415,9 @@ export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguratio
awsUseCrossRegionInference: protoConfig.awsUseCrossRegionInference,
awsBedrockUsePromptCache: protoConfig.awsBedrockUsePromptCache,
awsUseProfile: protoConfig.awsUseProfile,
awsAuthentication: protoConfig.awsAuthentication,
awsProfile: protoConfig.awsProfile,
awsBedrockApiKey: protoConfig.awsBedrockApiKey,
awsBedrockEndpoint: protoConfig.awsBedrockEndpoint,
awsBedrockCustomSelected: protoConfig.awsBedrockCustomSelected,
awsBedrockCustomModelBaseId: protoConfig.awsBedrockCustomModelBaseId as BedrockModelId | undefined,
@@ -441,6 +451,8 @@ export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguratio
azureApiVersion: protoConfig.azureApiVersion,
vsCodeLmModelSelector: protoConfig.vsCodeLmModelSelector,
qwenApiLine: protoConfig.qwenApiLine,
moonshotApiLine: protoConfig.moonshotApiLine,
moonshotApiKey: protoConfig.moonshotApiKey,
nebiusApiKey: protoConfig.nebiusApiKey,
asksageApiUrl: protoConfig.asksageApiUrl,
asksageApiKey: protoConfig.asksageApiKey,
@@ -30,6 +30,7 @@ export function convertApiConfigurationToProtoApiConfiguration(config: ApiConfig
qwenApiKey: config.qwenApiKey,
doubaoApiKey: config.doubaoApiKey,
mistralApiKey: config.mistralApiKey,
moonshotApiKey: config.moonshotApiKey,
nebiusApiKey: config.nebiusApiKey,
asksageApiKey: config.asksageApiKey,
xaiApiKey: config.xaiApiKey,
@@ -60,7 +61,9 @@ export function convertApiConfigurationToProtoApiConfiguration(config: ApiConfig
awsUseCrossRegionInference: config.awsUseCrossRegionInference,
awsBedrockUsePromptCache: config.awsBedrockUsePromptCache,
awsUseProfile: config.awsUseProfile,
awsAuthentication: config.awsAuthentication,
awsProfile: config.awsProfile,
awsBedrockApiKey: config.awsBedrockApiKey,
awsBedrockEndpoint: config.awsBedrockEndpoint,
// Vertex AI fields
@@ -99,6 +102,9 @@ export function convertApiConfigurationToProtoApiConfiguration(config: ApiConfig
// Qwen specific
qwenApiLine: config.qwenApiLine,
// Moonshot specific
moonshotApiLine: config.moonshotApiLine,
// OpenRouter specific
openrouterProviderSorting: config.openRouterProviderSorting,
@@ -150,6 +156,7 @@ export function convertProtoApiConfigurationToApiConfiguration(protoConfig: Prot
qwenApiKey: protoConfig.qwenApiKey,
doubaoApiKey: protoConfig.doubaoApiKey,
mistralApiKey: protoConfig.mistralApiKey,
moonshotApiKey: protoConfig.moonshotApiKey,
nebiusApiKey: protoConfig.nebiusApiKey,
asksageApiKey: protoConfig.asksageApiKey,
xaiApiKey: protoConfig.xaiApiKey,
@@ -177,6 +184,8 @@ export function convertProtoApiConfigurationToApiConfiguration(protoConfig: Prot
awsBedrockUsePromptCache: protoConfig.awsBedrockUsePromptCache,
awsUseProfile: protoConfig.awsUseProfile,
awsProfile: protoConfig.awsProfile,
awsAuthentication: protoConfig.awsAuthentication,
awsBedrockApiKey: protoConfig.awsBedrockApiKey,
awsBedrockEndpoint: protoConfig.awsBedrockEndpoint,
// Vertex AI fields
@@ -215,6 +224,9 @@ export function convertProtoApiConfigurationToApiConfiguration(protoConfig: Prot
// Qwen specific
qwenApiLine: protoConfig.qwenApiLine,
// Moonshot specific
moonshotApiLine: protoConfig.moonshotApiLine,
// OpenRouter specific
openRouterProviderSorting: protoConfig.openrouterProviderSorting,
@@ -0,0 +1,28 @@
import { getHostBridgeProvider } from "@/hosts/host-providers"
import { DiffViewProvider } from "@/integrations/editor/DiffViewProvider"
export class ExternalDiffViewProvider extends DiffViewProvider {
private activeDiffEditorId: string | undefined
override async openDiffEditor(): Promise<void> {
if (!this.absolutePath) {
return
}
const response = await getHostBridgeProvider().diffClient.openDiff({
path: this.absolutePath,
content: this.originalContent ?? "",
})
this.activeDiffEditorId = response.diffId
}
override async replaceText(
content: string,
rangeToReplace: { startLine: number; endLine: number },
_currentLine: number,
): Promise<void> {
await getHostBridgeProvider().diffClient.replaceText({
diffId: this.activeDiffEditorId,
content: content,
startLine: rangeToReplace.startLine,
endLine: rangeToReplace.endLine,
})
}
}
@@ -13,11 +13,12 @@ import { ExternalHostBridgeClientManager } from "./host-bridge-client-manager"
import { ExternalWebviewProvider } from "./ExternalWebviewProvider"
import { WebviewProviderType } from "@/shared/webview/types"
import { v4 as uuidv4 } from "uuid"
import { ExternalDiffViewProvider } from "./ExternalDiffviewProvider"
async function main() {
log("Starting standalone service...")
hostProviders.initializeHostProviders(createWebview, new ExternalHostBridgeClientManager())
hostProviders.initializeHostProviders(createWebview, createDiffView, new ExternalHostBridgeClientManager())
activate(extensionContext)
const controller = new Controller(extensionContext, outputChannel, postMessage, uuidv4())
startProtobusService(controller)
@@ -60,9 +61,12 @@ function getProtobusServiceNames(packageDefinition: { [x: string]: any }): strin
return protobusServiceNames
}
const createWebview = () => {
function createWebview() {
return new ExternalWebviewProvider(extensionContext, outputChannel, WebviewProviderType.SIDEBAR)
}
function createDiffView() {
return new ExternalDiffViewProvider()
}
/**
* Wraps a Promise-based handler function to make it compatible with gRPC's callback-based API.
@@ -4,12 +4,14 @@ import {
WorkspaceServiceClientImpl,
EnvServiceClientImpl,
WindowServiceClientImpl,
DiffServiceClientImpl,
} from "@generated/standalone/host-bridge-clients"
import {
WatchServiceClientInterface,
WorkspaceServiceClientInterface,
EnvServiceClientInterface,
WindowServiceClientInterface,
DiffServiceClientInterface,
} from "@generated/hosts/host-bridge-client-types"
import { HostBridgeClientProvider } from "@/hosts/host-provider-types"
@@ -23,6 +25,7 @@ export class ExternalHostBridgeClientManager implements HostBridgeClientProvider
workspaceClient: WorkspaceServiceClientInterface
envClient: EnvServiceClientInterface
windowClient: WindowServiceClientInterface
diffClient: DiffServiceClientInterface
constructor() {
const address = process.env.HOST_BRIDGE_ADDRESS || "localhost:50052"
@@ -32,6 +35,7 @@ export class ExternalHostBridgeClientManager implements HostBridgeClientProvider
this.workspaceClient = new WorkspaceServiceClientImpl(this.channel)
this.envClient = new EnvServiceClientImpl(this.channel)
this.windowClient = new WindowServiceClientImpl(this.channel)
this.diffClient = new DiffServiceClientImpl(this.channel)
}
public close(): void {
+30
View File
@@ -0,0 +1,30 @@
import { describe, it } from "mocha"
import "should"
import { shouldSkipReasoningForModel } from "../model-utils"
describe("shouldSkipReasoningForModel", () => {
it("should return true for grok-4 models", () => {
shouldSkipReasoningForModel("grok-4").should.equal(true)
shouldSkipReasoningForModel("x-ai/grok-4").should.equal(true)
shouldSkipReasoningForModel("openrouter/grok-4-turbo").should.equal(true)
shouldSkipReasoningForModel("some-provider/grok-4-mini").should.equal(true)
})
it("should return false for non-grok-4 models", () => {
shouldSkipReasoningForModel("grok-3").should.equal(false)
shouldSkipReasoningForModel("grok-2").should.equal(false)
shouldSkipReasoningForModel("claude-3-sonnet").should.equal(false)
shouldSkipReasoningForModel("gpt-4").should.equal(false)
shouldSkipReasoningForModel("gemini-pro").should.equal(false)
})
it("should return false for undefined or empty model IDs", () => {
shouldSkipReasoningForModel(undefined).should.equal(false)
shouldSkipReasoningForModel("").should.equal(false)
})
it("should be case sensitive", () => {
shouldSkipReasoningForModel("GROK-4").should.equal(false)
shouldSkipReasoningForModel("Grok-4").should.equal(false)
})
})
+11
View File
@@ -13,3 +13,14 @@ export function isGemini2dot5ModelFamily(api: ApiHandler): boolean {
const modelId = model.id
return modelId.includes("gemini-2.5")
}
/**
* Determines if reasoning content should be skipped for a given model
* Currently skips reasoning for Grok-4 models since they only display "thinking" without useful information
*/
export function shouldSkipReasoningForModel(modelId?: string): boolean {
if (!modelId) {
return false
}
return modelId.includes("grok-4")
}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "cline-standalone",
"name": "cline-core",
"version": "0.0.1",
"main": "standalone.js",
"main": "cline-core.js",
"dependencies": {
"@grpc/grpc-js": "^1.13.3",
"@grpc/reflection": "^1.0.4",
+10 -19
View File
@@ -9,6 +9,7 @@
"version": "0.3.0",
"dependencies": {
"@floating-ui/react": "^0.27.4",
"@fontsource/azeret-mono": "^5.2.9",
"@heroui/react": "^2.8.0-beta.2",
"@vscode/webview-ui-toolkit": "^1.4.0",
"debounce": "^2.1.1",
@@ -23,7 +24,6 @@
"posthog-js": "^1.224.0",
"pretty-bytes": "^6.1.1",
"react": "^18.3.1",
"react-countup": "^6.5.3",
"react-dom": "^18.3.1",
"react-remark": "^2.1.0",
"react-textarea-autosize": "^8.5.7",
@@ -1261,6 +1261,15 @@
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
"license": "MIT"
},
"node_modules/@fontsource/azeret-mono": {
"version": "5.2.9",
"resolved": "https://registry.npmjs.org/@fontsource/azeret-mono/-/azeret-mono-5.2.9.tgz",
"integrity": "sha512-1qnbVspQPI38qhSTSidWU4bjG5ynWCfkMwfPxahqxejJO/u4yT1FbPqG73s4fDmQSuDQYoA8jfTpoQiod7+fuA==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@formatjs/ecma402-abstract": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.4.tgz",
@@ -8327,12 +8336,6 @@
"layout-base": "^1.0.0"
}
},
"node_modules/countup.js": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/countup.js/-/countup.js-2.8.0.tgz",
"integrity": "sha512-f7xEhX0awl4NOElHulrl4XRfKoNH3rB+qfNSZZyjSZhaAoUk6elvhH+MNxMmlmuUJ2/QNTWPSA7U4mNtIAKljQ==",
"license": "MIT"
},
"node_modules/create-error-class": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz",
@@ -13711,18 +13714,6 @@
"node": ">=0.10.0"
}
},
"node_modules/react-countup": {
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/react-countup/-/react-countup-6.5.3.tgz",
"integrity": "sha512-udnqVQitxC7QWADSPDOxVWULkLvKUWrDapn5i53HE4DPRVgs+Y5rr4bo25qEl8jSh+0l2cToJgGMx+clxPM3+w==",
"license": "MIT",
"dependencies": {
"countup.js": "^2.8.0"
},
"peerDependencies": {
"react": ">= 16.3.0"
}
},
"node_modules/react-devtools": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/react-devtools/-/react-devtools-6.1.2.tgz",
+1 -1
View File
@@ -16,6 +16,7 @@
},
"dependencies": {
"@floating-ui/react": "^0.27.4",
"@fontsource/azeret-mono": "^5.2.9",
"@heroui/react": "^2.8.0-beta.2",
"@vscode/webview-ui-toolkit": "^1.4.0",
"debounce": "^2.1.1",
@@ -30,7 +31,6 @@
"posthog-js": "^1.224.0",
"pretty-bytes": "^6.1.1",
"react": "^18.3.1",
"react-countup": "^6.5.3",
"react-dom": "^18.3.1",
"react-remark": "^2.1.0",
"react-textarea-autosize": "^8.5.7",
+119 -35
View File
@@ -1,16 +1,80 @@
import { VSCodeButton, VSCodeDivider, VSCodeLink, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import { memo, useCallback, useEffect, useState } from "react"
import { BadgeCent } from "lucide-react"
import {
VSCodeButton,
VSCodeDivider,
VSCodeLink,
VSCodeDropdown,
VSCodeOption,
VSCodeTag,
} from "@vscode/webview-ui-toolkit/react"
import { memo, useCallback, useEffect, useState, useRef } from "react"
import { useClineAuth } from "@/context/ClineAuthContext"
import VSCodeButtonLink from "../common/VSCodeButtonLink"
import ClineLogoWhite from "../../assets/ClineLogoWhite"
import CountUp from "react-countup"
import CreditsHistoryTable from "./CreditsHistoryTable"
import { UsageTransaction, PaymentTransaction } from "@shared/ClineAccount"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { AccountServiceClient } from "@/services/grpc-client"
import { EmptyRequest } from "@shared/proto/common"
import { UserOrganization, UserOrganizationUpdateRequest } from "@shared/proto/account"
import { formatCreditsBalance } from "@/utils/format"
// Custom hook for animated credit display with styled decimals
const useAnimatedCredits = (targetValue: number, duration: number = 660) => {
const [currentValue, setCurrentValue] = useState(0)
const animationRef = useRef<number>()
const startTimeRef = useRef<number>()
useEffect(() => {
const animate = (timestamp: number) => {
if (!startTimeRef.current) {
startTimeRef.current = timestamp
}
const elapsed = timestamp - startTimeRef.current
const progress = Math.min(elapsed / duration, 1)
// Easing function (ease-out)
const easedProgress = 1 - Math.pow(1 - progress, 3)
const newValue = easedProgress * targetValue
setCurrentValue(newValue)
if (progress < 1) {
animationRef.current = requestAnimationFrame(animate)
}
}
// Reset and start animation
startTimeRef.current = undefined
animationRef.current = requestAnimationFrame(animate)
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current)
}
}
}, [targetValue, duration])
return currentValue
}
// Custom component to handle styled credit display
const StyledCreditDisplay = ({ balance }: { balance: number }) => {
const animatedValue = useAnimatedCredits(formatCreditsBalance(balance))
const formatted = animatedValue.toFixed(4)
const parts = formatted.split(".")
const wholePart = parts[0]
const decimalPart = parts[1] || "0000"
const firstTwoDecimals = decimalPart.slice(0, 2)
const lastTwoDecimals = decimalPart.slice(2)
return (
<span className="font-azeret-mono font-light tabular-nums">
{wholePart}.{firstTwoDecimals}
<span className="text-[var(--vscode-descriptionForeground)]">{lastTwoDecimals}</span>
</span>
)
}
type VSCodeDropdownChangeEvent = Event & {
target: {
@@ -38,13 +102,22 @@ const AccountView = ({ onDone }: AccountViewProps) => {
)
}
const getMainRole = (roles?: string[]) => {
if (!roles) return undefined
if (roles.includes("owner")) return "Owner"
if (roles.includes("admin")) return "Admin"
return "Member"
}
export const ClineAccountView = () => {
const { clineUser, handleSignIn, handleSignOut } = useClineAuth()
const { userInfo, apiConfiguration } = useExtensionState()
let user = apiConfiguration?.clineAccountId ? clineUser || userInfo : undefined
const [balance, setBalance] = useState(0)
const [balance, setBalance] = useState<number | null>(null)
const [userOrganizations, setUserOrganizations] = useState<UserOrganization[]>([])
const [activeOrganization, setActiveOrganization] = useState<UserOrganization | null>(null)
const [isLoading, setIsLoading] = useState(true)
@@ -60,12 +133,12 @@ export const ClineAccountView = () => {
setIsLoading(true)
try {
const response = await AccountServiceClient.getUserCredits(EmptyRequest.create())
setBalance(response.balance?.currentBalance || 0)
setBalance(response.balance?.currentBalance ?? null)
setUsageData(response.usageTransactions)
setPaymentsData(response.paymentTransactions)
} catch (error) {
console.error("Failed to fetch user credits data:", error)
setBalance(0)
setBalance(null)
setUsageData([])
setPaymentsData([])
} finally {
@@ -97,7 +170,7 @@ export const ClineAccountView = () => {
Promise.all([getUserCredits(), getUserOrganizations()])
} catch (error) {
console.error("Failed to fetch user data:", error)
setBalance(0)
setBalance(null)
setUsageData([])
setPaymentsData([])
} finally {
@@ -145,13 +218,13 @@ export const ClineAccountView = () => {
<div className="flex flex-col pr-3 h-full">
<div className="flex flex-col w-full">
<div className="flex items-center mb-6 flex-wrap gap-y-4">
{user.photoUrl ? (
{/* {user.photoUrl ? (
<img src={user.photoUrl} alt="Profile" className="size-16 rounded-full mr-4" />
) : (
<div className="size-16 rounded-full bg-[var(--vscode-button-background)] flex items-center justify-center text-2xl text-[var(--vscode-button-foreground)] mr-4">
{user.displayName?.[0] || user.email?.[0] || "?"}
</div>
)}
) : ( */}
<div className="size-16 rounded-full bg-[var(--vscode-button-background)] flex items-center justify-center text-2xl text-[var(--vscode-button-foreground)] mr-4">
{user.displayName?.[0] || user.email?.[0] || "?"}
</div>
{/* )} */}
<div className="flex flex-col">
{user.displayName && (
@@ -164,21 +237,28 @@ export const ClineAccountView = () => {
<div className="text-sm text-[var(--vscode-descriptionForeground)]">{user.email}</div>
)}
{userOrganizations && (
<VSCodeDropdown
key={activeOrganization?.organizationId || "personal"}
currentValue={activeOrganization?.organizationId || ""}
onChange={handleOrganizationChange}
disabled={isSwitchingOrg || isLoading}
style={{ width: "100%", marginTop: "4px" }}>
<VSCodeOption value="">Personal</VSCodeOption>
{userOrganizations.map((org: UserOrganization) => (
<VSCodeOption key={org.organizationId} value={org.organizationId}>
{org.name}
</VSCodeOption>
))}
</VSCodeDropdown>
)}
<div className="flex gap-2 items-center mt-1">
{userOrganizations && (
<VSCodeDropdown
key={activeOrganization?.organizationId || "personal"}
currentValue={activeOrganization?.organizationId || ""}
onChange={handleOrganizationChange}
disabled={isSwitchingOrg || isLoading}
className="w-full">
<VSCodeOption value="">Personal</VSCodeOption>
{userOrganizations.map((org: UserOrganization) => (
<VSCodeOption key={org.organizationId} value={org.organizationId}>
{org.name}
</VSCodeOption>
))}
</VSCodeDropdown>
)}
{activeOrganization?.roles && (
<VSCodeTag className="text-xs p-2" title="Role">
{getMainRole(activeOrganization.roles)}
</VSCodeTag>
)}
</div>
</div>
</div>
</div>
@@ -199,18 +279,22 @@ export const ClineAccountView = () => {
{activeOrganization === null && (
<div className="w-full flex flex-col items-center">
<div className="text-sm text-[var(--vscode-descriptionForeground)] mb-3">CURRENT BALANCE</div>
<div className="text-sm text-[var(--vscode-descriptionForeground)] mb-3 font-azeret-mono font-light">
CURRENT BALANCE
</div>
<div className="text-4xl font-bold text-[var(--vscode-foreground)] mb-6 flex items-center gap-2">
{isLoading ? (
<div className="text-[var(--vscode-descriptionForeground)]">Loading...</div>
) : (
<>
<BadgeCent className="size-6 text-[var(--vscode-foreground)]" />
{/* TODO: Do this in a more correct way. We have to divide by 10000
* because the balance is stored in microcredits in the backend.
*/}
<CountUp end={balance / 10000} duration={0.66} decimals={4} />
{balance === null ? (
<span>----</span>
) : (
<>
<StyledCreditDisplay balance={balance} />
</>
)}
<VSCodeButton appearance="icon" className="mt-1" onClick={getUserCredits}>
<span className="codicon codicon-refresh"></span>
</VSCodeButton>
+19 -1
View File
@@ -1,4 +1,4 @@
import { VSCodeBadge, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
import { VSCodeBadge, VSCodeButton, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
import deepEqual from "fast-deep-equal"
import React, { memo, MouseEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"
import styled from "styled-components"
@@ -36,6 +36,8 @@ import NewTaskPreview from "./NewTaskPreview"
import ReportBugPreview from "./ReportBugPreview"
import UserMessage from "./UserMessage"
import QuoteButton from "./QuoteButton"
import { useClineAuth } from "@/context/ClineAuthContext"
import { CLINE_ACCOUNT_AUTH_ERROR_MESSAGE } from "@shared/ClineAccount"
const normalColor = "var(--vscode-foreground)"
const errorColor = "var(--vscode-errorForeground)"
@@ -184,6 +186,7 @@ export const ChatRowContent = memo(
sendMessageFromChatRow,
onSetQuote,
}: ChatRowContentProps) => {
const { handleSignIn, clineUser } = useClineAuth()
const { mcpServers, mcpMarketplaceCatalog, onRelinquishControl, apiConfiguration } = useExtensionState()
const [seeNewChangesDisabled, setSeeNewChangesDisabled] = useState(false)
const [quoteButtonState, setQuoteButtonState] = useState<QuoteButtonState>({
@@ -1006,6 +1009,21 @@ export const ChatRowContent = memo(
.
</>
)}
{apiRequestFailedMessage?.includes(CLINE_ACCOUNT_AUTH_ERROR_MESSAGE) && (
<>
<br />
<br />
{clineUser ? (
<span style={{ color: "var(--vscode-descriptionForeground)" }}>
(Click "Retry" below)
</span>
) : (
<VSCodeButton onClick={handleSignIn} className="w-full mb-4">
Sign in to Cline
</VSCodeButton>
)}
</>
)}
</p>
)
})()}
@@ -25,7 +25,7 @@ const CreditLimitError: React.FC<CreditLimitErrorProps> = ({
<div className="mb-2">{message}</div>
<div className="mb-3">
<div className="text-[var(--vscode-foreground)]">
Current Balance: <span className="font-bold">${(currentBalance / 1000000).toFixed(4)}</span>
Current Balance: <span className="font-bold">${currentBalance.toFixed(4)}</span>
</div>
</div>
@@ -0,0 +1,29 @@
import React from "react"
import { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import { McpDisplayMode } from "@shared/McpDisplayMode"
interface McpDisplayModeDropdownProps {
value: McpDisplayMode
onChange: (mode: McpDisplayMode) => void
id?: string
className?: string
style?: React.CSSProperties
onClick?: (e: React.MouseEvent) => void
}
const McpDisplayModeDropdown: React.FC<McpDisplayModeDropdownProps> = ({ value, onChange, id, className, style, onClick }) => {
const handleChange = (e: any) => {
const newMode = e.target.value as McpDisplayMode
onChange(newMode)
}
return (
<VSCodeDropdown id={id} value={value} onChange={handleChange} onClick={onClick} className={className} style={style}>
<VSCodeOption value="plain">Plain Text</VSCodeOption>
<VSCodeOption value="rich">Rich Display</VSCodeOption>
<VSCodeOption value="markdown">Markdown</VSCodeOption>
</VSCodeDropdown>
)
}
export default McpDisplayModeDropdown
@@ -1,12 +1,17 @@
import React, { useEffect, useState, useCallback } from "react"
import { VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react" // Import ProgressRing
import { VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "../../../context/ExtensionStateContext"
import LinkPreview from "./LinkPreview"
import ImagePreview from "./ImagePreview"
import styled from "styled-components"
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
import ChatErrorBoundary from "@/components/chat/ChatErrorBoundary"
import { isUrl, isLocalhostUrl, formatUrlForOpening, checkIfImageUrl } from "./utils/mcpRichUtil"
import MarkdownBlock from "@/components/common/MarkdownBlock"
import McpDisplayModeDropdown from "./McpDisplayModeDropdown"
import { DropdownContainer } from "@/components/settings/ApiOptions"
import { updateSetting } from "@/components/settings/utils/settingsHandlers"
import { McpDisplayMode } from "@shared/McpDisplayMode"
import { UrlMatch, processResponseUrls, DisplaySegment, buildDisplaySegments } from "./utils/mcpRichUtil"
// Maximum number of URLs to process in total, per response
export const MAX_URLS = 50
@@ -36,46 +41,6 @@ const ResponseHeader = styled.div`
}
`
const ToggleSwitch = styled.div`
display: flex;
align-items: center;
font-size: 12px;
color: var(--vscode-descriptionForeground);
.toggle-label {
margin-right: 8px;
}
.toggle-container {
position: relative;
width: 40px;
height: 20px;
background-color: var(--vscode-button-secondaryBackground);
border-radius: 10px;
cursor: pointer;
transition: background-color 0.3s;
}
.toggle-container.active {
background-color: var(--vscode-button-background);
}
.toggle-handle {
position: absolute;
top: 2px;
left: 2px;
width: 16px;
height: 16px;
background-color: var(--vscode-button-foreground);
border-radius: 50%;
transition: transform 0.3s;
}
.toggle-container.active .toggle-handle {
transform: translateX(20px);
}
`
const ResponseContainer = styled.div`
position: relative;
font-family: var(--vscode-editor-font-family, monospace);
@@ -107,28 +72,16 @@ interface McpResponseDisplayProps {
responseText: string
}
// Represents a URL found in the text with its position and metadata
interface UrlMatch {
url: string // The actual URL
fullMatch: string // The full matched text
index: number // Position in the text
isImage: boolean // Whether this URL is an image
isProcessed: boolean // Whether we've already processed this URL (to avoid duplicates)
}
const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText }) => {
const { mcpResponsesCollapsed, mcpRichDisplayEnabled } = useExtensionState() // Get setting from context
const { mcpResponsesCollapsed, mcpDisplayMode } = useExtensionState() // Get setting from context
const [isExpanded, setIsExpanded] = useState(!mcpResponsesCollapsed) // Initialize with context setting
const [isLoading, setIsLoading] = useState(false) // Initial loading state for rich content
const [displayMode, setDisplayMode] = useState<"rich" | "plain">(() => {
// Initialize directly from the global setting.
return mcpRichDisplayEnabled ? "rich" : "plain"
})
const [urlMatches, setUrlMatches] = useState<UrlMatch[]>([])
const [error, setError] = useState<string | null>(null)
const toggleDisplayMode = useCallback(() => {
setDisplayMode((prevMode) => (prevMode === "rich" ? "plain" : "rich"))
const handleDisplayModeChange = useCallback((newMode: McpDisplayMode) => {
updateSetting("mcpDisplayMode", newMode)
}, [])
const toggleExpand = useCallback(() => {
@@ -142,138 +95,89 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
// Find all URLs in the text and determine if they're images
useEffect(() => {
// Skip all processing if in plain mode
if (!isExpanded || displayMode === "plain") {
// Skip all processing if in plain mode or markdown mode
if (!isExpanded || mcpDisplayMode === "plain" || mcpDisplayMode === "markdown") {
setIsLoading(false)
if (urlMatches.length > 0) {
setUrlMatches([]) // Clear any existing matches when in plain mode
setUrlMatches([]) // Clear any existing matches when not in rich mode
}
return
}
// Use a direct boolean for cancellation that's scoped to this effect run
let processingCanceled = false
const processResponse = async () => {
console.log("Processing MCP response for URL extraction")
setIsLoading(true)
setError(null)
try {
const text = responseText || ""
const matches: UrlMatch[] = []
const urlRegex = /(?:https?:\/\/|data:image)[^\s<>"']+/g
let urlMatch: RegExpExecArray | null
let urlCount = 0
console.log("Processing MCP response for URL extraction")
setIsLoading(true)
setError(null)
// First pass: Extract all URLs and immediately make them available for rendering
while ((urlMatch = urlRegex.exec(text)) !== null && urlCount < MAX_URLS) {
// Get the original URL from the match - never modify the original URL text
const url = urlMatch[0]
// Skip invalid URLs
if (!isUrl(url)) {
console.log("Skipping invalid URL:", url)
continue
}
// Skip localhost URLs to prevent security issues
if (isLocalhostUrl(url)) {
console.log("Skipping localhost URL:", url)
continue
}
matches.push({
url,
fullMatch: url,
index: urlMatch.index,
isImage: false, // Will check later
isProcessed: false,
})
urlCount++
}
console.log(`Found ${matches.length} URLs in text, will check if they are images`)
// Set matches immediately so UI can start rendering with loading states
setUrlMatches(matches.sort((a, b) => a.index - b.index))
// Mark loading as complete to show content immediately
// Use the orchestrator function from mcpRichUtil
const cleanup = processResponseUrls(
responseText || "",
MAX_URLS,
(matches) => {
setUrlMatches(matches)
setIsLoading(false)
// Process image checks in the background - one at a time to avoid network flooding
const processImageChecks = async () => {
console.log(`Starting sequential URL processing for ${matches.length} URLs`)
for (let i = 0; i < matches.length; i++) {
// Skip already processed URLs (from extension check)
if (matches[i].isProcessed) continue
// Check if processing has been canceled (switched to plain mode)
if (processingCanceled) {
console.log("URL processing canceled - display mode changed to plain")
return
}
const match = matches[i]
console.log(`Processing URL ${i + 1} of ${matches.length}: ${match.url}`)
try {
// Process each URL individually
const isImage = await checkIfImageUrl(match.url)
// Skip if processing has been canceled
if (processingCanceled) return
// Update the match in place
match.isImage = isImage
match.isProcessed = true
// Update state after each URL to show progress
// Create a new array to ensure React detects the state change
setUrlMatches([...matches])
} catch (err) {
console.log(`URL check error: ${match.url}`, err)
match.isProcessed = true
// Update state even on error
if (!processingCanceled) {
setUrlMatches([...matches])
}
}
// Delay between URL processing to avoid overwhelming the network
if (!processingCanceled && i < matches.length - 1) {
await new Promise((resolve) => setTimeout(resolve, 100))
}
}
console.log(`URL processing complete. Found ${matches.filter((m) => m.isImage).length} image URLs`)
}
// Start the background processing
processImageChecks()
} catch (error) {
setError("Failed to process response content. Switch to plain text mode to view safely.")
},
(updatedMatches) => {
setUrlMatches(updatedMatches)
},
(errorMessage) => {
setError(errorMessage)
setIsLoading(false)
}
}
},
)
processResponse()
return cleanup
}, [responseText, mcpDisplayMode, isExpanded])
// Cleanup function to cancel processing if component unmounts or dependencies change
return () => {
processingCanceled = true
console.log("Cleaning up URL processing")
// Helper function to render a display segment
const renderSegment = (segment: DisplaySegment): JSX.Element => {
switch (segment.type) {
case "text":
case "url":
return <UrlText key={segment.key}>{segment.content}</UrlText>
case "image":
return (
<div key={segment.key}>
<ImagePreview url={segment.url!} />
</div>
)
case "link":
return (
<div key={segment.key} style={{ margin: "10px 0" }}>
<LinkPreview url={segment.url!} />
</div>
)
case "error":
return (
<div
key={segment.key}
style={{
margin: "10px 0",
padding: "8px",
color: "var(--vscode-errorForeground)",
border: "1px solid var(--vscode-editorError-foreground)",
borderRadius: "4px",
height: "128px",
overflow: "auto",
}}>
{segment.content}
</div>
)
default:
return <React.Fragment key={segment.key} />
}
}, [responseText, displayMode, isExpanded])
}
// Function to render content based on display mode
const renderContent = () => {
if (!isExpanded) {
return null // Don't render content if not expanded
return null
}
if (isLoading && displayMode === "rich") {
if (isLoading && mcpDisplayMode === "rich") {
return (
<div style={{ display: "flex", justifyContent: "center", alignItems: "center", height: "50px" }}>
<VSCodeProgressRing />
@@ -281,12 +185,14 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
)
}
// For plain text mode, just show the text
if (displayMode === "plain") {
if (mcpDisplayMode === "plain") {
return <UrlText>{responseText}</UrlText>
}
// Show error message if there was an error
if (mcpDisplayMode === "markdown") {
return <MarkdownBlock markdown={responseText} />
}
if (error) {
return (
<>
@@ -296,97 +202,9 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
)
}
// For rich display mode, show the text with embedded content
if (displayMode === "rich") {
// We already know displayMode is "rich" if we get here
// Create an array of text segments and embedded content
const segments: JSX.Element[] = []
let lastIndex = 0
let segmentIndex = 0
// Track embed count for logging
let embedCount = 0
// Add the text before the first URL
if (urlMatches.length === 0) {
segments.push(<UrlText key={`segment-${segmentIndex}`}>{responseText}</UrlText>)
} else {
for (let i = 0; i < urlMatches.length; i++) {
const match = urlMatches[i]
const { url, fullMatch, index } = match
// Add text segment before this URL
if (index > lastIndex) {
segments.push(
<UrlText key={`segment-${segmentIndex++}`}>{responseText.substring(lastIndex, index)}</UrlText>,
)
}
// Add the URL text itself
segments.push(<UrlText key={`url-${segmentIndex++}`}>{fullMatch}</UrlText>)
// Calculate the end position of this URL in the text
const urlEndIndex = index + fullMatch.length
// Add embedded content after the URL
// For images, use the ImagePreview component
if (match.isImage) {
segments.push(
<div key={`embed-image-${url}-${segmentIndex++}`}>
{/* Use formatUrlForOpening for network calls but preserve original URL in display */}
<ImagePreview url={formatUrlForOpening(url)} />
</div>,
)
embedCount++
// console.log(`Added image embed for ${url}, embed count: ${embedCount}`);
} else if (match.isProcessed) {
// For non-image URLs or URLs we haven't processed yet, show link preview
try {
// Skip localhost URLs
if (!isLocalhostUrl(url)) {
// Use a unique key that includes the URL to ensure each preview is isolated
segments.push(
<div key={`embed-${url}-${segmentIndex++}`} style={{ margin: "10px 0" }}>
{/* Already using formatUrlForOpening for link previews */}
<LinkPreview url={formatUrlForOpening(url)} />
</div>,
)
embedCount++
// console.log(`Added link preview for ${url}, embed count: ${embedCount}`);
}
} catch (e) {
console.log("Link preview could not be created")
// Show error message for failed link preview
segments.push(
<div
key={`embed-error-${segmentIndex++}`}
style={{
margin: "10px 0",
padding: "8px",
color: "var(--vscode-errorForeground)",
border: "1px solid var(--vscode-editorError-foreground)",
borderRadius: "4px",
height: "128px", // Fixed height
overflow: "auto", // Allow scrolling if content overflows
}}>
Failed to create preview for: {url}
</div>,
)
}
}
// Update lastIndex for next segment
lastIndex = urlEndIndex
}
// Add any remaining text after the last URL
if (lastIndex < responseText.length) {
segments.push(<UrlText key={`segment-${segmentIndex++}`}>{responseText.substring(lastIndex)}</UrlText>)
}
}
return <>{segments}</>
if (mcpDisplayMode === "rich") {
const segments = buildDisplaySegments(responseText, urlMatches)
return <>{segments.map(renderSegment)}</>
}
return null
@@ -405,16 +223,15 @@ const McpResponseDisplay: React.FC<McpResponseDisplayProps> = ({ responseText })
<span className={`codicon codicon-chevron-${isExpanded ? "down" : "right"} header-icon`}></span>
Response
</div>
<div style={{ minWidth: isExpanded ? "auto" : "0", visibility: isExpanded ? "visible" : "hidden" }}>
<ToggleSwitch onClick={(e) => e.stopPropagation()}>
<span className="toggle-label">{displayMode === "rich" ? "Rich Display" : "Plain Text"}</span>
<div
className={`toggle-container ${displayMode === "rich" ? "active" : ""}`}
onClick={toggleDisplayMode}>
<div className="toggle-handle"></div>
</div>
</ToggleSwitch>
</div>
<DropdownContainer
style={{ minWidth: isExpanded ? "auto" : "0", visibility: isExpanded ? "visible" : "hidden" }}>
<McpDisplayModeDropdown
value={mcpDisplayMode}
onChange={handleDisplayModeChange}
onClick={(e) => e.stopPropagation()}
style={{ minWidth: "120px" }}
/>
</DropdownContainer>
</ResponseHeader>
{isExpanded && <div className="response-content">{renderContent()}</div>}
@@ -1,6 +1,23 @@
import { WebServiceClient } from "@/services/grpc-client"
import { StringRequest } from "@shared/proto/common"
// Represents a URL found in the text with its position and metadata
export interface UrlMatch {
url: string // The actual URL
fullMatch: string // The full matched text
index: number // Position in the text
isImage: boolean // Whether this URL is an image
isProcessed: boolean // Whether we've already processed this URL (to avoid duplicates)
}
// Display segment interface
export interface DisplaySegment {
type: "text" | "url" | "image" | "link" | "error"
content: string
url?: string
key: string // Pre-computed key for React
}
// Safely create a URL object with error handling and ensure HTTPS
export const safeCreateUrl = (url: string): URL | null => {
try {
@@ -168,3 +185,224 @@ export const checkIfImageUrl = async (url: string): Promise<boolean> => {
console.log(`URL protocol not supported for image check: ${url}`)
return false
}
/**
* Extracts all valid URLs from the given text
* @param text - The text to search for URLs
* @param maxUrls - Maximum number of URLs to extract (default: 50)
* @returns Array of URL matches sorted by position in text
*/
export const extractUrlsFromText = (text: string, maxUrls: number = 50): UrlMatch[] => {
const matches: UrlMatch[] = []
const urlRegex = /(?:https?:\/\/|data:image)[^\s<>"']+/g
let urlMatch: RegExpExecArray | null
let urlCount = 0
while ((urlMatch = urlRegex.exec(text)) !== null && urlCount < maxUrls) {
const url = urlMatch[0]
// Skip invalid URLs
if (!isUrl(url)) {
console.log("Skipping invalid URL:", url)
continue
}
// Skip localhost URLs to prevent security issues
if (isLocalhostUrl(url)) {
console.log("Skipping localhost URL:", url)
continue
}
matches.push({
url,
fullMatch: url,
index: urlMatch.index,
isImage: false, // Will be determined later
isProcessed: false,
})
urlCount++
}
console.log(`Found ${matches.length} URLs in text`)
return matches.sort((a, b) => a.index - b.index)
}
/**
* Processes URLs to determine their types (e.g., image vs link)
* Processes URLs sequentially to avoid network flooding
* @param matches - Array of URL matches to process
* @param onProgress - Callback for progress updates with updated matches
* @param cancellationToken - Object to check if processing should be cancelled
* @returns Promise that resolves when processing is complete
*/
export const processUrlTypes = async (
matches: UrlMatch[],
onProgress: (updatedMatches: UrlMatch[]) => void,
cancellationToken: { cancelled: boolean },
): Promise<void> => {
console.log(`Starting sequential URL processing for ${matches.length} URLs`)
for (let i = 0; i < matches.length; i++) {
// Skip already processed URLs
if (matches[i].isProcessed) continue
// Check if processing has been canceled
if (cancellationToken.cancelled) {
console.log("URL processing canceled")
return
}
const match = matches[i]
console.log(`Processing URL ${i + 1} of ${matches.length}: ${match.url}`)
try {
// Check if URL is an image
const isImage = await checkIfImageUrl(match.url)
// Skip if processing has been canceled
if (cancellationToken.cancelled) return
// Update the match
match.isImage = isImage
match.isProcessed = true
// Notify progress with a new array to ensure React detects changes
onProgress([...matches])
} catch (err) {
console.log(`URL check error: ${match.url}`, err)
match.isProcessed = true
// Update state even on error
if (!cancellationToken.cancelled) {
onProgress([...matches])
}
}
// Delay between URL processing to avoid overwhelming the network
if (!cancellationToken.cancelled && i < matches.length - 1) {
await new Promise((resolve) => setTimeout(resolve, 100))
}
}
console.log(`URL processing complete. Found ${matches.filter((m) => m.isImage).length} image URLs`)
}
/**
* Orchestrates the URL extraction and processing pipeline
* @param text - The response text to process
* @param maxUrls - Maximum number of URLs to process
* @param onMatchesFound - Callback when initial URLs are extracted
* @param onMatchesUpdated - Callback when URL types are determined
* @param onError - Error handler callback
* @returns Cleanup function to cancel processing
*/
export const processResponseUrls = (
text: string,
maxUrls: number,
onMatchesFound: (matches: UrlMatch[]) => void,
onMatchesUpdated: (matches: UrlMatch[]) => void,
onError: (error: string) => void,
): (() => void) => {
const cancellationToken = { cancelled: false }
const process = async () => {
try {
// Extract URLs from text
const matches = extractUrlsFromText(text, maxUrls)
// Immediately notify about found matches
onMatchesFound(matches)
// Process URLs in the background
await processUrlTypes(matches, onMatchesUpdated, cancellationToken)
} catch (error) {
onError("Failed to process response content. Switch to plain text mode to view safely.")
}
}
// Start processing
process()
// Return cleanup function
return () => {
cancellationToken.cancelled = true
console.log("Cleaning up URL processing")
}
}
/**
* Builds an array of display segments from response text and URL matches
* @param responseText - The full response text
* @param urlMatches - Array of URL matches with their positions and types
* @returns Array of display segments describing how to render the content
*/
export const buildDisplaySegments = (responseText: string, urlMatches: UrlMatch[]): DisplaySegment[] => {
const segments: DisplaySegment[] = []
let lastIndex = 0
let segmentIndex = 0
// Handle case with no URLs
if (urlMatches.length === 0) {
return [
{
type: "text",
content: responseText,
key: "segment-0",
},
]
}
// Process each URL match
for (let i = 0; i < urlMatches.length; i++) {
const match = urlMatches[i]
const { url, fullMatch, index } = match
// Add text segment before this URL
if (index > lastIndex) {
segments.push({
type: "text",
content: responseText.substring(lastIndex, index),
key: `segment-${segmentIndex++}`,
})
}
// Add the URL text itself
segments.push({
type: "url",
content: fullMatch,
key: `url-${segmentIndex++}`,
})
// Add embedded content after the URL
if (match.isImage) {
segments.push({
type: "image",
content: url,
url: formatUrlForOpening(url),
key: `embed-image-${url}-${segmentIndex++}`,
})
} else if (match.isProcessed && !isLocalhostUrl(url)) {
segments.push({
type: "link",
content: url,
url: formatUrlForOpening(url),
key: `embed-${url}-${segmentIndex++}`,
})
}
// Update lastIndex for next segment
lastIndex = index + fullMatch.length
}
// Add any remaining text after the last URL
if (lastIndex < responseText.length) {
segments.push({
type: "text",
content: responseText.substring(lastIndex),
key: `segment-${segmentIndex++}`,
})
}
return segments
}
@@ -31,6 +31,7 @@ import { OllamaProvider } from "./providers/OllamaProvider"
import { ClaudeCodeProvider } from "./providers/ClaudeCodeProvider"
import { SapAiCoreProvider } from "./providers/SapAiCoreProvider"
import { BedrockProvider } from "./providers/BedrockProvider"
import { MoonshotProvider } from "./providers/MoonshotProvider"
import { NebiusProvider } from "./providers/NebiusProvider"
import { LiteLlmProvider } from "./providers/LiteLlmProvider"
import { VSCodeLmProvider } from "./providers/VSCodeLmProvider"
@@ -146,6 +147,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
<VSCodeOption value="lmstudio">LM Studio</VSCodeOption>
<VSCodeOption value="ollama">Ollama</VSCodeOption>
<VSCodeOption value="litellm">LiteLLM</VSCodeOption>
<VSCodeOption value="moonshot">Moonshot AI</VSCodeOption>
<VSCodeOption value="nebius">Nebius AI Studio</VSCodeOption>
<VSCodeOption value="asksage">AskSage</VSCodeOption>
<VSCodeOption value="xai">xAI</VSCodeOption>
@@ -241,6 +243,10 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
<OllamaProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "moonshot" && (
<MoonshotProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "nebius" && (
<NebiusProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
@@ -54,9 +54,9 @@ const featuredModels = [
label: "Trending",
},
{
id: "x-ai/grok-4",
description: "Latest flagship model from xAI",
label: "Fast & Cheap",
id: "moonshotai/kimi-k2",
description: "Open source model topping coding benchmarks",
label: "New",
},
]
@@ -32,28 +32,40 @@ export const BedrockProvider = ({ showModelOptions, isPopup }: BedrockProviderPr
gap: 5,
}}>
<VSCodeRadioGroup
value={apiConfiguration?.awsUseProfile ? "profile" : "credentials"}
value={apiConfiguration?.awsAuthentication ?? (apiConfiguration?.awsProfile ? "profile" : "credentials")}
onChange={(e) => {
const value = (e.target as HTMLInputElement)?.value
const useProfile = value === "profile"
handleFieldChange("awsUseProfile", useProfile)
handleFieldChange("awsAuthentication", value)
}}>
<VSCodeRadio value="credentials">AWS Credentials</VSCodeRadio>
<VSCodeRadio value="apikey">API Key</VSCodeRadio>
<VSCodeRadio value="profile">AWS Profile</VSCodeRadio>
<VSCodeRadio value="credentials">AWS Credentials</VSCodeRadio>
</VSCodeRadioGroup>
{apiConfiguration?.awsUseProfile ? (
{(apiConfiguration?.awsAuthentication === undefined && apiConfiguration?.awsUseProfile) ||
apiConfiguration?.awsAuthentication == "profile" ? (
<DebouncedTextField
initialValue={apiConfiguration?.awsProfile || ""}
onChange={(value) => handleFieldChange("awsProfile", value)}
key="profile"
initialValue={apiConfiguration?.awsProfile ?? ""}
style={{ width: "100%" }}
onChange={(value) => handleFieldChange("awsProfile", value)}
placeholder="Enter profile name (default if empty)">
<span style={{ fontWeight: 500 }}>AWS Profile Name</span>
</DebouncedTextField>
) : apiConfiguration?.awsAuthentication == "apikey" ? (
<DebouncedTextField
key="apikey"
type="password"
initialValue={apiConfiguration?.awsBedrockApiKey ?? ""}
style={{ width: "100%" }}
onChange={(value) => handleFieldChange("awsBedrockApiKey", value)}
placeholder="Enter Bedrock Api Key">
<span style={{ fontWeight: 500 }}>AWS Bedrock Api Key</span>
</DebouncedTextField>
) : (
<>
<DebouncedTextField
key="accessKey"
initialValue={apiConfiguration?.awsAccessKey || ""}
onChange={(value) => handleFieldChange("awsAccessKey", value)}
style={{ width: "100%" }}

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