Compare commits

...

200 Commits

Author SHA1 Message Date
celestial-vault 7c91440b15 use optimized e2e script 2025-07-19 12:50:20 -07:00
celestial-vault e82886d3a7 empty commit 2025-07-19 11:21:31 -07:00
celestial-vault 20239c5edd change cache path to check for vscode 2025-07-19 11:15:07 -07:00
celestial-vault 875cac4d4b remove extra logs 2025-07-19 11:03:13 -07:00
celestial-vault 74fef93b2c cache vscode and playwright downloads 2025-07-19 10:53:09 -07:00
Ara be2d416359 Updating Mintlify version (#5030)
* Updating Mintlify version

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

---------

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

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

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

* add in moonshotApiKey to settings-conversion after main merge

* fix type errors

* fix frontend types

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

* consolidate to Mode type

* remove promise resolve

* move huggingface provider into the new schema

* add new providers to migration function

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

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

The structure of the two packages should mirror each other

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

* Fix imports

* Fix imports

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

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

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

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

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

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

* Update warning message text

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

---------

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

* refactor to dedupe

* add changeset

* Update moody-crabs-relate.md

* minor comment tweaks

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

Add getDocumentText to the diff service.

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

* Check activeDiffEditorId

* Update src/integrations/editor/DiffViewProvider.ts

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

* Use await with this.saveDocument()

* Remove test file

---------

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

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

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

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

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

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

* change env name

* Update webview-ui/vite.config.ts

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

* update env location

---------

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

# Conflicts:
#	src/standalone/ExternalDiffviewProvider.ts

* Fix typo

* Add host bridge rpcs for the diff service

* Add saveDocument and closeDiff to the host bridge.

* Formatting

* Use the host bridge in ExternalDiffviewProvider

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

* Updating CHANGELOG.md format

* Update CHANGELOG.md for version 3.19.7

---------

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

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

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

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

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

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

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

* include provider id

* includes model and provider id

* fix test

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

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

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

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

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

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

* fix: Markdown lint

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

* fix: Typo

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

---------

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

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

On Vscode it reset the active diff editor and decorations.

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

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

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

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

* Update comment

* Use await for async function

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

This reverts commit f00c5f4ecc.

* leave terminal impls

* terminal manager switch for standalone

* Export standaloneTerminalManager to global

* clean up standalone terminal switch

---------

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

Move the current logic into the VscodeDiffViewProvider.

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

* Add comment

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

* frontend env config

* rename config

* preview update

* separate mcp link

* mcp url updated

* config change to local

* webview config to local
2025-07-16 22:53:08 -07:00
Toshii 9a4e3655b3 remove import (#4981) 2025-07-16 19:40:43 -07:00
Tomás Barreiro b18ad77539 Improve Claude Code errors and create Docs (#4968)
* Create docs

* Improve the Claude Code error messages

* Replace with remote image
2025-07-16 18:22:01 -07:00
github-actions[bot] c50fce8101 v3.19.6 Release Notes (#4976)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md for version 3.19.6

* more modifications

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: pashpashpash <nik@cline.bot>
2025-07-16 17:21:22 -07:00
Sarah Fortune 47f84d7c2c Improve generated code that registers the vscode host bridge handlers. (#4972) 2025-07-16 17:14:37 -07:00
Ara 17c57162cd Refactoring terminal process logic and edge cases for simplicity (#4673)
* Refactor terminal output capture for better reliability

- Extract emitCurrentTerminalContents() as a private method for reusability
- Improve handling of commands with no/delayed output by capturing actual terminal contents instead of generic messages
- Add multiple fallback attempts (100ms, 1s, 3s) for terminals without shell integration
- Enhance timeout comments to clarify the 3-second delay purpose
- Add .tool-versions file (likely for version management)

* Refactor TerminalProcess run method into smaller functions

Split the monolithic run() method into focused helper methods for better
maintainability and readability. Extracted initialization, shell integration
execution, stream processing, and cleanup logic into separate private methods.
This improves code organization without changing functionality.

* Fixing the handling of echo commands and changing the wording of the terminal capture line

* fix: Embracing the ffmpeg for opus support
2025-07-16 16:33:01 -07:00
Toshii 0f1f3d84f7 add auto refresh to accounts page (#4901)
* add auto refresh to accounts page

* update

* 10 sec
2025-07-16 16:14:18 -07:00
Saoud Rizwan 0c5832b7a2 Fix kimi k2 provider sorting (#4975)
* Fix kimi2 provider sorting

* Create smart-flies-eat.md
2025-07-16 16:11:12 -07:00
celestial-vault 11c4c58ed3 update the current task's consecutiveAutoApprovedRequestsCount on max request change (#4955) 2025-07-16 16:07:01 -07:00
pashpashpash 27bf78ddbd swapping latest diff algo as default (#4412)
* swapping latest diff algo as default

* updating

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-07-16 15:59:48 -07:00
pashpashpash c3161567d3 removing unneeded docs section (#4974) 2025-07-16 15:50:08 -07:00
pashpashpash 516d7bccc9 adding documentation workflow (#4598)
* adding documentation workflow

* Update .clinerules/workflows/writing-documentation.md

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

* good example

* good example

* language

* Update writing-documentation.md

* Update writing-documentation.md

* Update writing-documentation.md

* Update writing-documentation.md

* Update writing-documentation.md

* Update writing-documentation.md

* Update writing-documentation.md

* Update writing-documentation.md

* Update writing-documentation.md

* Update writing-documentation.md

* Update writing-documentation.md

* Update writing-documentation.md

* Update writing-documentation.md

* Update writing-documentation.md

---------

Co-authored-by: Cline Evaluation <cline@example.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-16 15:37:24 -07:00
pashpashpash e91c8208a3 fix streamlit dashboard to be compatible with both old and new versions of streamlit (#4922) 2025-07-16 15:28:02 -07:00
Sarah Fortune 0fb40527c8 Add a script to run the standalone service. (#4966)
* Add a script to run the standalone service.

Remove spammy log statements.

* Add comment

* Fix some vscode stubs

* Update scripts/runstandalone.sh

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

* Update scripts/runstandalone.sh

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

* Update scripts/runstandalone.sh

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

* Remove old script

* Add comment

* Add comment

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-07-16 15:12:52 -07:00
Sarah Fortune 8a2e90084d Change the port numbers for the ProtoBus service and HostBridge. (#4969)
Don't use the default gRPC port number because it's more likely to already be in use.
2025-07-16 11:28:07 -07:00
Sarah Fortune 4c3384988c open-file calls vscode.workspace.openTextDocument but doesn't use the result. (#4960) 2025-07-16 03:01:25 -07:00
Daniel Steigman 8539bdce24 Upgraded telemetry to capture each message turn separately (#4954)
* refactor: Ugraded telemetry to capture each message turn seperatly

* go back to the correct posthog key oops
2025-07-15 22:49:35 -07:00
Bee cd7f3ef6a7 Disable recording webview click events (#4709)
* Disable recording webview click events

Introduces a temporary measure to disable the recording of webview click events in PostHog. This is achieved by adding a `temporaryDisabled` flag that, when true, prevents the initialization of PostHog and stops the identification of users.

This change is intended to be temporary and should be reverted in a future commit by removing the `temporaryDisabled` flag.

* Use separate PostHog config for development environment

This commit introduces a separate PostHog project for the development environment. This allows us to track events in the development environment without polluting the production data.

The `posthogConfig` now uses `posthogDevEnvConfig` when `process.env.isDev` is true, and `posthogProdConfig` otherwise.

* process.env.IS_DEV

* Update src/shared/services/config/posthog-config.ts

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

* fix format

---------

Co-authored-by: Beatrix Woo <beatrix@cline.bot>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-07-15 22:46:58 -07:00
github-actions[bot] 80fbcda03b v3.19.5 Release Notes (#4941)
-   Add Groq as a new API provider with support for all Groq models including Kimi-K2
-   Add user role display in organization UI for Cline account users
-   Fix message dialogs not showing option buttons properly
-   Fix authentication issues when using multiple VSCode windows
2025-07-15 21:49:06 -07:00
pashpashpash cef79e06db not showing request id when insufficient balance (#4953)
* not showing request id when insufficient balance

* whoops

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

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

The changes include:

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

* wip: github workflow

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

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

* add @vscode/vsce as dev dep

* update workflow

* apply feedback

* fix test workflows

* add command palette helper

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

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

* state change

* set TEMP_PROFILE

* v3.18.7 Release Notes

* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md for version 3.18.7

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: pashpashpash <nik@cline.bot>

* Remove optimistic loading from organization dropdown (#4746)

* update build script to javascript

* fix match

* Add mode switching to chat test

* expected

---------

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

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

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

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

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

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

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

* Update import path

* Move the hostbridge handlers in the a hostbridge packge.

Move the hostbridge handlers out of the top level of the vscode package into their own subpackage.
I have to move all the vscode specific code into the `hosts/vscode` package, and I want the hostbridge handlers to be grouped together, not mixed in with things like the VscodeDiffViewProvider, VscodeWebviewProvider etc.
2025-07-15 16:12:48 -07:00
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
Sarah Fortune c38f443ec4 DiffViewProvider clean-up (#4835)
* In the DiffViewProvider, store the absolutePath instead of the cwd.

Remove unused var `scrollListener`.

* Remove cwd param

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

* Updating CHANGELOG.md format

* Update CHANGELOG.md for version 3.18.12

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: pashpashpash <nik@cline.bot>
2025-07-10 19:50:20 -07:00
pashpashpash 30a169e0c3 supporting buy_credits_url response from backend (#4823) 2025-07-10 19:33:25 -07:00
Ara 77ab7f8ef9 Fix the flaky Cline provider switching toggle (#4791)
* Fix Flaky Cline provider toggle

* More resiliency
2025-07-10 19:32:19 -07:00
akfoster 6d5ea98026 fix: insufficient credits display (#4821)
* fix: insufficient credits display

* add changeset

* Update src/api/providers/cline.ts

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

---------

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

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

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

* changeset

* move this._authService.getAuthToken() to ensureClient

* Request validation and remove credit balance for cline team

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

The changes include:

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

* changeset

* Update src/api/providers/cline.ts

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

---------

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

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

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

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

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

* changeset

* move this._authService.getAuthToken() to ensureClient

---------

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

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

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

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

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

* Updating CHANGELOG.md format

* Update CHANGELOG.md for version 3.18.9 with improved descriptions

* changeset version bump

* Updating CHANGELOG.md format

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

* ahugosaia'ohs'gasgh

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: pashpashpash <nik@cline.bot>
2025-07-10 00:05:06 -07:00
pashpashpash 268bbec7f1 recommend grok 4 (#4798)
* recommend grok 4

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

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

* format fix

---------

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

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

* Adding Thinking Token Config to Gemini 2.5 pro model

* Adding console logs

* Adding console logs

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

* Updating CHANGELOG.md format

* Update CHANGELOG.md for version 3.18.9 with improved descriptions

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: pashpashpash <nik@cline.bot>
2025-07-09 14:44:25 -07:00
pashpashpash ba41131b36 Pashpashpash/enabling streaming cline (#4784)
* grok 4

* fixing streaming in cline provider

* removing grok promo from cline provider

* changeset

* oopsie

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

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

* add changeset

---------

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

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

* Updating CHANGELOG.md format

* Update CHANGELOG.md for version 3.18.7

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: pashpashpash <nik@cline.bot>
2025-07-08 18:41:16 -07:00
Dennise Bartlett d838bcdc34 Fix account buttons (#4742)
* Update links for account buttons

* add dashboard url

* setup links for future changes

---------

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

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

* Organization dropdown tweaks (#4710)

* organization dropdown tweaks

* reverted AuthService

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

* Add Github Auth Flow

* Fix merge conflicts

* Fix some dumb typing

* recreate dropdown value when initialized (#4716)

* added changeset

* Update urls to production and handle some PR concerns

* Get user credits when signing in (#4736)

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

* swapped order of getUserCredits, made calls async

* async changes continued, moved setIsLoading to finally

---------

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

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

* update name

* tmp dir

* Implement in-memory storage for temporary profiles

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

* Refactor tmp user directory for dev launch config

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

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

---------

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

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

* Ignore the whole directory, not just the contents.

* Don't package .DS_Store files

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

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

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

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

* Create calm-ads-glow.md

* Update request header

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

Add ai-client-type header for tracking

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

Create little-lions-joke.md

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

update changeset

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

update

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

---------

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

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

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

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

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

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

---------

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

* Updating CHANGELOG.md format

* Update CHANGELOG.md for version 3.18.5

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Cline Evaluation <cline@example.com>
2025-07-07 19:12:41 -07:00
Ara d453eed582 feat: Globally persist plan/act mode across sessions and ensure proper workspace level persistance of chatsettings(for language) (#4646)
* feat: Persist plan/act mode across sessions

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

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

* Code Cleanup

* Code Cleanup

* Get the chatsettings back to global

* Get the chatsettings back to global

* Get the chatsettings back to global

---------

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

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

The changes involve:

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

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

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

* Organize imports

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

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

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

* reorganized sapaicore models to be grouped in logical groups

* updated changese with changes in SAP AI Core

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

* Updating CHANGELOG.md format

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update package.json

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-07-07 13:26:11 -07:00
celestial-vault 7ab6189595 mark welcomeViewCompleted as true after auth redirect (#4699)
* mark welcomeViewCompleted as true after auth redirect

* Create real-ravens-nail.md

---------

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

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

* Create calm-ads-glow.md

---------

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

* addressing comments

---------

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

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

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

* Remove all .bak files from src/integrations
2025-07-03 15:36:21 -07:00
Sarah Fortune 9578d7cde1 Instead of getting the cwd from the workspaceFolders use the host bridge util getCwd() (#4647)
Remove top-level property cwd task, await can't be used at the top level.
Make cwd a class property, and pass the cwd into the constructor of task (await can't be used in the constructor either).
2025-07-03 15:36:08 -07:00
Sarah Fortune 2979d47e01 Replace cwd from the workspaceFolders with the host bridge. (#4645)
Don't export cwd from task/index.ts. This top-level property cwd will be removed in a following PR because await cannot be used at the top-level.
Use the hostbridge util getCwd() in createRuleFile.ts and refreshRules.ts instead of import the cwd from `task`.
Add a util function to get the desktop directory instead of constructing it multiple places, update uses with the new function getDesktopDir()
Replace function getCwd in FileContextTracker.ts with just getCwd from paths.ts.
2025-07-03 15:08:52 -07:00
github-actions[bot] 4569300f00 v3.18.3 Release Notes (#4644)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md for version 3.18.3

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Cline Evaluation <cline@example.com>
2025-07-03 15:06:57 -07:00
Toshii 36e3f4cdd3 add log + run options (#4630)
* log + options

* changeset

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-07-03 14:54:10 -07:00
Tomás Barreiro bc5225ce52 Improve Claude Code handling (#4619)
* Prevent filling the chat with error messages

* Improve env variables and remove the magic number

* Add changeset

* refactor

* Update run.ts

---------

Co-authored-by: Ara <arafat.da.khan@gmail.com>
2025-07-03 14:41:59 -07:00
Kevin Taylor 50dc89b551 Strip thinking tokens from Cerebras reasoning model inputs (#4635)
* Strip thinking tokens from Cerebras reasoning model inputs

Filter out thinking tokens in message history

* changeset

* changeset

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-07-03 14:40:58 -07:00
celestial-vault d576b68cca Move plan/act model settings to global storage (#4636)
* move model settings to global storage

* cleanup

* only run migration if globalState key is undefined
2025-07-03 16:24:14 -05:00
github-actions[bot] 6c2c0780ee v3.18.2 Release Notes (#4581) 2025-07-02 22:15:45 -07:00
pashpashpash 021a014012 setting claude 4 as best model (#4637)
Co-authored-by: Cline Evaluation <cline@example.com>
2025-07-02 21:45:30 -07:00
Ara ef1a68b5e3 Adding a troubleshooting guide for terminal problems (#4592) 2025-07-02 19:08:42 -07:00
Tomás Barreiro 72029fe205 feat: Introduce Thinking Budget customization for Claude Code (#4618) 2025-07-03 06:37:32 +05:30
celestial-vault 2af151e736 [SettingsView] [ApiConfig Section] save on change (#4554)
* refactor out apiconfig section

* add general settings section

* duplicate import

* move terminal, browser, and feature settings

* move files to sections folder

* refactor out debug section

* pull out about section

* implement save on change and remove confirmation modals for api config section

* add doc strings to new hook functions

* add debounced text field for smooth typing

* use context value directly for apiProvider dropdown value; remove unecessary memo; remove keys from ApiOptions

* add welcomeViewCompleted state boolean to control welcome view showing

* refactor other sections to save-on-change; remove form diff calculation logic

* cleanup

* remove memo

* make welcomeViewCompleted context value initial value false
2025-07-02 19:39:49 -05:00
canvrno 64963c4e9c confirmation popup when deleting tasks (#4627) 2025-07-01 22:41:58 -07:00
Toshii 52571ccee8 base (#4600) 2025-07-01 17:08:28 -07:00
Saoud Rizwan 87322feeb7 Fall back to getting current terminal content when shell integration API fails to return output (#4605)
* Revert to when terminal process worked more reliably

* Get last terminal output if no output is retrieved

* Fix getting terminal output for when shell integration unavailable

* Apply suggestions from code review

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

* Revert removing previous changes

* Update TerminalProcess to emit current terminal contents instead of a silent command completion message

* Revert when first chunk fails message

* Revert error title

* Fixing tests with fake timers

---------

Co-authored-by: Dennise Bartlett <bartlett.dc.1@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Cline Evaluation <cline@example.com>
2025-07-01 16:57:36 -07:00
kevinneung 27f8372c5b add cline.walkthrough to command handler (#4621)
added cline.walkthrough to command handler, since it would throw an error otherwise
2025-07-02 05:20:02 +05:30
Sarah Fortune b3d3e9861f Use the host bridge to get the cwd in autoApprove.ts (#4601)
Update the ToolExecutor to use await because the approval is now async.
2025-07-01 16:01:33 -07:00
pashpashpash 01178909ee adding unique remote git urls to context on first message env variables (#4622)
Co-authored-by: Cline Evaluation <cline@example.com>
2025-07-01 15:15:10 -07:00
Sarah Fortune c982216113 Cleanup: Remove unused property and param postMessage from ClineAccountService (#4620)
* Remove unused property and param postMessage

* Remove unused property and param postMessage

Remove unused import.
2025-07-01 14:44:41 -07:00
Sarah Fortune da6f705df2 Remove unused file get-python-env.ts (#4604) 2025-07-01 14:06:14 -07:00
Ara c7548a7f52 Fixing Bugs in ChatView with Primary/Secondary Buttons and related issues (#4553)
* Splitting chat view into multiple modular files

* Adding Comments and removing redundancies

* Fixing Bugs in ChatView with Primary/Secondary Buttons and related issues

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-07-01 12:58:18 -07:00
Tomás Barreiro e5f78a0456 Do not read auth variables from the user env when using Claude Code (#4591) 2025-07-01 12:09:58 +05:30
Sarah Fortune 4b5b090b29 Remove unused param cwd (#4602) 2025-06-30 20:28:40 -07:00
Sarah Fortune 559eba5dd1 Cleanup: Remove unused params from the ToolExecutor constructor. (#4588)
Organize imports.
2025-06-30 18:55:19 -07:00
Andrei Eternal c013b4f329 [PROTOBUS] showTextDocument host bridge (#4586)
* showTextDocument host bridge

* format fix

* sjf review cleanups

* prefer paths over URI based on chats with sjf

* also document_uri -> document_path

* remove unused metadata import

---------

Co-authored-by: Andrei Edell <andrei@nugbase.com>
2025-06-30 18:39:09 -07:00
Jorge García Rey dcf91b081e [LiteLLM] Group multiple requests into a single session using litellm_session_id parameter (#4457)
* feat: add litellm_session_id as part of chat completion request

Signed-off-by: Jorge García Rey <jorgegar@inditex.com>

* chore: add changeset

Signed-off-by: Jorge García Rey <jorgegar@inditex.com>

---------

Signed-off-by: Jorge García Rey <jorgegar@inditex.com>
2025-06-30 17:55:22 -07:00
pashpashpash 9ab0cc7648 deleting mermaid prompts (#4596)
Co-authored-by: Cline Evaluation <cline@example.com>
2025-06-30 17:52:25 -07:00
pashpashpash add572cc12 fixing closing telemetry banner (#4597)
Co-authored-by: Cline Evaluation <cline@example.com>
2025-06-30 17:37:28 -07:00
pashpashpash 307b92862b deprecating latex support (#4595)
Co-authored-by: Cline Evaluation <cline@example.com>
2025-06-30 17:06:58 -07:00
Toshii 020ae3006e adding distance metric safety check to the block-apply, for eval (#4477)
* diff apply add

Co-authored-by: chi.cat <git@chi.cat>

* fname

* integrate

* optionally save locally

* other cli

* name

* default

* return replacements with match type

* baseline -1

* baseline for evals

* work

* more info

---------

Co-authored-by: chi.cat <git@chi.cat>
2025-06-30 16:11:31 -07:00
Sarah Fortune c6d91be721 Replace vscode workspaceFolders with getCwd which uses the host bridge. (#4593) 2025-06-30 16:07:48 -07:00
Sarah Fortune 4d2fec787e Replace uses of workspaceFolders from the vscode SDK with the host bridge (#4589)
* Switch openMention to use getCwd instead of using the vscode SDK directly.

* Use getCwd in services/test/TestServer.ts instead of using the vscode SDK.

Don't get the cwd from the controller extension context global state,
use the workspace folder as the rest of the codebase is doing.

Replace GitHelper.getWorkspacePath with utils.getCwd()

* Use host bridge getWorkspacePaths in services/test/TestMode.ts

-instead of using the vscode SDK.
Remove unsed param `context`

* Remove unused file CheckpointTracker-old.ts

* Use hostbridge getCwd in CheckpointUtils.ts and message-state.ts
2025-06-30 14:54:36 -07:00
Sarah Fortune c8b05cdf9c Remove unused property postMessageToWebview from Task (#4587)
Cleanup imports.
2025-06-30 14:03:41 -07:00
Ara 9e69576fb0 Splitting chat view into multiple modular files (#4569)
* Splitting chat view into multiple modular files

* Adding Comments and removing redundancies

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-06-30 13:55:25 -07:00
Toshii e6760ed6cc race (#4583) 2025-06-30 11:33:00 -07:00
celestial-vault 27a531c86a [SettingsView.tsx] Extract About Section (#4537)
* refactor out apiconfig section

* add general settings section

* duplicate import

* move terminal, browser, and feature settings

* move files to sections folder

* refactor out debug section

* pull out about section
2025-06-30 10:47:18 -07:00
Lize Cai 09c773bd5f Add Claude 4 model to SAP AI Core Provider (#4418)
* add claude 4 support to sap aicore

Signed-off-by: Lize Cai <lize.cai@sap.com>

* add changelog

Signed-off-by: Lize Cai <lize.cai@sap.com>

* update model-utils to capture other naming for claude 4.

Signed-off-by: Lize Cai <lize.cai@sap.com>

* add claude 4 opus as well

Signed-off-by: Lize Cai <lize.cai@sap.com>

---------

Signed-off-by: Lize Cai <lize.cai@sap.com>
2025-06-30 10:24:45 -07:00
github-actions[bot] 037a781a6d v3.18.1 Release Notes
-   Add support for Claude 4 Sonnet in SAP AI Core provider (Thanks @GTxx!)
-   Fix ENAMETOOLONG error when using Claude Code provider with long conversation histories (Thanks @BarreiroT!)
-   Remove Gemini CLI provider because Google asked us to
-   Fix bug with "Delete All Tasks" functionality
2025-06-28 18:38:32 -07:00
Ara 968b46a179 Revert "Adding Gemini CLI Provider with Oauth support (#4472)" (#4556)
* Revert "Adding Gemini CLI Provider with Oauth support (#4472)"

This reverts commit 890148407a.

* revert

* revert

* revert

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-06-28 18:20:32 -07:00
Toshii 5a777f28b6 fix browser row in chat view to prevent constant rerendering (#4542)
* base

* browser 2
2025-06-28 10:12:51 -07:00
Toshii fe9e5bff74 base (#4539) 2025-06-28 00:27:13 -07:00
379 changed files with 24709 additions and 37096 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Fix ENAMETOOLONG when calling Claude Code
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
fix: mcp servers are not started when disabled
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Support Sonnet-4 in SAP AI Core provider
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Removed deleteNonFavoritedTasks, moved popup to extension, cleaned up deletion logic
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
host bridge migration - clipboard
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Refactor task class, moving auto approve
@@ -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
@@ -0,0 +1,392 @@
# General writing guide
# How I want you to write
I'm gonna write something technical.
It's often less about the nitty-gritty details of the tech stuff and more about learning something new or getting a solution handed to me on a silver platter.
Look, when I read, I want something out of it. So when I write, I gotta remember that my readers want something too. This whole piece? It's about cluing in anyone who writes for me, or wants me to write for them, on how I see this whole writing product thing.
I'm gonna lay out a checklist of stuff I'd like to have. It'll make the whole writing gig a bit smoother, you know?
## Crafting Compelling Titles
I often come across titles like "How to do X with Y,Z technology." These don't excite me because X or Y are usually unfamiliar unless they're already well-known. Its rarely the dream to use X unless X is the dream.
My dream isnt to use instructor, its to do something valueble with the data it extracts
An effective title should:
- Evoke an emotional response
- Highlight someone's goal
- Offer a dream or aspiration
- Challenge or comment on a belief
- Address someone's problems
I believe it's more impactful to write about specific problems. If this approach works, you can replicate it across various scenarios rather than staying too general.
- Time management for everyone can be a 15$ ebook
- Time management for executives is a 2000$ workshop
Aim for titles that answer questions you think everyone is asking, or address thoughts people have but can't quite articulate.
Instead of "How I do something" or "How to do something," frame it from the reader's perspective with "How you can do something." This makes the title more engaging. Just make sure the difference is advisory if the content is subjective. “How I made a million dollars” might be more reasonable than “How to make a million dollars” since you are the subject and the goal might be to share your story in hopes of helping others.
This approach ultimately trains the reader to have a stronger emotional connection to your content.
- "How I do X"
- "How You Can do X"
Between these two titles, it's obvious which one resonates more emotionally.
You can take it further by adding specific conditions. For instance, you could target a particular audience or set a timeframe:
- How to set up Braintrust
- How to set up Braintrust in 5 minutes
## NO adjectiives
I want you to almost always avoid adjectives and try to use evidence instead. Instead of saying "production ready," you can write something like "scaling this to 100 servers or 1 million documents per second." Numbers like that will tell you exactly what the specificity of your product is. If you have to use adjectives rather than evidence, you are probably making something up.
There's no reason to say something like "blazingly fast" unless those things are already known phrases.
Instead, say "200 times faster" or "30% faster." A 30% improvement in recommendation system speed is insane.
There's a 200 times performance improvement because we went from one programming language to another. It's just something that's a little bit more expected and understandable.
Another test that I really like using recently is tracking whether or not the statements you make can be:
- Visualized
- Proven false
- Said only by you
If you can nail all three, the claim you make will be more likely to resonate with an audience because only you can say it.
Earlier this year, I had an example where I embedded all of Wikipedia in 17 minutes with 20 bucks, and it got half a million views. All we posted was a video of me kicking off the job, and then you can see all the log lines go through. You see the number of containers go from 1 out of 50 to 50 out of 50.
It was easy to visualize and could have been proven false by being unreproducible. Lastly, Modal is the only company that could do that in such an effortless way, which made it unique.
## Keep It Digestible
- Aim for 5-minute reads
- Write at a Grade 10 reading level
- Break up long paragraphs
- Use headers and bullet points
## Make It Scannable
- Bold key points
- Use subheadings every 3-4 paragraphs
- Include plenty of white space
- Add relevant examples
This structure works whether you're writing a tweet thread or a full blog post. The key is making complex ideas accessible.
# Guide to Writing Cline Documentation
## Some general principles for explaining features
If you're talking about a feature, it's helpful to start with a human-readable explanations that cover what the feature is in simple terms. Skip jargon and explain it like you're talking to someone who's never seen it before. This sets the foundation for everything that follows.
Combine location and usage into one flowing section. Tell users exactly where to find the feature and how to use it, but weave the instructions into natural prose with a good balance of bullet points, numbered lists, code examples (if applicable), mintlify components, and headers/subheaders. Users shouldn't have to jump between separate "where is it" and "how do I use it" sections.
Show the feature in action with real examples like actual files, workflows, or code. Users need to see concrete implementations, not just abstract descriptions. This is where understanding turns into practical knowledge.
When talking about a feature, include an inspiration section that sparks imagination. This section pushes people from understanding to action by showing them what becomes possible when they use this feature creatively. It's what separates good documentation from great documentation.
## Writing Principles That Actually Work
### Write for Action, Not Just Understanding
Documentation should motivate users to try things. Instead of just explaining how something works, focus on what users can accomplish with it. The inspiration section is crucial - it's what transforms passive readers into active users.
### Create a Natural Story Flow
It should feel like a conversation that naturally progresses from "what is this?" to "how do I use it?" to "here's a real example" to "imagine what you could do with this."
### Show Real Examples, Not Toy Demos
Provide actual workflow files, real code snippets, and concrete implementations that users can copy and adapt. Abstract examples don't help anyone - users want to see exactly what they'll be working with.
### Keep It Scannable But Not Fragmented
Write in prose that flows naturally when read completely, but structure it so users can quickly find specific information when they're troubleshooting. Avoid dense walls of text, but also avoid over-formatting with excessive bullet points and bold headers. There should be a nice visual heirarchy of balance between all elements, so you can quickly scan the page and find what you're looking for.
## Language and Tone Guidelines
Write clearly without dumbing things down. Use simple language when possible, but don't avoid technical terms that users need to know. Explain concepts in terms of what users can achieve rather than how the software works internally.
Make your writing conversational and encouraging. Phrases like "you can also try" or "when that works" feel more natural than rigid instructional language. Help users feel confident about trying new things.
Keep content concise and purposeful. Every sentence should either help users understand something or help them do something. If it doesn't serve one of those purposes, cut it.
Build in context and reasoning. Users want to understand why they're doing something, not just what to do. This builds confidence and helps them troubleshoot when things don't work exactly as expected.
## Practical Implementation
Structure each feature page consistently with the four-section approach, but let the content flow naturally within that structure. Use visual assets like videos and screenshots to complement the written content - they often communicate more effectively than paragraphs of description.
Link generously to related resources, examples, and deeper documentation. Users should never feel stuck or wonder where to go next. Maintain a repository of real examples that users can reference and adapt to their own needs.
The goal is documentation that feels more like helpful guidance from an experienced colleague than a technical manual. Users should finish reading feeling excited about what they can accomplish, not just informed about what the feature does.
## Balance Structure with Flexibility
While they discuss having consistent documentation structure, there's also mention of making content feel less rigid and more natural. The writing should follow guidelines while still feeling conversational and engaging.
## Bad examples
I personally hate this pattern of bullet point **Bold Text** colon and then more text:
<bad_example_of_writing>
#### macOS
1. **Switch to bash**: Go to Cline Settings → Terminal → Default Terminal Profile → Select "bash"
2. **Disable Oh-My-Zsh temporarily**: If using zsh, try `mv ~/.zshrc ~/.zshrc.backup` and restart VSCode
3. **Set environment**: Add to your shell config: `export TERM=xterm-256color`
#### Windows
1. **Use PowerShell 7**: Install from Microsoft Store, then select it in Cline settings
2. **Disable Windows ConPTY**: VSCode Settings → Terminal Integrated: Windows Enable Conpty → Uncheck
3. **Try Command Prompt**: Sometimes simpler is better - switch to cmd.exe
#### Linux
1. **Use bash**: Most reliable option - select in Cline settings
2. **Check permissions**: Ensure VSCode has terminal access permissions
3. **Disable custom prompts**: Comment out prompt customizations in `.bashrc`
</bad_example_of_writing>
We should instead strive to write beautiful docs that read well. We can use bullet points and numbered lists but it should read naturally and be delightful to look at hierachally when scanning through the doc. There should be a good balance between blocks of text, code snippets, paragraphs, numbered lists, and bullet points. When scanning the documentation visually, you should feel like you're adminiring a tasteful art piece.
<good_example_of_writing>
#### macOS
The most common fix is switching to bash. Navigate to Cline Settings → Terminal → Default Terminal Profile and select "bash" from the dropdown.
If you're still having issues, Oh-My-Zsh might be interfering with terminal integration. Try temporarily disabling it:
- Run `mv ~/.zshrc ~/.zshrc.backup`
- Restart VSCode
You can also add `export TERM=xterm-256color` to your shell configuration file to improve compatibility.
#### Windows
PowerShell 7 provides the most reliable experience. Install it from the Microsoft Store, then select it in your Cline settings.
Still seeing problems? Try these solutions:
- Disable Windows ConPTY: VSCode Settings → Terminal Integrated: Windows Enable Conpty → uncheck
- Switch to Command Prompt (cmd.exe) - sometimes simpler shells work better
#### Linux
Bash is your most dependable option. Select it in Cline settings if you haven't already.
Check these common issues:
- Ensure VSCode has terminal access permissions
- Temporarily comment out custom prompt configurations in your `.bashrc`
</good_example_of_writing>
This is much more natural to read. Writing this way creates a conversational flow, and bullet points are used idiomatically.
# Using Mintlify Components Idiomatically
Mintlify's custom components can transform basic documentation into engaging, scannable content that users actually want to read. Here's how to use them effectively.
## Visual Content with Frames
Videos and images should be wrapped in `<Frame>` components rather than using raw HTML or markdown. This creates consistent styling and proper responsive behavior.
For videos, embed them directly rather than linking externally. Users are much more likely to watch a 30-second demonstration than click through to another platform:
```jsx
<Frame>
<iframe
style={{ width: "100%", aspectRatio: "16/9" }}
src="https://www.youtube.com/embed/your-video-id"
title="Feature demonstration"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen
/>
</Frame>
```
Screenshots work similarly - the frame provides visual polish and consistency:
```jsx
<Frame>
<img src="/path/to/screenshot.png" alt="Descriptive alt text" />
</Frame>
```
## Cards for Navigation and Overview
Cards excel at creating scannable overviews that link to detailed documentation. They're perfect for feature listings, getting started guides, or any section where users need to choose their path.
Use the two-column layout for related features:
```jsx
<Columns cols={2}>
<Card title="Feature Name" icon="relevant-icon" href="/link/to/docs">
Brief description that explains what this feature does and why someone would use it.
</Card>
<Card title="Related Feature" icon="another-icon" href="/another/link">
Another concise explanation that helps users understand the value proposition.
</Card>
</Columns>
```
The key is writing card descriptions that are informative enough to help users decide whether to click through, but concise enough to scan quickly. Each card should answer "what does this do?" and "why would I need this?"
## Tips and Notes for Context
Use `<Tip>` components for helpful information that enhances the main content without cluttering it:
```jsx
<Tip>
Pro tip: You can combine multiple @ mentions in a single message to give Cline
comprehensive context about your issue.
</Tip>
```
`<Note>` components work well for important caveats or technical limitations:
```jsx
<Note>
Due to VS Code limitations, some features require specific settings to work properly.
</Note>
```
`<Info>` is also cool:
<Info>
**Quick Fix**: If you're experiencing terminal issues, try switching to a simpler shell like `bash` in the Cline settings.
This resolves 90% of terminal integration problems.
</Info>
**Never** fall into that awful **Bold Text** - description pattern that we specifically identified as bad writing. The content should flow naturally as connected thoughts rather than feeling like a templated AI response with forced formatting.
## When to Use Bullet Points and Numbered Lists Strategically
Bullet points serve functional purposes - use them for:
**Sequential actions or troubleshooting steps** where users need to follow a specific order:
1. Install the extension
2. Restart VSCode
3. Check the settings panel
**Lists of related options** where users need to choose one approach:
- Try PowerShell 7 for the most reliable experience
- Switch to Command Prompt if you're still having issues
- Use WSL Bash for Linux compatibility
**Quick reference items** that users might need to scan quickly when problem-solving.
**Improving Visual Hierarchy** when there's a wall of text - that's a good time to introduce bullet points or numbered lists.
Each bulleted item or numbered list should be a discrete action or piece of information that benefits from being visually separated. This is a key weapon you can employ when going for that artwork experience I mentioned earlier.
<good_example_of_bullet_points>
## Finding and Configuring Terminal Settings
You can access Cline's terminal settings by clicking the settings icon in the Cline sidebar, then navigating to the Terminal section. These settings control how Cline interacts with your system's terminal.
- The **Default Terminal Profile** setting determines which shell Cline uses for executing commands. If you're experiencing issues, this is usually the first thing to change. I personally keep this set to `bash` on all my systems because it's the most reliable option, even though I use `zsh` for my regular terminal work.
- **Shell Integration Timeout** controls how long Cline waits for the terminal to become ready. The default is 4 seconds, but if you have a heavy shell configuration (lots of plugins, slow startup scripts), you might need to increase this to 10 or even 15 seconds. I've found that WSL environments and SSH connections often need longer timeouts.
- The **Enable Aggressive Terminal Reuse** setting determines whether Cline tries to reuse existing terminals even when they're not in the correct directory. When this causes problems (commands running in the wrong directory, virtual environment issues), disabling it creates more terminal instances but ensures each command runs in a clean state.
- **Terminal Output Line Limit** sets how many lines of output Cline will read from commands. The default of 500 lines works for most cases, but you might need to adjust it. For verbose build outputs, increase it. For commands with progress bars that spam thousands of lines, decrease it to around 100 to avoid consuming too many tokens.
</good_example_of_bullet_points>
## Write Like a Human, Not an AI
Keep it short and direct. If you can say something in fewer words, do it. Long explanations often confuse more than they help.
Use normal sentence lengths. Mix short and long sentences naturally, like you would when talking to someone. Avoid those meandering compound sentences that go on forever.
Cut the corporate speak. Instead of "utilize," say "use." Instead of "in order to," just say "to." Write like you're explaining something to a colleague, not drafting a press release.
Don't over-explain obvious things. If users are reading documentation, they probably understand basic concepts. Respect their intelligence.
## Never use em dashes or emojis
That's pretty much it. Never use them. Only AI writes with em dashes or emojis.
# Anthropomorphizing Cline
When referring to Cline, always call him a "him" not an "it".
Bad example:
- When Cline cant execute commands or read their output, you lose access to one of its most powerful capabilities.
Good Example:
- When Cline cant execute commands or read their output, you lose access to one of his most powerful capabilities.
# Using "I" when sharing your workflow
Adding a personal touch goes a long way. There are great examples in the docs currently where I use "I" to share how I personally use cline, from dev to dev. It's a great technique.
# Crosslinking relevant documentation pages
Make sure you crosslink when you're done writing the docs. If there are relevant docs, just link to them.
# Brevity is the soul of wit
Don't ramble if you don't need to. Use bullet points and numbered lists. Keep things easy to read.
<bad_example>
When Cline can't execute commands or read their output, you lose access to one of his most powerful capabilities. Terminal integration problems are frustrating, but they're usually fixable with a few simple changes.
## The Most Common Problem: Shell Integration Issues
If you're seeing "Shell integration unavailable" or Cline isn't getting command output, the issue is almost always your shell configuration. Complex shell setups with custom prompts, plugins, and fancy configurations can interfere with VSCode's terminal integration.
**Switch to bash first.** This fixes the problem 90% of the time. Navigate to Cline Settings → Terminal → Default Terminal Profile and select "bash" from the dropdown. Restart VSCode after making this change.
Still having issues? Try increasing the shell integration timeout. Go to Cline Settings → Terminal → Shell Integration Timeout and change it from 4 seconds to 10 seconds. Heavy shell configurations need more time to initialize properly.
If commands are running in the wrong directories or you're seeing weird behavior, disable aggressive terminal reuse. In Cline Settings → Terminal, uncheck "Enable aggressive terminal reuse." This creates more terminal instances but ensures each command runs in a clean environment.
</bad_exaxmple>
The first part is total filler, useless to any serious developer. You can tell it's written by a non technical person that doesn't value clean, straightforward information.
<good_example>
## Shell Integration Issues
If you're seeing "Shell integration unavailable" or Cline can't read command output, your shell configuration is interfering with VSCode's terminal integration.
**Switch to bash first.** Go to Cline Settings → Terminal → Default Terminal Profile and select "bash." This fixes 90% of problems.
Still broken? Try these:
- Increase shell integration timeout to 10 seconds in Cline Settings → Terminal
- Disable "aggressive terminal reuse" if commands run in wrong directories
- Restart VSCode after making changes
</good_example>
The good version cuts straight to the problem and solution. No hand-holding, no emotional language about frustration, just the facts: what's wrong, how to fix it, what to try next. Respects that developers want information, not sympathy.RetryClaude can make mistakes. Please double-check responses.
ALWAYS consider your audience. And your audience is devs who don't want their time wasted. Give them the info. I cannot stress this enough. Use bullet points and numbered lists. Prose is good, but every word should actually mean something to the dev reading it.
# Lastly, before you start writing docs
1. Internalize these guidelines. I mean it.
2. Read `docs/docs.json` and get an understanding of the structure of the docs. This will come in handly at the end when you're doing a final pass so you can cross link to docs where relevant.
3. Read some good examples that I personally wrote and am proud of:
- docs/features/slash-commands/workflows.mdx
- docs/features/slash-commands/new-task.mdx
- docs/features/at-mentions/overview.mdx
- docs/features/drag-and-drop.mdx
4. If the user specifies any other instructions make sure you follow them.
+1
View File
@@ -22,6 +22,7 @@
"react-hooks/exhaustive-deps": "off",
"eslint-rules/no-protobuf-object-literals": "error",
"eslint-rules/no-grpc-client-object-literals": "error",
"eslint-rules/no-direct-vscode-api": "warn",
"no-restricted-syntax": [
"error",
{
+108
View File
@@ -0,0 +1,108 @@
name: E2E Tests
on:
push:
branches:
- main
pull_request:
types: [opened, reopened, synchronize, ready_for_review]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
matrix_prep:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- id: set-matrix
run: |
echo 'matrix=[{"runner":"ubuntu"},{"runner":"windows"},{"runner":"macos"}]' >> $GITHUB_OUTPUT
e2e:
needs: matrix_prep
strategy:
fail-fast: false
matrix:
include: ${{ fromJson(needs.matrix_prep.outputs.matrix) }}
runs-on: ${{ matrix.runner }}-latest
timeout-minutes: 20
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: 22
# Cache root dependencies - only reuse if package-lock.json exactly matches
- name: Cache root dependencies
uses: actions/cache@v4
id: root-cache
with:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
# Cache webview-ui dependencies - only reuse if package-lock.json exactly matches
- name: Cache webview-ui dependencies
uses: actions/cache@v4
id: webview-cache
with:
path: webview-ui/node_modules
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
# Cache VS Code installation
- name: Cache VS Code
uses: actions/cache@v4
id: vscode-cache
with:
path: .vscode-test
key: vscode-${{ runner.os }}-stable-${{ hashFiles('.vscode-test.mjs', 'package.json') }}
restore-keys: |
vscode-${{ runner.os }}-stable-
# Cache Playwright browsers
- name: Cache Playwright browsers
uses: actions/cache@v4
id: playwright-cache
with:
path: |
~/.cache/ms-playwright
~/Library/Caches/ms-playwright
~/AppData/Local/ms-playwright
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
restore-keys: |
playwright-browsers-${{ runner.os }}-
- name: Install root dependencies
if: steps.root-cache.outputs.cache-hit != 'true'
run: npm ci
- name: Install webview-ui dependencies
if: steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci
- name: Install xvfb on Linux
if: matrix.runner == 'ubuntu'
run: sudo apt-get update && sudo apt-get install -y xvfb
# Run optimized E2E tests (eliminates redundant builds)
- name: Run E2E tests - Linux
if: matrix.runner == 'ubuntu'
run: xvfb-run -a npm run test:e2e:optimal
- name: Run E2E tests - Non-Linux
if: matrix.runner != 'ubuntu'
run: npm run test:e2e:optimal
- uses: actions/upload-artifact@v4
if: ${{ failure() }}
with:
name: playwright-recordings-${{ matrix.runner }}
path: |
test-results/playwright/
+1
View File
@@ -94,6 +94,7 @@ jobs:
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
OVSX_PAT: ${{ secrets.OVSX_PAT }}
CLINE_ENVIRONMENT: production
run: |
# Required to generate the .vsix
vsce package --out "cline-${{ steps.get_version.outputs.version }}.vsix"
+4
View File
@@ -68,6 +68,10 @@ jobs:
if: steps.webview-cache.outputs.cache-hit != 'true'
run: cd webview-ui && npm ci
- name: Install xvfb on Linux
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y xvfb
- name: Install local modules on windows
if: runner.os == 'Windows' && steps.root-cache.outputs.cache-hit == 'true'
run: |
+5 -10
View File
@@ -7,6 +7,7 @@ tmp
*.vsix
.DS_Store
.idea
pnpm-lock.yaml
@@ -21,20 +22,14 @@ coverage
*evals.env
# Generated files
## Generated files ##
src/generated/
# Core
src/core/controller/*/methods.ts
src/core/controller/*/index.ts
src/core/controller/grpc-service-config.ts
# Shared
src/shared/proto/*.ts
src/shared/proto/host/*.ts
# Webview
webview-ui/src/services/grpc-client.ts
# Host bridge
src/hosts/vscode/*/methods.ts
src/hosts/vscode/*/index.ts
src/hosts/vscode/client/host-grpc-client.ts
src/hosts/vscode/host-grpc-service-config.ts
src/standalone/server-setup.ts
# E2E Tests
test-results
+3 -1
View File
@@ -5,4 +5,6 @@ webview-ui/build/
package-lock.json
src/core/prompts/system.ts
src/core/prompts/model_prompts/claude4.ts
evals/
evals/
docs/
out/
+1 -1
View File
@@ -2,7 +2,7 @@ import { defineConfig } from "@vscode/test-cli"
import path from "path"
export default defineConfig({
files: "{out/**/*.test.js,src/**/*.test.js}",
files: "{out/**/*.test.js,src/**/*.test.js,!src/test/e2e/**/*.test.js,!out/src/test/e2e/**/*.test.js}",
mocha: {
ui: "bdd",
timeout: 20000, // Maximum time (in ms) that a test can run before failing
+8 -5
View File
@@ -14,7 +14,8 @@
"preLaunchTask": "${defaultBuildTask}",
"env": {
"IS_DEV": "true",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}"
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
"CLINE_ENVIRONMENT": "production"
}
},
{
@@ -23,20 +24,22 @@
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--user-data-dir=${workspaceFolder}/dist/tmp/user",
"--profile-temp",
"--sync",
"off",
"--sync=off",
"--disable-extensions",
"--extensionDevelopmentPath=${workspaceFolder}",
"${workspaceFolder}"
],
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
"preLaunchTask": "clean-sandbox",
"preLaunchTask": "clean-tmp-user",
"internalConsoleOptions": "openOnSessionStart",
"postDebugTask": "stop",
"env": {
"IS_DEV": "true",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}"
"TEMP_PROFILE": "true",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
"CLINE_ENVIRONMENT": "production"
}
},
{
+2 -2
View File
@@ -233,10 +233,10 @@
"type": "shell"
},
{
"label": "clean-sandbox",
"label": "clean-tmp-user",
"type": "shell",
"dependsOn": ["watch"],
"command": "rm -rf .vscode-dev"
"command": "rm -rf ${workspaceFolder}/dist/tmp/user && mkdir -p ${workspaceFolder}/dist/tmp/user"
}
],
"inputs": [
+26 -8
View File
@@ -1,24 +1,40 @@
# Default
.vscode/**
.vscode-test/**
out/**
node_modules/**
out/
dist-standalone/
node_modules/
src/**
standalone/**
.gitignore
.yarnrc
esbuild.js
vsc-extension-quickstart.md
**/tsconfig.json
tsconfig*.json
**/.eslintrc.json
**/*.map
**/*.ts
**/.vscode-test.*
eslint-rules/**
.github/**
.husky/**
# Custom
demo.gif
**/demo.gif
.nvmrc
.gitattributes
.prettierignore
.husky/
.github/
eslint-rules/
old_docs/
evals/
.changie.yaml
.codespellrc
.mocharc.json
buf.yaml
.changeset/
.clinerules/
# Ignore all webview-ui files except the build directory (https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/frameworks/hello-world-react-cra/.vscodeignore)
webview-ui/src/**
@@ -32,17 +48,19 @@ webview-ui/node_modules/**
# Ignore docs
docs/**
old_docs/**
# Fix issue where codicons don't get packaged (https://github.com/microsoft/vscode-extension-samples/issues/692)
!node_modules/@vscode/codicons/dist/codicon.css
!node_modules/@vscode/codicons/dist/codicon.ttf
# Include KaTeX CSS and fonts for LaTeX rendering
!webview-ui/node_modules/katex/dist/katex.min.css
!webview-ui/node_modules/katex/dist/fonts/**
# Include default themes JSON files used in getTheme
!src/integrations/theme/default-themes/**
# Include icons
!assets/icons/**
# Ignore E2E build files
e2e-build.js
e2e.vsix
test-results/
+114
View File
@@ -1,5 +1,119 @@
# Changelog
## [3.19.7]
- Add Hugging Face as a new API provider with support for their inference API models
- Improve Claude Code error messages with better guidance for common setup issues (Thanks @BarreiroT!)
- Fix authentication sync issues when using multiple VSCode windows
## [3.19.6]
- Improve Kimi K2 model provider routing with additional provider options for better availability and performance
- Fixed terminal bug where Cline failed to capture output of certain fast-running commands
- Fixed bug with increasing auto approved number of requests not resetting the counter mid-task
## [3.19.5]
- Add Groq as a new API provider with support for all Groq models including Kimi-K2
- Add user role display in organization UI for Cline account users
- Fix message dialogs not showing option buttons properly
- Fix authentication issues when using multiple VSCode windows
## [3.19.4]
- Add ability to choose Chinese endpoint for Moonshot provider
## [3.19.3]
- Add Moonshot AI provider
## [3.19.2]
- Show request ID in error messages returned by Cline Accounts API to help debug user reported issues
## [3.19.1]
- Fix documentation
## [3.19.0]
- Add Kimi-K2 as a recommended model in the Cline Provider, and route to Together/Groq for 131k context window and high throughput
- Added API Key support for Bedrock integration
## [3.18.14]
- Fix bug where Cline account users logged in with invalid token would not be shown as logged out in webview presentation layer
## [3.18.13]
- Fix authentication issue where Cline accounts users would keep getting logged out or seeing 'Unexpected API response' errors
## [3.18.12]
- Fix flaky organization switching behavior in Cline provider that caused UI inconsistencies and double loading
- Fix insufficient credits error display to properly show error messages when account balance is too low
- Improve credit balance validation and error handling for Cline provider requests
## [3.18.11]
- Fix authentication issues with Cline provider by ensuring the client always uses the latest auth token
## [3.18.10]
- Update recommended fast & cheap model to Grok 4 in OpenRouter model picker
- Fix Gemini 2.5 Pro thinking budget slider and add support for Gemini 2.5 Flash Lite Preview model (Thanks @arafatkatze!)
## [3.18.9]
- Fix streaming reliability issues with Cline provider that could cause connection problems during long conversations
- Fix authentication error handling for Cline provider to show clearer error messages when not signed in and prevent recursive failed requests
- Remove incorrect pricing display for SAP AI Core provider since it uses non-USD "Capacity Units" that cannot be directly converted (Thanks @ncryptedV1!)
## [3.18.8]
- Update pricing for Grok 3 model because the promotion ended
## [3.18.7]
- Remove promotional "free" messaging for Grok 3 model in UI
## [3.18.6]
- Update request header to include `"ai-client-type": "Cline"` to SAP Api Provider
- Add organization accounts
## [3.18.5]
- Fix Plan/Act mode persistence across sessions and multi-workspace conflicts
- Improve provider switching performance by 18x (from 550ms to 30ms) with batched storage operations
- Improve SAP AI Core provider model organization and fix exception handling (Thanks @schardosin!)
## [3.18.4]
- Add support for Gemini 2.5 Pro and Flash to SAP AI Core Provider
- Fix logging in with Cline account not getting past welcome screen
## [3.18.3]
- Improve Cerebras Qwen model performance by removing thinking tokens from model input (Thanks @kevint-cerebras!)
- Improve Claude Code provider with better error handling and performance optimizations (Thanks @BarreiroT!)
## [3.18.2]
- Fix issue where terminal output would not be captured if shell integration fails by falling back to capturing the terminal content.
- Add confirmation popup when deleting tasks
- Add support for Claude Sonnet 4 and Opus 4 model in SAP AI Core provider (Thanks @lizzzcai!)
- Add support for `litellm_session_id` to group requests in a single session (Thanks @jorgegarciarey!)
- Add "Thinking Budget" customization for Claude Code (Thanks @BarreiroT!)
- Fix issue where the extension would use the user's environment variables for authentication when using Claude Code (Thanks @BarreiroT!)
## [3.18.1]
- Add support for Claude 4 Sonnet in SAP AI Core provider (Thanks @GTxx!)
- Fix ENAMETOOLONG error when using Claude Code provider with long conversation histories (Thanks @BarreiroT!)
- Remove Gemini CLI provider because Google asked us to
- Fix bug with "Delete All Tasks" functionality
## [3.18.0]
- Optimized Cline to work with the Claude 4 family of models, resulting in improved performance, reliability, and new capabilities
+1 -1
View File
@@ -51,7 +51,7 @@ Thanks to [Claude 3.7 Sonnet's agentic coding capabilities](https://www.anthrop
### Use any API and Model
Cline supports API providers like OpenRouter, Anthropic, OpenAI, Google Gemini, AWS Bedrock, Azure, GCP Vertex, and Cerebras. You can also configure any OpenAI compatible API, or use a local model through LM Studio/Ollama. If you're using OpenRouter, the extension fetches their latest model list, allowing you to use the newest models as soon as they're available.
Cline supports API providers like OpenRouter, Anthropic, OpenAI, Google Gemini, AWS Bedrock, Azure, GCP Vertex, Cerebras and Groq. You can also configure any OpenAI compatible API, or use a local model through LM Studio/Ollama. If you're using OpenRouter, the extension fetches their latest model list, allowing you to use the newest models as soon as they're available.
The extension also keeps track of total tokens and API usage cost for the entire task loop and individual requests, keeping you informed of spend every step of the way.
+7 -2
View File
@@ -61,7 +61,6 @@
"getting-started/installing-cline",
"getting-started/installing-dev-essentials",
"getting-started/model-selection-guide",
"getting-started/our-favorite-tech-stack",
"getting-started/task-management",
"getting-started/understanding-context-management",
"getting-started/what-is-cline"
@@ -147,6 +146,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",
@@ -159,7 +159,8 @@
"provider-config/openai",
"provider-config/openai-compatible",
"provider-config/openrouter",
"provider-config/requesty"
"provider-config/requesty",
"provider-config/sap-aicore"
]
},
{
@@ -170,6 +171,10 @@
"running-models-locally/ollama"
]
},
{
"group": "Troubleshooting",
"pages": ["troubleshooting/terminal-quick-fixes", "troubleshooting/terminal-integration-guide"]
},
{
"group": "More Info",
"pages": ["more-info/telemetry"]
@@ -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)
@@ -58,3 +58,16 @@ When you use the terminal mention in your message, here's what happens behind th
6. The AI can now "see" the complete terminal output with all formatting preserved
This process happens automatically whenever you use the terminal mention, giving the AI access to your command results, error messages, and other terminal output without you having to copy it manually.
## Troubleshooting Terminal Issues
If you're experiencing issues with terminal mentions or terminal integration in general (such as "Shell Integration Unavailable" or commands not showing output), please refer to our comprehensive [Terminal Integration Troubleshooting Guide](/troubleshooting/terminal-integration-guide).
Common issues include:
- Terminal mentions not capturing output
- "Shell Integration Unavailable" messages in Cline chat
- Commands executing but output not visible to Cline
- Terminal integration working inconsistently
The troubleshooting guide provides platform-specific solutions and detailed configuration steps to resolve these issues.
@@ -74,8 +74,25 @@ This approach ensures that all terminal output, including colors and formatting,
- **Select specific output when needed**: By default, the integration captures all terminal content, but you can also select specific lines before right-clicking to focus on just the relevant output.
- **Combine with file mentions**: After sending terminal output to Cline, you can enhance your question by mentioning relevant files using the @ mentions feature.
- **Combine terminal outputs with file mentions**: After sending terminal output to Cline, you can enhance your question by mentioning relevant files using the @ mentions feature.
- **Use for build and test output**: Terminal integration is particularly useful for understanding complex build errors or test failures that span multiple lines.
- **Contextualize build & test outputs with the terminal**: Terminal integration is particularly useful for understanding complex build errors or test failures that span multiple lines.
Next time you're staring at a cryptic error message in your terminal, try using Cline's terminal integration instead of copying and pasting. You'll get more accurate help because Cline can see the complete terminal context with proper formatting.
## Troubleshooting Terminal Issues
If you're experiencing issues with terminal integration, such as "Shell Integration Unavailable" or commands not showing output, please refer to our comprehensive [Terminal Integration Troubleshooting Guide](/troubleshooting/terminal-integration-guide).
The troubleshooting guide covers:
- Common terminal integration issues and quick fixes
- Platform-specific solutions for Windows, macOS, and Linux
- Shell-specific configurations for zsh, bash, PowerShell, and more
- Advanced debugging techniques
- Terminal settings optimization
<Tip>
**Quick Fix**: Most terminal issues can be resolved by switching to bash in the Cline settings and increasing the shell
integration timeout to 10 seconds.
</Tip>
@@ -1,238 +0,0 @@
---
title: "Our Favorite Tech Stack"
description: "A curated list of our recommended technologies and tools for building modern web applications with Cline."
---
## Recommended Stack for New Cline Users (2025)
### Your Complete Development Environment
#### Development Tools
- **VS Code** - Your code editor, [download here](https://code.visualstudio.com/)
- **GitHub** - Where your code lives, [sign up here](https://github.com)
#### Frontend
- **Next.js 14+** - React framework with App Router
- **Tailwind CSS** - Beautiful styling without writing CSS
- **TypeScript** - JavaScript, but safer and smarter
#### Backend
- **Supabase** - Your complete backend solution, [sign up with GitHub](https://supabase.com)
- PostgreSQL database
- Authentication
- File storage
- Real-time updates
#### Deployment
- **Vercel** - Where your app runs, [sign up with GitHub](https://vercel.com)
- Automatic deployments from GitHub
- Preview deployments for testing
- Production-ready CDN
#### AI Development
Choose your AI assistant based on your needs:
| Model | Input Cost (per 1M tokens) | Output Cost (per 1M tokens) | Best For |
| ----------------- | -------------------------- | --------------------------- | ------------------------------ |
| Claude 3.5 Sonnet | $3.00 | $15.00 | Production apps, complex tasks |
| DeepSeek R1 | $1.00 | $3.00 | Budget-conscious production |
| DeepSeek V3 | $0.14 | $2.20 | Budget-conscious development |
#### Free Tier Benefits
**Vercel (Hobby)**
- 100 GB data transfer/month
- 100k serverless function invocations
- 100 MB deployment size
- Automatic HTTPS & CI/CD
**Supabase (Free)**
- 500 MB database storage
- 1 GB file storage
- 50k monthly active users
- 2M real-time messages/month
**GitHub (Free)**
- Unlimited public repositories
- GitHub Actions CI/CD
- Project management tools
- Collaboration features
### Getting Started
1. Install the development essentials:
- Follow our [Development Essentials Installation Guide](https://docs.cline.bot/getting-started/installing-dev-essentials)
2. Set up Cline's Memory Bank:
- Follow the [Memory Bank setup instructions](https://docs.cline.bot/prompting/cline-memory-bank)
- Create an empty `cline_docs` folder in your project root
- Create `projectBrief.md` in the `cline_docs` folder (see example below)
- Tell Cline to "initialize memory bank"
3. Add our recommended stack configuration:
- Create `.clinerules` file (see template below)
- Let Cline handle the rest!
#### Example Project Brief
```markdown
# Project Brief
## Overview
Building a [type of application] that will [main purpose].
## Core Features
- Feature 1
- Feature 2
- Feature 3
## Target Users
[Describe who will use your application]
## Technical Preferences (optional)
- Any specific technologies you want to use
- Any specific requirements or constraints
```
### .clinerules Template
```markdown
# Project Configuration
## Tech Stack
- Next.js 14+ with App Router
- Tailwind CSS for styling
- Supabase for backend
- Vercel for deployment
- GitHub for version control
## Project Structure
/src
/app # Next.js App Router pages
/components # React components
/lib # Utility functions
/types # TypeScript types
/supabase
/migrations # SQL migration files
/seed # Seed data files
/public # Static assets
## Database Migrations
SQL files in /supabase/migrations should:
- Use sequential numbering: 001, 002, etc.
- Include descriptive names
- Be reviewed by Cline before execution
Example: 001_create_users_table.sql
## Development Workflow
- Cline helps write and review code changes
- Vercel automatically deploys from main branch
- Database migrations reviewed by Cline before execution
## Security
DO NOT read or modify:
- .env files
- \*_/config/secrets._
- Any file containing API keys or credentials
```
### Learning Resources (2025)
Want to learn more about the technologies we're using? Here are some great resources:
#### Next.js and React
- [Official Learn Next.js Course](https://nextjs.org/learn) - Interactive tutorial
- [NextJS App Router: Modern Web Dev in 1 Hour](https://www.youtube.com/nextjs-modern) - Quick overview
- [Building Real-World Apps with Next.js](https://www.youtube.com/nextjs-real-world) - Practical examples
#### Supabase
- [Supabase From Scratch](https://www.udemy.com/supabase-scratch) - Comprehensive course
- [Official Quickstart Guides](https://supabase.com/docs/guides/getting-started)
- [Real-Time Apps with Next.js and Supabase](https://www.newline.co/courses/supabase-nextjs)
#### Tailwind CSS
- [Tailwind CSS Tutorial for Beginners](https://www.youtube.com/tailwind-2025)
- [Official Tailwind Documentation](https://tailwindcss.com/docs)
- Interactive course at [Scrimba Tailwind CSS Course](https://scrimba.com/learn/tailwind)
### Other Things to Know
#### Working with Git & GitHub
Git helps you track changes in your code and collaborate with others. Here are the essential commands you'll use:
**Daily Development**
```bash
# Save your changes (do this often!)
git add . # Stage all changed files
git commit -m "Add login page" # Save changes with a clear message
# Share your changes
git push origin main # Upload to GitHub
```
**Common Workflow**
1. **Start of day**: Get latest changes
```bash
git pull origin main # Download latest code
```
2. **During development**: Save work regularly
```bash
git add .
git commit -m "Clear message about changes"
```
3. **End of day**: Share your progress
```bash
git push origin main # Upload to GitHub
```
**Best Practices**
- Commit often with clear messages
- Pull before starting new work
- Push completed work to share with others
- Use `.gitignore` to avoid committing sensitive files
> **Tip**: Vercel automatically deploys when you push to main!
#### Environment Variables
- Store secrets in `.env.local` for development
- Add them to Vercel project settings for production
- Never commit `.env` files to Git
#### Getting Help
1. Use `/help` in Cline chat for immediate assistance
2. Check [Cline Documentation](https://docs.cline.bot)
3. Join our [Discord Community](https://discord.gg/cline)
4. Search GitHub issues for common problems
Remember: Cline is here to help at every step. Just ask for guidance or clarification when needed!
-10118
View File
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -4,13 +4,15 @@
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "mintlify dev"
"dev": "mintlify dev",
"check": "mintlify broken-links",
"rename": "mintlify rename"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"mintlify": "^4.0.538"
"mintlify": "^4.2.23"
}
}
@@ -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.
+27 -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
@@ -32,6 +32,31 @@ First, you'll need to install and authenticate Claude Code on your system:
/>
</Frame>
<br />
<Accordion title="Windows Setup">
On Windows, Cline supports integrating with Claude Code through WSL.
Windows doesn't support long commands, and Claude Code only accepts the system prompt through a flag, which means we can't properly prompt Claude through Claude Code. Anthropic is [working on a workaround to streamline this](https://github.com/anthropics/claude-code/issues/3411).
1. **Make sure you have WSL set-up**. You can follow [this](https://code.visualstudio.com/docs/remote/wsl#_installation) guide to do it.
2. **Open VSCode from WSL** and verify it's properly set-up. You should see an indicator in the bottom left that says "WSL". You can find an image of the indicator [here](https://code.visualstudio.com/docs/remote/wsl#_from-the-wsl-terminal).
3. Install Cline within WSL and make sure it shows the following:
<Frame>
<img
src="https://storage.googleapis.com/cline_public_images/docs/assets/cline_wsl_installed_extension.webp"
alt="Indicator that an extension is installed on WSL"
/>
</Frame>
4. Clone or [move](https://stackoverflow.com/a/42586455) your project over to WSL
5. Follow the [instructions on how to set up Claude Code normally](#setup), but from the WSL terminal.
</Accordion>
### Finding your Claude Code path
If you're not sure where Claude Code is installed:
+39
View File
@@ -0,0 +1,39 @@
---
title: "SAP AI Core"
description: "Learn how to configure and use LLM models from Generative AI Hub in SAP AI Core with Cline."
---
SAP AI Core and the generative AI hub help you to integrate LLMs and AI into new business processes in a cost-efficient manner.
**Website:** [SAP Help Portal](https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/what-is-sap-ai-core)
### Getting a Service Binding
> 💡 **Information**
>
> SAP AI Core, and Generative AI Hub, are offerings from SAP BTP.
> You need an active SAP BTP contract and a existing subaccount with a SAP AI Core instance to perform these steps.
1. **Access:** Go to your subaccount via [BTP Cloud Cockpit](cockpit.btp.cloud.sap/cockpit)
2. **Create a Service Binding:** Go to "Instances and Subscriptions", select your SAP AI Core service instance and click on Service Bindings > Create.
3. **Copy the Service Binding:** Copy the service binding values.
### Supported Models
SAP AI Core supports a large and growing number of models.
Refer to the [Generative AI Hub Supported Models page](https://me.sap.com/notes/3437766) for the complete and up-to-date list.
### Configuration in Cline
1. **Open Cline Settings:** Click the settings icon (⚙️) in the Cline panel.
2. **Select Provider:** Choose "SAP AI Core" from the "API Provider" dropdown.
3. **Enter Client Id:** Add the `.clientid` field from the service binding into the "AI Core Client Id" field.
4. **Enter Client Secret:** Add the `.clientsecret` field from the service binding into the "AI Core Client Secret" field.
5. **Enter Base URL:** Add the `.serviceurls.AI_API_URL` field from the service binding into the "AI Core Base URL" field.
6. **Enter Auth URL:** Add the `.url` field from the service binding into the "AI Core Auth URL" field.
7. **Enter Resource Group:** Add the resource group where you have your model deployments. See [Create a Deployment for a Generative AI Model](https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/create-deployment-for-generative-ai-model-in-sap-ai-core).
8. **Select Model:** Choose your desired model from the "Model" dropdown.
### Tips and Notes
- **Model Selection:** SAP AI Core offers a wide range of models. You won't be able to use the model, even if selected, if a deployment doesn't exist in the provided resource group.
@@ -0,0 +1,399 @@
---
title: "Terminal Integration Troubleshooting Guide"
sidebarTitle: "Terminal Troubleshooting"
description: "Complete guide to resolving terminal integration issues in Cline"
---
This guide helps you resolve terminal integration issues in Cline. Terminal integration is crucial for Cline to execute commands and read their output, enabling it to understand errors, test results, and command responses.
<Tip>
If you're experiencing terminal issues, try switching to a simpler shell like `bash` in the Cline settings, under "Terminal Settings"
This resolves most terminal integration problems.
</Tip>
## Quick Diagnosis Flowchart
Follow this flowchart to quickly identify your issue:
```mermaid
graph TD
A[Terminal Issue] --> B{Can Cline execute commands?}
B -->|No| C[Shell Integration Unavailable]
B -->|Yes| D{Can Cline see the output?}
D -->|No| E[Output Capture Failed]
D -->|Yes| F{Is the output corrupted?}
F -->|Yes| G[Character Filtering Issue]
F -->|No| H{Does the command hang?}
H -->|Yes| I[Long-Running Command Issue]
H -->|No| J[Check Terminal Settings]
C --> K[Try Solution 1]
E --> L[Try Solution 2]
G --> M[Try Solution 3]
I --> N[Try Solution 4]
style A fill:#f9f,stroke:#333,stroke-width:2px
style K fill:#9f9,stroke:#333,stroke-width:2px
style L fill:#9f9,stroke:#333,stroke-width:2px
style M fill:#9f9,stroke:#333,stroke-width:2px
style N fill:#9f9,stroke:#333,stroke-width:2px
```
## Common Issues & Quick Solutions
### 1. Shell Integration Unavailable
**Symptoms:**
- Message: "Shell Integration Unavailable"
- Commands execute but Cline can't read output
- Terminal works fine manually but not with Cline
**Quick Solutions:**
#### macOS
- **Switch to bash**
1. Go to Cline Settings
2. Left-Click the **"Terminal Settings"** tab
3. Navigate to **"Default Terminal Profile"** and select **"bash"** from the drop-down menu
- **Disable Oh-My-Zsh temporarily**:
1. If using zsh, enter `mv ~/.zshrc ~/.zshrc.backup` into the terminal
2. Restart VSCode
- **Set environment**:
1.a For Zsh users, use one of the following Zsh commands to edit your shell profile:
- `nano ~/.zshrc`
- `vim ~/.zshrc`
- `code ~/.zshrc`
1.b For Bash users
- nano ~/.bash_profile
2. Add the following to your shell config: `export TERM=xterm-256color`
3. Save your configuration
#### Windows
- **Use PowerShell 7**
1. Install from Microsoft Store
2. Go to Cline Settings
3. Left-Click the **"Terminal Settings"** tab
4. Navigate to **"Default Terminal Profile"** and select **"PowerShell 7"** from the drop-down menu
- **Disable Windows ConPTY**
1. Navigate to your VSCode Settings
2. Enter "Integrated: Windows Enable Conpty" into the Settings searchbar
3. Uncheck the option
- **Try Command Prompt**
1. Go to Cline Settings
2. Left-Click the **"Terminal Settings"** tab
3. Navigate to **"Default Terminal Profile"** and select **"Command Prompt"** from the drop-down menu
#### Linux
- **Use bash**
1. Go to Cline Settings
2. Left-Click the **"Terminal Settings"** tab
3. Navigate to **"Default Terminal Profile"** and select **"bash"** from the drop-down menu
- **Check permissions**
1. Ensure VSCode has terminal access permissions
- **Disable custom prompts**
1. Comment out prompt customizations in `.bashrc`
### 2. Command Output Not Visible
**Symptoms:**
- Cline states in chat: "[Command is running but producing no output]"
- Commands complete but Cline doesn't see results
- Commands work sometimes but not consistently
**Solutions:**
- **Increase Shell Integration Timeout**
1. Within Cline, left-click the **Settings** button in the top right-hand corner of the chat window
2. Once in the **Settings** window, left-click the **"Terminal Settings"** tab from the left-hand column
3. Navigate to "Shell integration timeout (seconds)" and enter **"10"** into the text field
- **Disable Terminal Reuse**
1. Within Cline, left-click the **Settings** button in the top right-hand corner of the chat window
2. Once in the **Settings** window, left-click the **"Terminal Settings"** tab from the left-hand column
3. Look for **"Enable aggressive terminal reuse"**, and **uncheck** this option
- **Check for interfering extensions**
1. Disable other terminal-related VSCode extensions
### 3. Character Filtering Issues
**Symptoms:**
- Commas missing from output (JSON appears corrupted)
- Special characters stripped from terminal output
- Syntax errors that don't appear when running manually
**Solution:**
This is a known bug in output processing. Workarounds:
- Recommend AI to use file output instead
1. Tell Cline in chat or Cline rules, to use `command > output.txt` before reading the file/s
<Tip>
This family of issues is only partially solved in the latest Cline versions, so if you still face this, create a GitHub issue
if it is a persistent problem.
</Tip>
### 4. Long-Running Commands & Progress Bars
**Symptoms:**
- Docker builds never complete in Cline
- Progress bars consume thousands of tokens
- The Cline button "Proceed while running" doesn't work properly in chat
<Tip>
This family of issues has been solved in latest Cline versions but if you still face any issues, then create a GitHub issue
for this.
</Tip>
## Terminal Settings Explained
Access these in Cline by clicking the settings icon, and navigating to the "Terminal Settings" section:
### Default Terminal Profile
- **What it does**: Selects which shell Cline uses for commands
- **When to change**: If experiencing shell integration issues with your default shell
- **Recommended**: - macOS: bash (if zsh has issues) - Windows: PowerShell 7 - Linux: bash
### Shell Integration Timeout
- **What it does**: How long Cline waits for the terminal to be ready
- **Default**: 4 seconds
- **When to increase**:
- Slow shell startup (heavy .zshrc/.bashrc)
- WSL environments
- SSH connections
- **Recommended**: - Start with 10 seconds if having issues
### Enable Aggressive Terminal Reuse
- **What it does**: Reuses existing terminals even if not in the correct directory
- **When to disable**:
- Commands execute in wrong directory
- Virtual environment issues
- Terminal state corruption
- **Trade-off**: - Disabling creates more terminals but ensures clean state
### Terminal Output Line Limit
- **What it does**: Limits how many lines Cline reads from terminal output
- **Default**: 500 lines
- **When to adjust**:
- Increase for verbose build outputs
- Decrease if hitting token limits
- Set to 100 for commands with progress bars
## Platform-Specific Solutions
### macOS Issues
#### Oh-My-Zsh Conflicts
Oh-My-Zsh often interferes with shell integration. Solutions:
1. Create a minimal `.zshrc` for VSCode:
```bash
# ~/.zshrc-vscode
export TERM=xterm-256color
export PAGER=cat
# Minimal PATH and environment setup
```
2. Configure VSCode to use it:
```json
{
"terminal.integrated.env.osx": {
"ZDOTDIR": "~/.zshrc-vscode"
}
}
```
#### macOS 15+ Issues
Recent macOS versions have stricter terminal permissions:
1. System Preferences → Privacy & Security → Developer Tools
2. Add Visual Studio Code
3. Restart VSCode completely
### Windows Issues
#### PowerShell Execution Policy
If commands fail silently:
```powershell
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
```
#### WSL Integration
For WSL issues:
1. Use WSL extension for VSCode
2. Open folder in WSL: `code .` from WSL terminal
3. Select "WSL Bash" as terminal profile in Cline
#### Path Issues
Windows path problems:
1. Use forward slashes in Cline: `C:/Users/...`
2. Quote paths with spaces: `"C:/Program Files/..."`
3. Avoid `~` - use full paths
### Linux/SSH/Container Issues
#### SSH Connections
For remote development:
1. Install Cline on the remote machine, not locally
2. Use SSH extension's integrated terminal
3. Increase timeout to 15+ seconds
#### Docker Containers
When developing in containers:
1. Install Cline in the container
2. Use Dev Containers extension
3. Ensure shell integration scripts are available
## Shell-Specific Fixes
### Zsh
```bash
# Add to ~/.zshrc
export TERM=xterm-256color
export PAGER=cat
# Disable fancy prompts for VSCode
if [[ "$TERM_PROGRAM" == "vscode" ]]; then
PS1="%n@%m %1~ %# "
fi
```
### Bash
```bash
# Add to ~/.bashrc
export TERM=xterm-256color
export PAGER=cat
# Simple prompt for VSCode
if [[ "$TERM_PROGRAM" == "vscode" ]]; then
PS1='\u@\h:\w\$ '
fi
```
### Fish
```fish
# Add to ~/.config/fish/config.fish
set -x TERM xterm-256color
set -x PAGER cat
# Disable fancy features in VSCode
if test "$TERM_PROGRAM" = "vscode"
function fish_prompt
echo (whoami)'@'(hostname)':'(pwd)'> '
end
end
```
### PowerShell
```powershell
# Add to $PROFILE
$env:PAGER = "cat"
# Disable progress bars
$ProgressPreference = 'SilentlyContinue'
```
## Advanced Troubleshooting
### Debug Mode
Enable terminal debugging to see what's happening:
1. Open VSCode Command Palette (Cmd/Ctrl+Shift+P)
2. Run: "Developer: Set Log Level..."
3. Choose "Trace"
4. Check Output panel → "Cline" for terminal logs
### Manual Shell Integration Test
Test if shell integration works at all:
```bash
# In VSCode terminal
echo $TERM_PROGRAM # Should show "vscode"
echo $VSCODE_SHELL_INTEGRATION # Should be "1"
```
## FAQ
### Why does Cline create so many terminals?
When shell integration fails, Cline can't reuse terminals safely (they might be running long processes). Enable shell integration or adjust the terminal reuse setting.
### Can I use my custom shell (nushell, xonsh, etc.)?
Cline officially supports bash, zsh, fish, and PowerShell. Custom shells may work but aren't guaranteed. Use bash as a fallback.
### Why do some commands work but others don't?
Commands that use interactive features (pagers, progress bars, curses) often fail. Set `PAGER=cat` and use non-interactive flags.
### How do I know if shell integration is working?
Working integration shows command output in Cline's chat. Failed integration shows "Shell Integration Unavailable" or "[Command is running but producing no output]".
## Still Having Issues?
If you've tried everything:
1. **Collect Debug Info**:
```bash
echo "Shell: $SHELL"
echo "Term: $TERM"
echo "VSCode: $TERM_PROGRAM"
which bash
bash --version
```
2. **Report the Issue**:
- Use `/reportbug` in Cline github issues
- Include your debug info
- Mention which solutions you tried
<Tip>
Remember: Most terminal issues are resolved by switching to bash and increasing the timeout. Start there before trying complex
solutions.
</Tip>
@@ -0,0 +1,51 @@
---
title: "Terminal Quick Fixes"
sidebarTitle: "Terminal Quick Fixes"
description: "Quick solutions for common terminal issues"
---
**Here is a list of common fixes, starting with the most applicable:**
- **Switch to bash** (solves most instances)
1. Within Cline, left-click the **Settings** button in the top right-hand corner of the chat window
2. Once in the **Settings** window, left-click the **"Terminal Settings"** tab from the left-hand column
3. Navigate to **"Default Terminal Profile"** and select **"bash"** from the drop-down
- **Increase timeout**
1. Within Cline, left-click the **Settings** button in the top right-hand corner of the chat window
2. Once in the **Settings** window, left-click the **"Terminal Settings"** tab from the left-hand column
3. Navigate to "Shell integration timeout (seconds)" and enter **"10"** into the text field
- **Disable terminal reuse**
1. Within Cline, left-click the **Settings** button in the top right-hand corner of the chat window
2. Once in the **Settings** window, left-click the **"Terminal Settings"** tab from the left-hand column
3. Look for **"Enable aggressive terminal reuse"**, and **uncheck** this option
## Platform-Specific Fixes
### macOS + Oh-My-Zsh
```bash
# Create minimal config for VSCode
echo 'export TERM=xterm-256color' > ~/.zshrc-vscode
echo 'export PAGER=cat' >> ~/.zshrc-vscode
```
### Windows PowerShell
```powershell
# Run as Administrator
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
```
### WSL
- Open folder from WSL: `code .`
- Select **"WSL Bash"** in Cline settings, under **"Terminal Settings"**
- Increase **"Shell integration timeout (seconds)"** to **15**
## Full Guide
For detailed troubleshooting, see the [Complete Terminal Troubleshooting Guide](/troubleshooting/terminal-integration-guide).
+14 -3
View File
@@ -5,6 +5,7 @@ const path = require("path")
const production = process.argv.includes("--production")
const watch = process.argv.includes("--watch")
const standalone = process.argv.includes("--standalone")
const e2eBuild = process.argv.includes("--e2e-build")
const destDir = standalone ? "dist-standalone" : "dist"
/**
@@ -153,15 +154,25 @@ const extensionConfig = {
// Standalone-specific configuration
const standaloneConfig = {
...baseConfig,
entryPoints: ["src/standalone/standalone.ts"],
outfile: `${destDir}/standalone.js`,
entryPoints: ["src/standalone/cline-core.ts"],
outfile: `${destDir}/cline-core.js`,
// These gRPC protos need to load files from the module directory at runtime,
// so they cannot be bundled.
external: ["vscode", "@grpc/reflection", "grpc-health-check"],
}
// E2E build script configuration
const e2eBuildConfig = {
...baseConfig,
entryPoints: ["src/test/e2e/utils/build.ts"],
outfile: `${destDir}/e2e-build.js`,
external: ["@vscode/test-electron", "execa"],
sourcemap: false,
plugins: [aliasResolverPlugin, esbuildProblemMatcherPlugin],
}
async function main() {
const config = standalone ? standaloneConfig : extensionConfig
const config = standalone ? standaloneConfig : e2eBuild ? e2eBuildConfig : extensionConfig
const extensionCtx = await esbuild.context(config)
if (watch) {
await extensionCtx.watch()
@@ -0,0 +1,123 @@
const { RuleTester: DirectApiRuleTester } = require("eslint")
const noDirectVscodeApiRule = require("../no-direct-vscode-api")
const directApiRuleTester = new DirectApiRuleTester({
parser: require.resolve("@typescript-eslint/parser"),
parserOptions: {
ecmaVersion: 2020,
sourceType: "module",
ecmaFeatures: {
jsx: true,
},
},
})
directApiRuleTester.run("no-direct-vscode-api", noDirectVscodeApiRule, {
valid: [
// Should allow vscode.postMessage in grpc-client-base.ts
{
code: `vscode.postMessage({ type: "grpc_request", data: {} })`,
filename: "grpc-client-base.ts",
},
{
code: `vscode.postMessage({ type: "grpc_request_cancel" })`,
filename: "/path/to/grpc-client-base.ts",
},
// Should allow in exception directories
{
code: `vscode.workspace.workspaceFolders`,
filename: "/src/hosts/vscode/host-bridge.ts",
},
{
code: `vscode.workspace.fs.stat(uri)`,
filename: "/standalone/runtime-files/helpers.ts",
},
// Should allow other vscode API calls
{
code: `vscode.window.showInformationMessage("Hello")`,
filename: "test.ts",
},
// Should allow postMessage calls on other objects
{
code: `window.postMessage({ type: "test" }, "*")`,
filename: "test.ts",
},
// Should allow variables named vscode but not calling postMessage
{
code: `const vscode = { other: "method" }; vscode.other()`,
filename: "test.ts",
},
],
invalid: [
// Should disallow vscode.postMessage in regular files
{
code: `vscode.postMessage({ type: "test", data: {} })`,
filename: "test.ts",
errors: [
{
messageId: "useGrpcClient",
},
],
},
// Should disallow vscode.postMessage in components
{
code: `vscode.postMessage({ type: "apiConfiguration", apiConfiguration })`,
filename: "ApiOptions.tsx",
errors: [
{
messageId: "useGrpcClient",
},
],
},
// Should disallow vscode.postMessage in test files
{
code: `vscode.postMessage({ type: "newTask", text: message.text })`,
filename: "test.test.ts",
errors: [
{
messageId: "useGrpcClient",
},
],
},
// Should disallow property access for disallowed APIs
{
code: `const folders = vscode.workspace.workspaceFolders;`,
filename: "workspace.ts",
errors: [
{
messageId: "useHostBridge",
},
],
},
// Should disallow method calls for disallowed APIs
{
code: `const relativePath = vscode.workspace.asRelativePath(filePath);`,
filename: "path-utils.ts",
errors: [
{
messageId: "usePathUtils",
},
],
},
// Should disallow nested property access
{
code: `const stats = await vscode.workspace.fs.stat(uri);`,
filename: "file-utils.ts",
errors: [
{
messageId: "useFsUtils",
},
],
},
// Should disallow getting a workspace folder
{
code: `const folder = vscode.workspace.getWorkspaceFolder(uri);`,
filename: "path-helper.ts",
errors: [
{
messageId: "usePathUtils",
},
],
},
],
})
@@ -1,74 +0,0 @@
const { RuleTester: VscodeRuleTester } = require("eslint")
const vscodePostmessageRule = require("../no-vscode-postmessage")
const vscodeRuleTester = new VscodeRuleTester({
parser: require.resolve("@typescript-eslint/parser"),
parserOptions: {
ecmaVersion: 2020,
sourceType: "module",
ecmaFeatures: {
jsx: true,
},
},
})
vscodeRuleTester.run("no-vscode-postmessage", vscodePostmessageRule, {
valid: [
// Should allow vscode.postMessage in grpc-client-base.ts
{
code: `vscode.postMessage({ type: "grpc_request", data: {} })`,
filename: "grpc-client-base.ts",
},
{
code: `vscode.postMessage({ type: "grpc_request_cancel" })`,
filename: "/path/to/grpc-client-base.ts",
},
// Should allow other vscode API calls
{
code: `vscode.window.showInformationMessage("Hello")`,
filename: "test.ts",
},
// Should allow postMessage calls on other objects
{
code: `window.postMessage({ type: "test" }, "*")`,
filename: "test.ts",
},
// Should allow variables named vscode but not calling postMessage
{
code: `const vscode = { other: "method" }; vscode.other()`,
filename: "test.ts",
},
],
invalid: [
// Should ban vscode.postMessage in regular files
{
code: `vscode.postMessage({ type: "test", data: {} })`,
filename: "test.ts",
errors: [
{
messageId: "useGrpcClient",
},
],
},
// Should ban vscode.postMessage in components
{
code: `vscode.postMessage({ type: "apiConfiguration", apiConfiguration })`,
filename: "ApiOptions.tsx",
errors: [
{
messageId: "useGrpcClient",
},
],
},
// Should ban vscode.postMessage in test files
{
code: `vscode.postMessage({ type: "newTask", text: message.text })`,
filename: "test.test.ts",
errors: [
{
messageId: "useGrpcClient",
},
],
},
],
})
+3 -3
View File
@@ -1,13 +1,13 @@
// eslint-rules/index.js
const noProtobufObjectLiterals = require("./no-protobuf-object-literals")
const noGrpcClientObjectLiterals = require("./no-grpc-client-object-literals")
const noVscodePostmessage = require("./no-vscode-postmessage")
const noDirectVscodeApi = require("./no-direct-vscode-api")
module.exports = {
rules: {
"no-protobuf-object-literals": noProtobufObjectLiterals,
"no-grpc-client-object-literals": noGrpcClientObjectLiterals,
"no-vscode-postmessage": noVscodePostmessage,
"no-direct-vscode-api": noDirectVscodeApi,
},
configs: {
recommended: {
@@ -15,7 +15,7 @@ module.exports = {
rules: {
"local/no-protobuf-object-literals": "error",
"local/no-grpc-client-object-literals": "error",
"local/no-vscode-postmessage": "error",
"local/no-direct-vscode-api": "warn",
},
},
},
+209
View File
@@ -0,0 +1,209 @@
const { ESLintUtils } = require("@typescript-eslint/utils")
const path = require("path")
const createRule = ESLintUtils.RuleCreator((name) => `https://cline.bot/eslint-rules/${name}`)
// Configuration of disallowed VSCode APIs and their recommended alternatives
const disallowedApis = {
"vscode.postMessage": {
messageId: "useGrpcClient",
},
"vscode.workspace.fs.stat": {
messageId: "useFsUtils",
},
"vscode.workspace.fs.writeFile": {
messageId: "useFsUtils",
},
"vscode.workspace.workspaceFolders": {
messageId: "useHostBridgeWorkspace",
},
"vscode.workspace.asRelativePath": {
messageId: "usePathUtils",
},
"vscode.workspace.getWorkspaceFolder": {
messageId: "usePathUtils",
},
"vscode.window.showTextDocument": {
messageId: "useHostBridge",
},
"vscode.workspace.applyEdit": {
messageId: "useHostBridge",
},
// "vscode.env.openExternal": {
// messageId: "useUtils",
// },
// "vscode.window.showWarningMessage": {
// messageId: "useHostBridgeShowMessage",
// },
"vscode.window.showOpenDialog": {
messageId: "useHostBridgeShowMessage",
},
// There are too many warnings for these calls, uncomment the following
// when the migration is finished.
// "vscode.window.showErrorMessage": {
// messageId: "useHostBridgeShowMessage",
// },
// "vscode.window.showInformationMessage": {
// messageId: "useHostBridgeShowMessage",
// },
}
module.exports = createRule({
name: "no-direct-vscode-api",
meta: {
type: "problem",
docs: {
description:
"Disallow direct VSCode API usage in favor of Cline's abstraction layers, except in src/hosts/vscode and standalone/runtime-files directories",
recommended: "error",
},
messages: {
useGrpcClient:
"Use gRPC service clients instead of vscode.postMessage().\n" +
"Example: AccountServiceClient.methodName(RequestType.create({...})) instead of vscode.postMessage({type: '...'}).\n" +
"Found: {{code}}",
useFsUtils:
"Use utilities in @/utils/fs instead of vscode.workspace.fs\n" +
"Example: import { isDirectory } from '@/utils/fs' or use the file system methods from the host bridge provider.\n" +
"Found: {{code}}",
usePathUtils:
"Use path utilities from @/utils/path instead of VSCode workspace path methods.\n" +
"This provides consistent path handling across different environments.\n" +
"Found: {{code}}",
useHostBridgeWorkspace:
"Use getHostBridgeProvider().workspaceClient.getWorkspacePaths({}) instead of vscode.workspace.workspaceFolders.\n" +
"This provides a consistent abstraction across VSCode and standalone environments.\n" +
"Found: {{code}}",
useHostBridgeShowMessage:
"Use getHostBridgeProvider().windowClient.showMessage instead of the vscode.window.showMessage.\n" +
"This provides a consistent abstraction across VSCode and standalone environments.\n" +
"Found: {{code}}",
useHostBridge:
"Use the host bridge instead of calling vscode APIs directly.\n" +
"This provides a consistent abstraction across VSCode and standalone environments.\n" +
"Found: {{code}}",
useUtils:
"Use utilities in @/utils instead of calling vscode APIs directly.\n" +
"This provides a consistent abstraction across VSCode and standalone environments.\n" +
"Found: {{code}}",
},
schema: [],
},
defaultOptions: [],
create(context) {
// Pattern for checking memberExpressions like vscode.workspace.fs.stat
function checkMemberExpression(node) {
if (isExcluded(context.filename)) {
// Skip if this file is being excluded.
return
}
// For handling nested properties like vscode.workspace.fs.stat
function getFullPropertyPath(node) {
if (node.type !== "MemberExpression") {
return node.name || ""
}
const objectPart = getFullPropertyPath(node.object)
const propertyPart = node.property.name || ""
return objectPart ? `${objectPart}.${propertyPart}` : propertyPart
}
// Check if the expression matches one of our disallowed patterns
if (node.object && node.object.type === "Identifier" && node.object.name === "vscode") {
const fullPath = `vscode.${node.property.name}`
checkDisallowedApi(fullPath, node)
}
// Handle nested expressions like vscode.workspace.fs.stat
else if (node.object && node.object.type === "MemberExpression") {
const fullPath = getFullPropertyPath(node)
// Only proceed if it starts with vscode
if (fullPath.startsWith("vscode.")) {
checkDisallowedApi(fullPath, node)
}
}
}
// Check if an expression matches a disallowed API and report if it does
function checkDisallowedApi(expressionPath, node) {
// Check exact matches
if (disallowedApis[expressionPath]) {
reportViolation(expressionPath, node)
return
}
// Check prefix matches (for nested properties)
for (const disallowedApi in disallowedApis) {
// For direct property access like vscode.workspace.workspaceFolders
if (expressionPath === disallowedApi) {
reportViolation(disallowedApi, node)
return
}
// For method calls like vscode.workspace.asRelativePath(...)
if (expressionPath.startsWith(`${disallowedApi}.`) || expressionPath.startsWith(`${disallowedApi}(`)) {
reportViolation(disallowedApi, node)
return
}
}
}
// Report a violation with the appropriate message
function reportViolation(disallowedApi, node) {
const sourceCode = context.sourceCode
const config = disallowedApis[disallowedApi]
// For method calls, get the whole call expression
let reportNode = node
let parentNode = sourceCode.getAncestors(node).pop()
if (parentNode && parentNode.type === "CallExpression" && parentNode.callee === node) {
reportNode = parentNode
}
const callText = sourceCode.getText(reportNode).trim()
context.report({
node: reportNode,
messageId: config.messageId,
data: {
code: callText,
},
})
}
function isExcluded(filename) {
// Check if current file is in an exception directory or is grpc-client-base.ts
if (path.basename(filename) === "grpc-client-base.ts") {
return true
}
// Skip checking files in src/hosts/vscode or standalone/runtime-files
if (filename.includes("/src/hosts/vscode/")) {
return true
}
if (filename.includes("/standalone/runtime-files/")) {
return true
}
}
return {
// Detect basic member expressions (e.g., vscode.postMessage)
MemberExpression(node) {
checkMemberExpression(node)
},
// Detect property access through destructuring
VariableDeclarator(node) {
// Skip if this file is in an exception directory or is grpc-client-base.ts
if (isExcluded(context.filename)) {
return
}
// Destructuring pattern checks removed as developers don't use the API this way
// They always use direct imports: import * as vscode from "vscode" and direct access: vscode.thing.foo
},
}
},
})
-61
View File
@@ -1,61 +0,0 @@
const { ESLintUtils } = require("@typescript-eslint/utils")
const path = require("path")
const createRule = ESLintUtils.RuleCreator((name) => `https://cline.bot/eslint-rules/${name}`)
module.exports = createRule({
name: "no-vscode-postmessage",
meta: {
type: "problem",
docs: {
description: "Ban vscode.postMessage() calls in favor of gRPC service clients, except in grpc-client-base.ts",
recommended: "error",
},
messages: {
useGrpcClient:
"Use gRPC service clients instead of vscode.postMessage().\n" +
"Example: AccountServiceClient.methodName(RequestType.create({...})) instead of vscode.postMessage({type: '...'}).\n" +
"Found: {{code}}",
},
schema: [],
},
defaultOptions: [],
create(context) {
// Check if current file is grpc-client-base.ts (exception case)
const filename = context.filename
const isGrpcClientBase = path.basename(filename) === "grpc-client-base.ts"
return {
// Detect vscode.postMessage calls
"CallExpression[callee.type='MemberExpression']"(node) {
// Skip if this is grpc-client-base.ts
if (isGrpcClientBase) {
return
}
const callee = node.callee
// Check for vscode.postMessage pattern
if (
callee.object &&
callee.object.type === "Identifier" &&
callee.object.name === "vscode" &&
callee.property &&
callee.property.name === "postMessage"
) {
const sourceCode = context.sourceCode
const callText = sourceCode.getText(node).trim()
context.report({
node,
messageId: "useGrpcClient",
data: {
code: callText,
},
})
}
},
}
},
})
+10
View File
@@ -6,6 +6,7 @@ interface RunDiffEvalOptions {
modelIds: string
systemPromptName: string
validAttemptsPerCase: number
maxAttemptsPerCase?: number
parsingFunction: string
diffEditFunction: string
thinkingBudget: number
@@ -16,6 +17,7 @@ interface RunDiffEvalOptions {
replay: boolean
replayRunId?: string
diffApplyFile?: string
saveLocally: boolean
maxCases?: number
}
@@ -70,10 +72,18 @@ export async function runDiffEvalHandler(options: RunDiffEvalOptions) {
args.push("--verbose")
}
if (options.maxAttemptsPerCase) {
args.push("--max-attempts-per-case", String(options.maxAttemptsPerCase))
}
if (options.maxCases) {
args.push("--max-cases", String(options.maxCases))
}
if (options.saveLocally) {
args.push("--save-locally")
}
try {
console.log(chalk.gray(`Executing: npx tsx ${scriptPath} ${args.join(" ")}`))
+3
View File
@@ -87,6 +87,7 @@ program
.option("--model-ids <model_ids>", "Comma-separated list of model IDs to test")
.option("--system-prompt-name <name>", "The name of the system prompt to use", "basicSystemPrompt")
.option("-n, --valid-attempts-per-case <number>", "Number of valid attempts per test case per model (will retry until this many valid attempts are collected)", "1")
.option("--max-attempts-per-case <number>", "Maximum total attempts per test case (default: 10x valid attempts)")
.option("--max-cases <number>", "Maximum number of test cases to run (limits total cases loaded)")
.option("--parsing-function <name>", "The parsing function to use", "parseAssistantMessageV2")
.option("--diff-edit-function <name>", "The diff editing function to use", "constructNewFileContentV2")
@@ -95,12 +96,14 @@ program
.option("--replay", "Run evaluation from a pre-recorded LLM output, skipping the API call", false)
.option("--replay-run-id <run_id>", "The ID of the run to replay from the database")
.option("--diff-apply-file <filename>", "The name of the diff apply file to use for the replay")
.option("--save-locally", "Save results to local JSON files in addition to database", false)
.option("-v, --verbose", "Enable verbose logging", false)
.action(async (options) => {
try {
const fullOptions = {
...options,
validAttemptsPerCase: parseInt(options.validAttemptsPerCase, 10),
maxAttemptsPerCase: options.maxAttemptsPerCase ? parseInt(options.maxAttemptsPerCase, 10) : undefined,
thinkingBudget: parseInt(options.thinkingBudget, 10),
maxCases: options.maxCases ? parseInt(options.maxCases, 10) : undefined,
}
+24 -12
View File
@@ -11,9 +11,10 @@ import {
import { constructNewFileContent as constructNewFileContent_06_06_25 } from "./diff-apply/diff-06-06-25"
import { constructNewFileContent as constructNewFileContent_06_23_25 } from "./diff-apply/diff-06-23-25"
import { constructNewFileContent as constructNewFileContent_06_25_25 } from "./diff-apply/diff-06-25-25"
import { constructNewFileContent as constructNewFileContent_06_26_25 } from "./diff-apply/diff-06-26-25"
type ParseAssistantMessageFn = (message: string) => AssistantMessageContent[]
type ConstructNewFileContentFn = (diff: string, original: string, strict: boolean) => Promise<string>
type ConstructNewFileContentFn = (diff: string, original: string, strict: boolean) => Promise<string | any>
const parsingFunctions: Record<string, ParseAssistantMessageFn> = {
parseAssistantMessageV1: parseAssistantMessageV1,
@@ -25,9 +26,11 @@ const diffEditingFunctions: Record<string, ConstructNewFileContentFn> = {
"diff-06-06-25": constructNewFileContent_06_06_25,
"diff-06-23-25": constructNewFileContent_06_23_25,
"diff-06-25-25": constructNewFileContent_06_25_25,
"diff-06-26-25": constructNewFileContent_06_26_25,
}
import { TestInput, TestResult, ExtractedToolCall } from "./types"
import { log } from "./helpers"
export { TestInput, TestResult, ExtractedToolCall }
interface StreamResult {
@@ -282,21 +285,21 @@ export async function runSingleEvaluation(input: TestInput): Promise<TestResult>
}
// check that we are editing the correct file path
console.log(`Expected file path: "${originalFilePath}"`);
console.log(`Actual file path used: "${diffToolPath}"`);
log(input.isVerbose, `Expected file path: "${originalFilePath}"`)
log(input.isVerbose, `Actual file path used: "${diffToolPath}"`)
if (diffToolPath !== originalFilePath) {
console.log(`❌ File path mismatch detected!`);
log(input.isVerbose, `❌ File path mismatch detected!`)
// Enhanced logging:
if (streamResult?.assistantMessage) {
console.log(` Full model output (assistantMessage):`);
console.log(` -----------------------------------------`);
console.log(` ${streamResult.assistantMessage}`);
console.log(` -----------------------------------------`);
log(input.isVerbose, ` Full model output (assistantMessage):`)
log(input.isVerbose, ` -----------------------------------------`)
log(input.isVerbose, ` ${streamResult.assistantMessage}`)
log(input.isVerbose, ` -----------------------------------------`)
}
if (toolCall) {
console.log(` Parsed tool call that caused mismatch:`);
console.log(` ${JSON.stringify(toolCall, null, 2)}`);
console.log(` -----------------------------------------`);
log(input.isVerbose, ` Parsed tool call that caused mismatch:`)
log(input.isVerbose, ` ${JSON.stringify(toolCall, null, 2)}`)
log(input.isVerbose, ` -----------------------------------------`)
}
return {
success: false,
@@ -308,10 +311,18 @@ export async function runSingleEvaluation(input: TestInput): Promise<TestResult>
// checking if the diff edit succeeds, if it failed it will throw an error
let diffSuccess = true
let replacementData: any = undefined
try {
await constructNewFileContent(diffToolContent, originalFile, true)
const result = await constructNewFileContent(diffToolContent, originalFile, true)
// Check if result is an object with replacements (new format)
if (typeof result === 'object' && result !== null && 'replacements' in result) {
replacementData = result.replacements
}
// If it's just a string, diffSuccess stays true and replacementData stays undefined
} catch (error: any) {
diffSuccess = false
log(input.isVerbose, `ERROR: ${error}`)
}
return {
@@ -320,6 +331,7 @@ export async function runSingleEvaluation(input: TestInput): Promise<TestResult>
toolCalls: detectedToolCalls,
diffEdit: diffToolContent,
diffEditSuccess: diffSuccess,
replacementData: replacementData,
}
} catch (error: any) {
return {
+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.
+23 -11
View File
@@ -3,10 +3,11 @@ import { parseAssistantMessageV2, AssistantMessageContent } from "./parsing/pars
import { constructNewFileContent as constructNewFileContent_06_06_25 } from "./diff-apply/diff-06-06-25"
import { constructNewFileContent as constructNewFileContent_06_23_25 } from "./diff-apply/diff-06-23-25"
import { constructNewFileContent as constructNewFileContent_06_25_25 } from "./diff-apply/diff-06-25-25"
import { constructNewFileContent as constructNewFileContent_06_26_25 } from "./diff-apply/diff-06-26-25"
import { constructNewFileContent as constructNewFileContentV3 } from "../../src/core/assistant-message/diff"
import { basicSystemPrompt } from "./prompts/basicSystemPrompt-06-06-25"
import { claude4SystemPrompt } from "./prompts/claude4SystemPrompt-06-06-25"
import { formatResponse } from "./helpers"
import { formatResponse, log } from "./helpers"
import { Anthropic } from "@anthropic-ai/sdk"
import * as fs from "fs"
import * as path from "path"
@@ -39,12 +40,6 @@ const encoding = get_encoding("cl100k_base");
let openRouterModelDataGlobal: Record<string, EvalOpenRouterModelInfo> = {}; // Global to store fetched data
function log(isVerbose: boolean, message: string) {
if (isVerbose) {
console.log(message)
}
}
const systemPromptGeneratorLookup: Record<string, ConstructSystemPromptFn> = {
basicSystemPrompt: basicSystemPrompt,
claude4SystemPrompt: claude4SystemPrompt,
@@ -484,6 +479,7 @@ class NodeTestRunner {
"diff-06-06-25": constructNewFileContent_06_06_25,
"diff-06-23-25": constructNewFileContent_06_23_25,
"diff-06-25-25": constructNewFileContent_06_25_25,
"diff-06-26-25": constructNewFileContent_06_26_25,
constructNewFileContentV3: constructNewFileContentV3,
}
const constructNewFileContent = diffEditingFunctions[diffApplyFile]
@@ -639,6 +635,7 @@ class NodeTestRunner {
thinkingBudgetTokens: testConfig.thinking_tokens_budget,
originalDiffEditToolCallMessage: testConfig.replay ? testCase.original_diff_edit_tool_call_message : undefined,
diffApplyFile: testConfig.diff_apply_file,
isVerbose: isVerbose,
}
if (isVerbose) {
@@ -805,8 +802,8 @@ class NodeTestRunner {
log(isVerbose, `Warning: Failed to store result in database: ${error}`);
}
// Safety check to prevent infinite loops - limit to 10 attempts per valid attempt requested
if (totalAttempts >= testConfig.number_of_runs * 10) {
// Safety check to prevent infinite loops - use configurable max attempts limit
if (totalAttempts >= testConfig.max_attempts_per_case) {
log(isVerbose, ` ⚠️ Reached maximum attempts (${totalAttempts}) for test case ${testCase.test_id}. Only got ${validAttempts}/${testConfig.number_of_runs} valid attempts.`);
break;
}
@@ -925,14 +922,16 @@ async function main() {
.option("--model-ids <model_ids>", "Comma-separated list of model IDs to test")
.option("--system-prompt-name <name>", "The name of the system prompt to use", "basicSystemPrompt")
.option("-n, --valid-attempts-per-case <number>", "Number of valid attempts per test case per model (will retry until this many valid attempts are collected)", "1")
.option("--max-attempts-per-case <number>", "Maximum total attempts per test case (default: 10x valid attempts)")
.option("--max-cases <number>", "Maximum number of test cases to run (limits total cases loaded)")
.option("--parsing-function <name>", "The parsing function to use", "parseAssistantMessageV2")
.option("--diff-edit-function <name>", "The diff editing function to use", "constructNewFileContentV2")
.option("--diff-edit-function <name>", "The diff editing function to use", "diff-06-26-25")
.option("--thinking-budget <tokens>", "Set the thinking tokens budget", "0")
.option("--parallel", "Run tests in parallel", false)
.option("--replay", "Run evaluation from a pre-recorded LLM output, skipping the API call", false)
.option("--replay-run-id <run_id>", "The ID of the run to replay from the database")
.option("--diff-apply-file <filename>", "The name of the diff apply file to use for the replay")
.option("--save-locally", "Save results to local JSON files in addition to database", false)
.option("-v, --verbose", "Enable verbose logging", false)
.option("--max-concurrency <number>", "Maximum number of parallel requests", "80")
@@ -943,6 +942,7 @@ async function main() {
const isVerbose = options.verbose
const testPath = options.testPath
const outputPath = options.outputPath
const saveLocally = options.saveLocally
const maxConcurrency = parseInt(options.maxConcurrency, 10);
// Parse model IDs from comma-separated string
@@ -953,6 +953,11 @@ async function main() {
}
const validAttemptsPerCase = parseInt(options.validAttemptsPerCase, 10);
// Compute dynamic default for max attempts: 10x valid attempts if not specified
const maxAttemptsPerCase = options.maxAttemptsPerCase
? parseInt(options.maxAttemptsPerCase, 10)
: validAttemptsPerCase * 10;
const runner = new NodeTestRunner(options.replay || !!options.replayRunId)
@@ -1058,6 +1063,7 @@ async function main() {
model_id: modelId,
system_prompt_name: options.systemPromptName,
number_of_runs: validAttemptsPerCase,
max_attempts_per_case: maxAttemptsPerCase,
parsing_function: options.parsingFunction,
diff_edit_function: options.diffEditFunction,
thinking_tokens_budget: parseInt(options.thinkingBudget, 10),
@@ -1125,7 +1131,7 @@ async function main() {
remainingTasks = remainingTasks.filter(task => {
const taskId = `${task.modelId}-${task.testCase.test_id}`;
if (taskStates[taskId].total >= validAttemptsPerCase * 10) {
if (taskStates[taskId].total >= task.testConfig.max_attempts_per_case) {
log(isVerbose, ` ⚠️ Reached maximum attempts for ${task.testCase.test_id} with ${task.modelId}.`);
return false;
}
@@ -1150,6 +1156,12 @@ async function main() {
const durationSeconds = ((endTime - startTime) / 1000).toFixed(2)
log(isVerbose, `\n-Total execution time: ${durationSeconds} seconds`)
// Save results locally if requested
if (saveLocally) {
runner.saveTestResults(results, outputPath);
log(isVerbose, `✓ Results also saved to JSON files in ${outputPath}`);
}
log(isVerbose, `\n✓ All results stored in database. Use the dashboard to view results.`)
} catch (error) {
console.error("\nError running tests:", error)
+12 -2
View File
@@ -937,8 +937,18 @@ def main():
# Build current URL
# Dynamically derive the base URL
server_address = st.server.server_address if hasattr(st.server, 'server_address') else "localhost"
server_port = st.server.server_port if hasattr(st.server, 'server_port') else "8501"
try:
# For older Streamlit versions
server_address = st.server.server_address
server_port = st.server.server_port
except AttributeError:
# Fallback for newer Streamlit versions where st.server is removed
# We can't reliably get the server address/port from within the script anymore.
# We'll default to localhost and the default port.
# The user can see the correct network URL in the terminal.
server_address = "localhost"
server_port = 8501
base_url = f"http://{server_address}:{server_port}"
current_url = f"{base_url}/?run_id={st.session_state.selected_run_id}"
if st.session_state.drill_down_model:
@@ -0,0 +1,960 @@
const SEARCH_BLOCK_START = "------- SEARCH"
const SEARCH_BLOCK_END = "======="
const REPLACE_BLOCK_END = "+++++++ REPLACE"
const SEARCH_BLOCK_CHAR = "-"
const REPLACE_BLOCK_CHAR = "+"
const LEGACY_SEARCH_BLOCK_CHAR = "<"
const LEGACY_REPLACE_BLOCK_CHAR = ">"
// Replace the exact string constants with flexible regex patterns
const SEARCH_BLOCK_START_REGEX = /^[-]{3,} SEARCH>?$/
const LEGACY_SEARCH_BLOCK_START_REGEX = /^[<]{3,} SEARCH>?$/
const SEARCH_BLOCK_END_REGEX = /^[=]{3,}$/
const REPLACE_BLOCK_END_REGEX = /^[+]{3,} REPLACE>?$/
const LEGACY_REPLACE_BLOCK_END_REGEX = /^[>]{3,} REPLACE>?$/
// Similarity thresholds for block anchor fallback matching
const SINGLE_CANDIDATE_SIMILARITY_THRESHOLD = 0.0
const MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD = 0.0
/**
* Levenshtein distance algorithm implementation
*/
function levenshtein(a: string, b: string): number {
// Handle empty strings
if (a === "" || b === "") {
return Math.max(a.length, b.length)
}
const matrix = Array.from({ length: a.length + 1 }, (_, i) =>
Array.from({ length: b.length + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)),
)
for (let i = 1; i <= a.length; i++) {
for (let j = 1; j <= b.length; j++) {
const cost = a[i - 1] === b[j - 1] ? 0 : 1
matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost)
}
}
return matrix[a.length][b.length]
}
// Helper functions to check if a line matches the flexible patterns
function isSearchBlockStart(line: string): boolean {
return SEARCH_BLOCK_START_REGEX.test(line) || LEGACY_SEARCH_BLOCK_START_REGEX.test(line)
}
function isSearchBlockEnd(line: string): boolean {
return SEARCH_BLOCK_END_REGEX.test(line)
}
function isReplaceBlockEnd(line: string): boolean {
return REPLACE_BLOCK_END_REGEX.test(line) || LEGACY_REPLACE_BLOCK_END_REGEX.test(line)
}
/**
* Attempts a line-trimmed fallback match for the given search content in the original content.
* It tries to match `searchContent` lines against a block of lines in `originalContent` starting
* from `lastProcessedIndex`. Lines are matched by trimming leading/trailing whitespace and ensuring
* they are identical afterwards.
*
* Returns [matchIndexStart, matchIndexEnd] if found, or false if not found.
*/
function lineTrimmedFallbackMatch(originalContent: string, searchContent: string, startIndex: number): [number, number] | false {
// Split both contents into lines
const originalLines = originalContent.split("\n")
const searchLines = searchContent.split("\n")
// Trim trailing empty line if exists (from the trailing \n in searchContent)
if (searchLines[searchLines.length - 1] === "") {
searchLines.pop()
}
// Find the line number where startIndex falls
let startLineNum = 0
let currentIndex = 0
while (currentIndex < startIndex && startLineNum < originalLines.length) {
currentIndex += originalLines[startLineNum].length + 1 // +1 for \n
startLineNum++
}
// For each possible starting position in original content
for (let i = startLineNum; i <= originalLines.length - searchLines.length; i++) {
let matches = true
// Try to match all search lines from this position
for (let j = 0; j < searchLines.length; j++) {
const originalTrimmed = originalLines[i + j].trim()
const searchTrimmed = searchLines[j].trim()
if (originalTrimmed !== searchTrimmed) {
matches = false
break
}
}
// If we found a match, calculate the exact character positions
if (matches) {
// Find start character index
let matchStartIndex = 0
for (let k = 0; k < i; k++) {
matchStartIndex += originalLines[k].length + 1 // +1 for \n
}
// Find end character index
let matchEndIndex = matchStartIndex
for (let k = 0; k < searchLines.length; k++) {
matchEndIndex += originalLines[i + k].length + 1 // +1 for \n
}
return [matchStartIndex, matchEndIndex]
}
}
return false
}
/**
* Attempts to match blocks of code by using the first and last lines as anchors,
* with similarity checking to prevent false positives.
* This is a third-tier fallback strategy that helps match blocks where we can identify
* the correct location by matching the beginning and end, even if the exact content
* differs slightly.
*
* The matching strategy:
* 1. Only attempts to match blocks of 3 or more lines to avoid false positives
* 2. Extracts from the search content:
* - First line as the "start anchor"
* - Last line as the "end anchor"
* 3. Collects all candidate positions where both anchors match
* 4. Uses levenshtein distance to calculate similarity of middle lines
* 5. Returns match only if similarity meets threshold requirements
*
* This approach is particularly useful for matching blocks of code where:
* - The exact content might have minor differences
* - The beginning and end of the block are distinctive enough to serve as anchors
* - The overall structure (number of lines) remains the same
* - The middle content is reasonably similar (prevents false positives)
*
* @param originalContent - The full content of the original file
* @param searchContent - The content we're trying to find in the original file
* @param startIndex - The character index in originalContent where to start searching
* @returns A tuple of [startIndex, endIndex] if a match is found, false otherwise
*/
function blockAnchorFallbackMatch(originalContent: string, searchContent: string, startIndex: number): [number, number, number] | false {
const originalLines = originalContent.split("\n")
const searchLines = searchContent.split("\n")
// Only use this approach for blocks of 3+ lines
if (searchLines.length < 3) {
return false
}
// Trim trailing empty line if exists
if (searchLines[searchLines.length - 1] === "") {
searchLines.pop()
}
const firstLineSearch = searchLines[0].trim()
const lastLineSearch = searchLines[searchLines.length - 1].trim()
const searchBlockSize = searchLines.length
// Find the line number where startIndex falls
let startLineNum = 0
let currentIndex = 0
while (currentIndex < startIndex && startLineNum < originalLines.length) {
currentIndex += originalLines[startLineNum].length + 1
startLineNum++
}
// Collect all candidate positions
const candidates: number[] = []
for (let i = startLineNum; i <= originalLines.length - searchBlockSize; i++) {
if (originalLines[i].trim() === firstLineSearch && originalLines[i + searchBlockSize - 1].trim() === lastLineSearch) {
candidates.push(i)
}
}
// Return immediately if no candidates
if (candidates.length === 0) {
return false
}
// Handle single candidate scenario (using relaxed threshold)
if (candidates.length === 1) {
const i = candidates[0]
let similarity = 0
let linesToCheck = searchBlockSize - 2
for (let j = 1; j < searchBlockSize - 1; j++) {
const originalLine = originalLines[i + j].trim()
const searchLine = searchLines[j].trim()
const maxLen = Math.max(originalLine.length, searchLine.length)
if (maxLen === 0) {
continue
}
const distance = levenshtein(originalLine, searchLine)
similarity += (1 - distance / maxLen) / linesToCheck
// Exit early when threshold is reached
if (similarity >= SINGLE_CANDIDATE_SIMILARITY_THRESHOLD) {
break
}
}
if (similarity >= SINGLE_CANDIDATE_SIMILARITY_THRESHOLD) {
let matchStartIndex = 0
for (let k = 0; k < i; k++) {
matchStartIndex += originalLines[k].length + 1
}
let matchEndIndex = matchStartIndex
for (let k = 0; k < searchBlockSize; k++) {
matchEndIndex += originalLines[i + k].length + 1
}
return [matchStartIndex, matchEndIndex, similarity]
}
return false
}
// Calculate similarity for multiple candidates
let bestMatchIndex = -1
let maxSimilarity = -1
for (const i of candidates) {
let similarity = 0
for (let j = 1; j < searchBlockSize - 1; j++) {
const originalLine = originalLines[i + j].trim()
const searchLine = searchLines[j].trim()
const maxLen = Math.max(originalLine.length, searchLine.length)
if (maxLen === 0) {
continue
}
const distance = levenshtein(originalLine, searchLine)
similarity += 1 - distance / maxLen
}
similarity /= searchBlockSize - 2 // Average similarity
if (similarity > maxSimilarity) {
maxSimilarity = similarity
bestMatchIndex = i
}
}
// Threshold judgment
if (maxSimilarity >= MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD) {
const i = bestMatchIndex
let matchStartIndex = 0
for (let k = 0; k < i; k++) {
matchStartIndex += originalLines[k].length + 1
}
let matchEndIndex = matchStartIndex
for (let k = 0; k < searchBlockSize; k++) {
matchEndIndex += originalLines[i + k].length + 1
}
return [matchStartIndex, matchEndIndex, maxSimilarity]
}
return false
}
/**
* This function reconstructs the file content by applying a streamed diff (in a
* specialized SEARCH/REPLACE block format) to the original file content. It is designed
* to handle both incremental updates and the final resulting file after all chunks have
* been processed.
*
* The diff format is a custom structure that uses three markers to define changes:
*
* ------- SEARCH
* [Exact content to find in the original file]
* =======
* [Content to replace with]
* +++++++ REPLACE
*
* Behavior and Assumptions:
* 1. The file is processed chunk-by-chunk. Each chunk of `diffContent` may contain
* partial or complete SEARCH/REPLACE blocks. By calling this function with each
* incremental chunk (with `isFinal` indicating the last chunk), the final reconstructed
* file content is produced.
*
* 2. Matching Strategy (in order of attempt):
* a. Exact Match: First attempts to find the exact SEARCH block text in the original file
* b. Line-Trimmed Match: Falls back to line-by-line comparison ignoring leading/trailing whitespace
* c. Block Anchor Match: For blocks of 3+ lines, tries to match using first/last lines as anchors
* If all matching strategies fail, an error is thrown.
*
* 3. Empty SEARCH Section:
* - If SEARCH is empty and the original file is empty, this indicates creating a new file
* (pure insertion).
* - If SEARCH is empty and the original file is not empty, this indicates a complete
* file replacement (the entire original content is considered matched and replaced).
*
* 4. Applying Changes:
* - Before encountering the "=======" marker, lines are accumulated as search content.
* - After "=======" and before ">>>>>>> REPLACE", lines are accumulated as replacement content.
* - Once the block is complete (">>>>>>> REPLACE"), the matched section in the original
* file is replaced with the accumulated replacement lines, and the position in the original
* file is advanced.
*
* 5. Incremental Output:
* - As soon as the match location is found and we are in the REPLACE section, each new
* replacement line is appended to the result so that partial updates can be viewed
* incrementally.
*
* 6. Partial Markers:
* - If the final line of the chunk looks like it might be part of a marker but is not one
* of the known markers, it is removed. This prevents incomplete or partial markers
* from corrupting the output.
*
* 7. Finalization:
* - Once all chunks have been processed (when `isFinal` is true), any remaining original
* content after the last replaced section is appended to the result.
* - Trailing newlines are not forcibly added. The code tries to output exactly what is specified.
*
* Errors:
* - If the search block cannot be matched using any of the available matching strategies,
* an error is thrown.
*/
export async function constructNewFileContent(
diffContent: string,
originalContent: string,
isFinal: boolean,
version: "v1" | "v2" = "v1",
): Promise<any> {
const constructor = constructNewFileContentVersionMapping[version]
if (!constructor) {
throw new Error(`Invalid version '${version}' for file content constructor`)
}
return constructor(diffContent, originalContent, isFinal)
}
const constructNewFileContentVersionMapping: Record<
string,
(diffContent: string, originalContent: string, isFinal: boolean) => Promise<any>
> = {
v1: constructNewFileContentV1,
v2: constructNewFileContentV2,
} as const
async function constructNewFileContentV1(diffContent: string, originalContent: string, isFinal: boolean): Promise<{
content: string;
replacements: Array<{
start: number;
end: number;
content: string;
method: string;
similarity: number;
searchContent: string;
matchedText: string;
}>;
}> {
let result = ""
let lastProcessedIndex = 0
let currentSearchContent = ""
let currentReplaceContent = ""
let inSearch = false
let inReplace = false
let searchMatchIndex = -1
let searchEndIndex = -1
let matchMethod = ""
let similarityScore = -1.0
// Track all replacements to handle out-of-order edits
let replacements: Array<{
start: number;
end: number;
content: string;
method: string;
similarity: number;
searchContent: string;
matchedText: string;
}> = []
let pendingOutOfOrderReplacement = false
let lines = diffContent.split("\n")
// If the last line looks like a partial marker but isn't recognized,
// remove it because it might be incomplete.
const lastLine = lines[lines.length - 1]
if (
lines.length > 0 &&
(lastLine.startsWith(SEARCH_BLOCK_CHAR) ||
lastLine.startsWith(LEGACY_SEARCH_BLOCK_CHAR) ||
lastLine.startsWith("=") ||
lastLine.startsWith(REPLACE_BLOCK_CHAR) ||
lastLine.startsWith(LEGACY_REPLACE_BLOCK_CHAR)) &&
!isSearchBlockStart(lastLine) &&
!isSearchBlockEnd(lastLine) &&
!isReplaceBlockEnd(lastLine)
) {
lines.pop()
}
for (const line of lines) {
if (isSearchBlockStart(line)) {
inSearch = true
currentSearchContent = ""
currentReplaceContent = ""
continue
}
if (isSearchBlockEnd(line)) {
inSearch = false
inReplace = true
// Remove trailing linebreak for adding the === marker
// if (currentSearchContent.endsWith("\r\n")) {
// currentSearchContent = currentSearchContent.slice(0, -2)
// } else if (currentSearchContent.endsWith("\n")) {
// currentSearchContent = currentSearchContent.slice(0, -1)
// }
if (!currentSearchContent) {
// Empty search block
if (originalContent.length === 0) {
// New file scenario: nothing to match, just start inserting
searchMatchIndex = 0
searchEndIndex = 0
matchMethod = "empty_new_file"
} else {
// ERROR: Empty search block with non-empty file indicates malformed SEARCH marker
throw new Error(
"Empty SEARCH block detected with non-empty file. This usually indicates a malformed SEARCH marker.\n" +
"Please ensure your SEARCH marker follows the correct format:\n" +
"- Use '------- SEARCH' (7+ dashes + space + SEARCH)\n",
)
}
} else {
// Add check for inefficient full-file search
// if (currentSearchContent.trim() === originalContent.trim()) {
// throw new Error(
// "The SEARCH block contains the entire file content. Please either:\n" +
// "1. Use an empty SEARCH block to replace the entire file, or\n" +
// "2. Make focused changes to specific parts of the file that need modification.",
// )
// }
// Exact search match scenario
const exactIndex = originalContent.indexOf(currentSearchContent, lastProcessedIndex)
if (exactIndex !== -1) {
searchMatchIndex = exactIndex
searchEndIndex = exactIndex + currentSearchContent.length
matchMethod = "exact_match"
} else {
// Attempt fallback line-trimmed matching
const lineMatch = lineTrimmedFallbackMatch(originalContent, currentSearchContent, lastProcessedIndex)
if (lineMatch) {
;[searchMatchIndex, searchEndIndex] = lineMatch
matchMethod = "line_trimmed_fallback"
} else {
// Try block anchor fallback for larger blocks
const blockMatch = blockAnchorFallbackMatch(originalContent, currentSearchContent, lastProcessedIndex)
if (blockMatch) {
;[searchMatchIndex, searchEndIndex, similarityScore] = blockMatch
matchMethod = "block_anchor_fallback"
} else {
// Last resort: search the entire file from the beginning
const fullFileIndex = originalContent.indexOf(currentSearchContent, 0)
if (fullFileIndex !== -1) {
// Found in the file - could be out of order
searchMatchIndex = fullFileIndex
searchEndIndex = fullFileIndex + currentSearchContent.length
matchMethod = "full_file_search"
if (searchMatchIndex < lastProcessedIndex) {
pendingOutOfOrderReplacement = true
}
} else {
throw new Error(
`The SEARCH block:\n${currentSearchContent.trimEnd()}\n...does not match anything in the file.`,
)
}
}
}
}
}
// Check if this is an out-of-order replacement
if (searchMatchIndex < lastProcessedIndex) {
pendingOutOfOrderReplacement = true
}
// For in-order replacements, output everything up to the match location
if (!pendingOutOfOrderReplacement) {
result += originalContent.slice(lastProcessedIndex, searchMatchIndex)
}
continue
}
if (isReplaceBlockEnd(line)) {
// Finished one replace block
if (searchMatchIndex === -1) {
throw new Error(
`The SEARCH block:\n${currentSearchContent.trimEnd()}\n...is malformatted.`,
)
}
// Store this replacement
replacements.push({
start: searchMatchIndex,
end: searchEndIndex,
content: currentReplaceContent,
method: matchMethod,
similarity: similarityScore,
searchContent: currentSearchContent,
matchedText: originalContent.slice(searchMatchIndex, searchEndIndex),
})
// If this was an in-order replacement, advance lastProcessedIndex
if (!pendingOutOfOrderReplacement) {
lastProcessedIndex = searchEndIndex
}
// Reset for next block
inSearch = false
inReplace = false
currentSearchContent = ""
currentReplaceContent = ""
searchMatchIndex = -1
searchEndIndex = -1
similarityScore = -1.0
pendingOutOfOrderReplacement = false
continue
}
// Accumulate content for search or replace
// (currentReplaceContent is not being used for anything right now since we directly append to result.)
// (We artificially add a linebreak since we split on \n at the beginning. In order to not include a trailing linebreak in the final search/result blocks we need to remove it before using them. This allows for partial line matches to be correctly identified.)
// NOTE: search/replace blocks must be arranged in the order they appear in the file due to how we build the content using lastProcessedIndex. We also cannot strip the trailing newline since for non-partial lines it would remove the linebreak from the original content. (If we remove end linebreak from search, then we'd also have to remove it from replace but we can't know if it's a partial line or not since the model may be using the line break to indicate the end of the block rather than as part of the search content.) We require the model to output full lines in order for our fallbacks to work as well.
if (inSearch) {
currentSearchContent += line + "\n"
} else if (inReplace) {
currentReplaceContent += line + "\n"
// Only output replacement lines immediately for in-order replacements
if (searchMatchIndex !== -1 && !pendingOutOfOrderReplacement) {
result += line + "\n"
}
}
}
// If this is the final chunk, we need to apply all replacements and build the final result
if (isFinal) {
// Handle the case where we're still in replace mode when processing ends
// and this is the final chunk - treat it as if we encountered the REPLACE marker
if (inReplace && searchMatchIndex !== -1) {
// Store this replacement
replacements.push({
start: searchMatchIndex,
end: searchEndIndex,
content: currentReplaceContent,
method: matchMethod,
similarity: similarityScore,
searchContent: currentSearchContent,
matchedText: originalContent.slice(searchMatchIndex, searchEndIndex),
})
// If this was an in-order replacement, advance lastProcessedIndex
if (!pendingOutOfOrderReplacement) {
lastProcessedIndex = searchEndIndex
}
// Reset state
inSearch = false
inReplace = false
currentSearchContent = ""
currentReplaceContent = ""
searchMatchIndex = -1
searchEndIndex = -1
pendingOutOfOrderReplacement = false
}
// end of handling missing replace marker
// Sort replacements by start position
replacements.sort((a, b) => a.start - b.start)
// Rebuild the entire result by applying all replacements
result = ""
let currentPos = 0
for (const replacement of replacements) {
// Add original content up to this replacement
result += originalContent.slice(currentPos, replacement.start)
// Add the replacement content
result += replacement.content
// Move position to after the replaced section
currentPos = replacement.end
}
// Add any remaining original content
result += originalContent.slice(currentPos)
}
// For testing - return debug info
return {
content: result,
replacements: replacements
}
}
enum ProcessingState {
Idle = 0,
StateSearch = 1 << 0,
StateReplace = 1 << 1,
}
class NewFileContentConstructor {
private originalContent: string
private isFinal: boolean
private state: number
private pendingNonStandardLines: string[]
private result: string
private lastProcessedIndex: number
private currentSearchContent: string
private currentReplaceContent: string
private searchMatchIndex: number
private searchEndIndex: number
constructor(originalContent: string, isFinal: boolean) {
this.originalContent = originalContent
this.isFinal = isFinal
this.pendingNonStandardLines = []
this.result = ""
this.lastProcessedIndex = 0
this.state = ProcessingState.Idle
this.currentSearchContent = ""
this.currentReplaceContent = ""
this.searchMatchIndex = -1
this.searchEndIndex = -1
}
private resetForNextBlock() {
// Reset for next block
this.state = ProcessingState.Idle
this.currentSearchContent = ""
this.currentReplaceContent = ""
this.searchMatchIndex = -1
this.searchEndIndex = -1
}
private findLastMatchingLineIndex(regx: RegExp, lineLimit: number) {
for (let i = lineLimit; i > 0; ) {
i--
if (this.pendingNonStandardLines[i].match(regx)) {
return i
}
}
return -1
}
private updateProcessingState(newState: ProcessingState) {
const isValidTransition =
(this.state === ProcessingState.Idle && newState === ProcessingState.StateSearch) ||
(this.state === ProcessingState.StateSearch && newState === ProcessingState.StateReplace)
if (!isValidTransition) {
throw new Error(
`Invalid state transition.\n` +
"Valid transitions are:\n" +
"- Idle → StateSearch\n" +
"- StateSearch → StateReplace",
)
}
this.state |= newState
}
private isStateActive(state: ProcessingState): boolean {
return (this.state & state) === state
}
private activateReplaceState() {
this.updateProcessingState(ProcessingState.StateReplace)
}
private activateSearchState() {
this.updateProcessingState(ProcessingState.StateSearch)
this.currentSearchContent = ""
this.currentReplaceContent = ""
}
private isSearchingActive(): boolean {
return this.isStateActive(ProcessingState.StateSearch)
}
private isReplacingActive(): boolean {
return this.isStateActive(ProcessingState.StateReplace)
}
private hasPendingNonStandardLines(pendingNonStandardLineLimit: number): boolean {
return this.pendingNonStandardLines.length - pendingNonStandardLineLimit < this.pendingNonStandardLines.length
}
public processLine(line: string) {
this.internalProcessLine(line, true, this.pendingNonStandardLines.length)
}
public getResult() {
// If this is the final chunk, append any remaining original content
if (this.isFinal && this.lastProcessedIndex < this.originalContent.length) {
this.result += this.originalContent.slice(this.lastProcessedIndex)
}
if (this.isFinal && this.state !== ProcessingState.Idle) {
throw new Error("File processing incomplete - SEARCH/REPLACE operations still active during finalization")
}
return this.result
}
private internalProcessLine(
line: string,
canWritependingNonStandardLines: boolean,
pendingNonStandardLineLimit: number,
): number {
let removeLineCount = 0
if (isSearchBlockStart(line)) {
removeLineCount = this.trimPendingNonStandardTrailingEmptyLines(pendingNonStandardLineLimit)
if (removeLineCount > 0) {
pendingNonStandardLineLimit = pendingNonStandardLineLimit - removeLineCount
}
if (this.hasPendingNonStandardLines(pendingNonStandardLineLimit)) {
this.tryFixSearchReplaceBlock(pendingNonStandardLineLimit)
canWritependingNonStandardLines && (this.pendingNonStandardLines.length = 0)
}
this.activateSearchState()
} else if (isSearchBlockEnd(line)) {
// 校验非标内容
if (!this.isSearchingActive()) {
this.tryFixSearchBlock(pendingNonStandardLineLimit)
canWritependingNonStandardLines && (this.pendingNonStandardLines.length = 0)
}
this.activateReplaceState()
this.beforeReplace()
} else if (isReplaceBlockEnd(line)) {
if (!this.isReplacingActive()) {
this.tryFixReplaceBlock(pendingNonStandardLineLimit)
canWritependingNonStandardLines && (this.pendingNonStandardLines.length = 0)
}
this.lastProcessedIndex = this.searchEndIndex
this.resetForNextBlock()
} else {
// Accumulate content for search or replace
// (currentReplaceContent is not being used for anything right now since we directly append to result.)
// (We artificially add a linebreak since we split on \n at the beginning. In order to not include a trailing linebreak in the final search/result blocks we need to remove it before using them. This allows for partial line matches to be correctly identified.)
// NOTE: search/replace blocks must be arranged in the order they appear in the file due to how we build the content using lastProcessedIndex. We also cannot strip the trailing newline since for non-partial lines it would remove the linebreak from the original content. (If we remove end linebreak from search, then we'd also have to remove it from replace but we can't know if it's a partial line or not since the model may be using the line break to indicate the end of the block rather than as part of the search content.) We require the model to output full lines in order for our fallbacks to work as well.
if (this.isReplacingActive()) {
this.currentReplaceContent += line + "\n"
// Output replacement lines immediately if we know the insertion point
if (this.searchMatchIndex !== -1) {
this.result += line + "\n"
}
} else if (this.isSearchingActive()) {
this.currentSearchContent += line + "\n"
} else {
let appendToPendingNonStandardLines = canWritependingNonStandardLines
if (appendToPendingNonStandardLines) {
// 处理非标内容
this.pendingNonStandardLines.push(line)
}
}
}
return removeLineCount
}
private beforeReplace() {
// Remove trailing linebreak for adding the === marker
// if (currentSearchContent.endsWith("\r\n")) {
// currentSearchContent = currentSearchContent.slice(0, -2)
// } else if (currentSearchContent.endsWith("\n")) {
// currentSearchContent = currentSearchContent.slice(0, -1)
// }
if (!this.currentSearchContent) {
// Empty search block
if (this.originalContent.length === 0) {
// New file scenario: nothing to match, just start inserting
this.searchMatchIndex = 0
this.searchEndIndex = 0
} else {
// Complete file replacement scenario: treat the entire file as matched
this.searchMatchIndex = 0
this.searchEndIndex = this.originalContent.length
}
} else {
// Add check for inefficient full-file search
// if (currentSearchContent.trim() === originalContent.trim()) {
// throw new Error(
// "The SEARCH block contains the entire file content. Please either:\n" +
// "1. Use an empty SEARCH block to replace the entire file, or\n" +
// "2. Make focused changes to specific parts of the file that need modification.",
// )
// }
// Exact search match scenario
const exactIndex = this.originalContent.indexOf(this.currentSearchContent, this.lastProcessedIndex)
if (exactIndex !== -1) {
this.searchMatchIndex = exactIndex
this.searchEndIndex = exactIndex + this.currentSearchContent.length
} else {
// Attempt fallback line-trimmed matching
const lineMatch = lineTrimmedFallbackMatch(
this.originalContent,
this.currentSearchContent,
this.lastProcessedIndex,
)
if (lineMatch) {
;[this.searchMatchIndex, this.searchEndIndex] = lineMatch
} else {
// Try block anchor fallback for larger blocks
const blockMatch = blockAnchorFallbackMatch(
this.originalContent,
this.currentSearchContent,
this.lastProcessedIndex,
)
if (blockMatch) {
;[this.searchMatchIndex, this.searchEndIndex, /* ignore similarity */] = blockMatch
} else {
throw new Error(
`The SEARCH block:\n${this.currentSearchContent.trimEnd()}\n...does not match anything in the file.`,
)
}
}
}
}
if (this.searchMatchIndex < this.lastProcessedIndex) {
throw new Error(
`The SEARCH block:\n${this.currentSearchContent.trimEnd()}\n...matched an incorrect content in the file.`,
)
}
// Output everything up to the match location
this.result += this.originalContent.slice(this.lastProcessedIndex, this.searchMatchIndex)
}
private tryFixSearchBlock(lineLimit: number): number {
let removeLineCount = 0
if (lineLimit < 0) {
lineLimit = this.pendingNonStandardLines.length
}
if (!lineLimit) {
throw new Error("Invalid SEARCH/REPLACE block structure - no lines available to process")
}
let searchTagRegexp = /^([-]{3,}|[<]{3,}) SEARCH$/
const searchTagIndex = this.findLastMatchingLineIndex(searchTagRegexp, lineLimit)
if (searchTagIndex !== -1) {
let fixLines = this.pendingNonStandardLines.slice(searchTagIndex, lineLimit)
fixLines[0] = SEARCH_BLOCK_START
for (const line of fixLines) {
removeLineCount += this.internalProcessLine(line, false, searchTagIndex)
}
} else {
throw new Error(
`Invalid REPLACE marker detected - could not find matching SEARCH block starting from line ${searchTagIndex + 1}`,
)
}
return removeLineCount
}
private tryFixReplaceBlock(lineLimit: number): number {
let removeLineCount = 0
if (lineLimit < 0) {
lineLimit = this.pendingNonStandardLines.length
}
if (!lineLimit) {
throw new Error()
}
let replaceBeginTagRegexp = /^[=]{3,}$/
const replaceBeginTagIndex = this.findLastMatchingLineIndex(replaceBeginTagRegexp, lineLimit)
if (replaceBeginTagIndex !== -1) {
// // 校验非标内容
// if (!this.isSearchingActive()) {
// removeLineCount += this.tryFixSearchBlock(replaceBeginTagIndex)
// }
let fixLines = this.pendingNonStandardLines.slice(replaceBeginTagIndex - removeLineCount, lineLimit - removeLineCount)
fixLines[0] = SEARCH_BLOCK_END
for (const line of fixLines) {
removeLineCount += this.internalProcessLine(line, false, replaceBeginTagIndex - removeLineCount)
}
} else {
throw new Error(`Malformed REPLACE block - missing valid separator after line ${replaceBeginTagIndex + 1}`)
}
return removeLineCount
}
private tryFixSearchReplaceBlock(lineLimit: number): number {
let removeLineCount = 0
if (lineLimit < 0) {
lineLimit = this.pendingNonStandardLines.length
}
if (!lineLimit) {
throw new Error()
}
let replaceEndTagRegexp = /^([+]{3,}|[>]{3,}) REPLACE$/
const replaceEndTagIndex = this.findLastMatchingLineIndex(replaceEndTagRegexp, lineLimit)
const likeReplaceEndTag = replaceEndTagIndex === lineLimit - 1
if (likeReplaceEndTag) {
// // 校验非标内容
// if (!this.isReplacingActive()) {
// removeLineCount += this.tryFixReplaceBlock(replaceEndTagIndex)
// }
let fixLines = this.pendingNonStandardLines.slice(replaceEndTagIndex - removeLineCount, lineLimit - removeLineCount)
fixLines[fixLines.length - 1] = REPLACE_BLOCK_END
for (const line of fixLines) {
removeLineCount += this.internalProcessLine(line, false, replaceEndTagIndex - removeLineCount)
}
} else {
throw new Error("Malformed SEARCH/REPLACE block structure: Missing valid closing REPLACE marker")
}
return removeLineCount
}
/**
* Removes trailing empty lines from the pendingNonStandardLines array
* @param lineLimit - The index to start checking from (exclusive).
* Removes empty lines from lineLimit-1 backwards.
* @returns The number of empty lines removed
*/
private trimPendingNonStandardTrailingEmptyLines(lineLimit: number): number {
let removedCount = 0
let i = Math.min(lineLimit, this.pendingNonStandardLines.length) - 1
while (i >= 0 && this.pendingNonStandardLines[i].trim() === "") {
this.pendingNonStandardLines.pop()
removedCount++
i--
}
return removedCount
}
}
export async function constructNewFileContentV2(diffContent: string, originalContent: string, isFinal: boolean): Promise<string> {
let newFileContentConstructor = new NewFileContentConstructor(originalContent, isFinal)
let lines = diffContent.split("\n")
// If the last line looks like a partial marker but isn't recognized,
// remove it because it might be incomplete.
const lastLine = lines[lines.length - 1]
if (
lines.length > 0 &&
(lastLine.startsWith(SEARCH_BLOCK_CHAR) ||
lastLine.startsWith(LEGACY_SEARCH_BLOCK_CHAR) ||
lastLine.startsWith("=") ||
lastLine.startsWith(REPLACE_BLOCK_CHAR) ||
lastLine.startsWith(LEGACY_REPLACE_BLOCK_CHAR)) &&
lastLine !== SEARCH_BLOCK_START &&
lastLine !== SEARCH_BLOCK_END &&
lastLine !== REPLACE_BLOCK_END
) {
lines.pop()
}
for (const line of lines) {
newFileContentConstructor.processLine(line)
}
let result = newFileContentConstructor.getResult()
return result
}
+6
View File
@@ -23,3 +23,9 @@ export const formatResponse = {
return formatImagesIntoBlocks(images)
},
}
export function log(isVerbose: boolean, message: string) {
if (isVerbose) {
console.log(message)
}
}
+3
View File
@@ -29,6 +29,7 @@ export interface TestConfig {
model_id: string
system_prompt_name: string
number_of_runs: number
max_attempts_per_case: number
parsing_function: string
diff_edit_function: string
thinking_tokens_budget: number
@@ -81,6 +82,7 @@ export interface TestResult {
diffEdit?: string
toolCalls?: ExtractedToolCall[]
diffEditSuccess?: boolean
replacementData?: any
error?: string
errorString?: string
}
@@ -102,4 +104,5 @@ export interface TestInput {
thinkingBudgetTokens: number
originalDiffEditToolCallMessage?: string
diffApplyFile?: string
isVerbose: boolean
}
+5549 -14305
View File
File diff suppressed because it is too large Load Diff
+16 -11
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.0",
"version": "3.19.7",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -330,12 +330,12 @@
"watch:esbuild": "node esbuild.js --watch",
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
"package": "npm run check-types && npm run build:webview && npm run lint && node esbuild.js --production",
"protos": "node proto/build-proto.js && node scripts/generate-server-setup.mjs && node scripts/generate-host-bridge-client.mjs",
"protos": "node scripts/build-proto.mjs && node scripts/generate-server-setup.mjs && node scripts/generate-host-bridge-client.mjs",
"postprotos": "prettier src/shared/proto src/core/controller src/hosts/ webview-ui/src/services src/generated --write --log-level warn",
"clean": "rimraf dist dist-standalone webview-ui/build src/generated out/",
"compile-tests": "node ./scripts/build-tests.js",
"watch-tests": "tsc -p . -w --outDir out",
"check-types": "npm run protos && tsc --noEmit",
"check-types": "npm run protos && npx tsc --noEmit && cd webview-ui && npx tsc -b --noEmit",
"lint": "eslint src --ext ts && eslint webview-ui/src --ext ts && buf lint && cd webview-ui && npm run lint",
"format": "prettier . --check",
"format:fix": "prettier . --write",
@@ -345,6 +345,9 @@
"test:integration": "vscode-test",
"test:unit": "TS_NODE_PROJECT='./tsconfig.unit-test.json' mocha",
"test:coverage": "vscode-test --coverage",
"e2e": "playwright test -c playwright.config.ts",
"test:e2e": "playwright install && vsce package --no-dependencies --out dist/e2e.vsix && node src/test/e2e/utils/build.js && playwright test",
"test:e2e:optimal": "vsce package --no-dependencies --out dist/e2e.vsix && node src/test/e2e/utils/build.js && playwright test",
"install:all": "npm install && cd webview-ui && npm install",
"dev:webview": "cd webview-ui && npm run dev",
"build:webview": "cd webview-ui && npm run build",
@@ -354,9 +357,9 @@
"prepare": "husky",
"changeset": "changeset",
"version-packages": "changeset version",
"docs": "cd docs && mintlify dev",
"docs:check-links": "cd docs && mintlify broken-links",
"docs:rename-file": "cd docs && mintlify rename",
"docs": "cd docs && npm run dev",
"docs:check-links": "cd docs && npm run check",
"docs:rename-file": "cd docs && npm run rename",
"report-issue": "node scripts/report-issue.js"
},
"lint-staged": {
@@ -383,7 +386,8 @@
"@typescript-eslint/parser": "^7.18.0",
"@typescript-eslint/utils": "^8.33.0",
"@vscode/test-cli": "^0.0.10",
"@vscode/test-electron": "^2.4.1",
"@vscode/test-electron": "^2.5.2",
"@vscode/vsce": "^3.6.0",
"chai": "^4.3.10",
"chalk": "^5.3.0",
"esbuild": "^0.25.0",
@@ -392,7 +396,7 @@
"grpc-tools": "^1.13.0",
"husky": "^9.1.7",
"lint-staged": "^16.1.0",
"mintlify": "^4.0.515",
"minimatch": "^3.0.3",
"npm-run-all": "^4.1.5",
"prettier": "^3.3.3",
"protoc-gen-ts": "^0.8.7",
@@ -408,8 +412,8 @@
"dependencies": {
"@anthropic-ai/sdk": "^0.37.0",
"@anthropic-ai/vertex-sdk": "^0.6.4",
"@aws-sdk/client-bedrock-runtime": "^3.826.0",
"@aws-sdk/credential-providers": "^3.826.0",
"@aws-sdk/client-bedrock-runtime": "^3.840.0",
"@aws-sdk/credential-providers": "^3.840.0",
"@bufbuild/protobuf": "^2.2.5",
"@cerebras/cerebras_cloud_sdk": "^1.35.0",
"@google-cloud/vertexai": "^1.9.3",
@@ -424,6 +428,7 @@
"@opentelemetry/sdk-node": "^0.39.1",
"@opentelemetry/sdk-trace-node": "^1.30.1",
"@opentelemetry/semantic-conventions": "^1.30.0",
"@playwright/test": "^1.53.2",
"@sentry/browser": "^9.12.0",
"@streamparser/json": "^0.0.22",
"@vscode/codicons": "^0.0.36",
@@ -442,13 +447,13 @@
"fzf": "^0.5.2",
"get-folder-size": "^5.0.0",
"globby": "^14.0.2",
"google-auth-library": "^10.1.0",
"grpc-health-check": "^2.0.2",
"iconv-lite": "^0.6.3",
"ignore": "^7.0.3",
"image-size": "^2.0.2",
"isbinaryfile": "^5.0.2",
"jschardet": "^3.1.4",
"jwt-decode": "^4.0.0",
"mammoth": "^1.8.0",
"monaco-vscode-textmate-theme-converter": "^0.1.7",
"nice-grpc": "^2.1.12",
+17
View File
@@ -0,0 +1,17 @@
import { defineConfig } from "@playwright/test"
const isGitHubAction = !!process.env.CI
export default defineConfig({
workers: 1,
retries: 1,
testDir: "src/test/e2e",
timeout: 20000,
expect: {
timeout: 20000,
},
fullyParallel: true,
reporter: isGitHubAction ? [["github"], ["list"]] : [["list"]],
globalSetup: require.resolve("./src/test/e2e/utils/setup"),
globalTeardown: require.resolve("./src/test/e2e/utils/teardown"),
})
+96 -43
View File
@@ -1,76 +1,129 @@
syntax = "proto3";
package cline;
import "common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
// Service for account-related operations
service AccountService {
// Handles the user clicking the login link in the UI.
// Generates a secure nonce for state validation, stores it in secrets,
// and opens the authentication URL in the external browser.
rpc accountLoginClicked(EmptyRequest) returns (String);
// Handles the user clicking the login link in the UI.
// Generates a secure nonce for state validation, stores it in secrets,
// and opens the authentication URL in the external browser.
rpc accountLoginClicked(EmptyRequest) returns (String);
// Handles the user clicking the logout button in the UI.
// Clears API keys and user state.
rpc accountLogoutClicked(EmptyRequest) returns (Empty);
// Handles the user clicking the logout button in the UI.
// Clears API keys and user state.
rpc accountLogoutClicked(EmptyRequest) returns (Empty);
// Subscribe to auth callback events (when authentication tokens are received)
rpc subscribeToAuthCallback(EmptyRequest) returns (stream String);
// Subscribe to auth status update events (when authentication state changes)
rpc subscribeToAuthStatusUpdate(EmptyRequest)
returns (stream AuthState);
// Handles authentication state changes from the Firebase context.
// Updates the user info in global state and returns the updated value.
rpc authStateChanged(AuthStateChangedRequest) returns (AuthStateChanged);
// Handles authentication state changes from the Firebase context.
// Updates the user info in global state and returns the updated value.
rpc authStateChanged(AuthStateChangedRequest)
returns (AuthState);
// Fetches all user credits data (balance, usage transactions, payment transactions)
rpc fetchUserCreditsData(EmptyRequest) returns (UserCreditsData);
// Fetches all user credits data
// (balance, usage transactions, payment transactions)
rpc getUserCredits(EmptyRequest) returns (UserCreditsData);
rpc getOrganizationCredits(GetOrganizationCreditsRequest) returns (OrganizationCreditsData);
// Fetches all user organizations data
// Returns a list of UserOrganization objects
rpc getUserOrganizations(EmptyRequest) returns (UserOrganizationsResponse);
rpc setUserOrganization(UserOrganizationUpdateRequest) returns (Empty);
}
message AuthStateChangedRequest {
Metadata metadata = 1;
UserInfo user = 2;
Metadata metadata = 1;
UserInfo user = 2;
}
message AuthStateChanged {
optional UserInfo user = 1;
message AuthState {
optional UserInfo user = 1;
}
// User's information
message UserInfo {
optional string display_name = 1;
optional string email = 2;
optional string photo_url = 3;
string uid = 1;
optional string display_name = 2;
optional string email = 3;
optional string photo_url = 4;
}
message UserOrganization {
bool active = 1;
string member_id = 2;
string name = 3;
string organization_id = 4;
repeated string roles = 5; // ["admin", "member", "owner"]
}
message UserOrganizationsResponse {
repeated UserOrganization organizations = 1;
}
message UserOrganizationUpdateRequest {
optional string organization_id = 1;
}
// Response containing all user credits data
message UserCreditsData {
UserCreditsBalance balance = 1;
repeated UsageTransaction usage_transactions = 2;
repeated PaymentTransaction payment_transactions = 3;
UserCreditsBalance balance = 1;
repeated UsageTransaction usage_transactions = 2;
repeated PaymentTransaction payment_transactions = 3;
}
message GetOrganizationCreditsRequest {
string organization_id = 1;
}
message OrganizationCreditsData {
UserCreditsBalance balance = 1;
string organization_id = 2;
repeated OrganizationUsageTransaction usage_transactions = 3;
}
// User's current credit balance
message UserCreditsBalance {
double current_balance = 1;
double current_balance = 1;
}
// Usage transaction record
message UsageTransaction {
string spent_at = 1;
string creator_id = 2;
double credits = 3;
string model_provider = 4;
string model = 5;
int32 prompt_tokens = 6;
int32 completion_tokens = 7;
int32 total_tokens = 8;
string ai_inference_provider_name = 1;
string ai_model_name = 2;
string ai_model_type_name = 3;
int32 completion_tokens = 4;
double cost_usd = 5;
string created_at = 6;
double credits_used = 7;
string generation_id = 8;
string organization_id = 9;
int32 prompt_tokens = 10;
int32 total_tokens = 11;
string user_id = 12;
}
// Payment transaction record
message PaymentTransaction {
string paid_at = 1;
string creator_id = 2;
int32 amount_cents = 3;
double credits = 4;
string paid_at = 1;
string creator_id = 2;
int32 amount_cents = 3;
double credits = 4;
}
message OrganizationUsageTransaction {
string ai_inference_provider_name = 1;
string ai_model_name = 2;
string ai_model_type_name = 3;
int32 completion_tokens = 4;
double cost_usd = 5;
string created_at = 6;
double credits_used = 7;
string generation_id = 8;
string organization_id = 9;
int32 prompt_tokens = 10;
int32 total_tokens = 11;
string user_id = 12;
}
+1 -2
View File
@@ -1,11 +1,10 @@
syntax = "proto3";
package cline;
import "common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
service BrowserService {
rpc getBrowserConnectionInfo(EmptyRequest) returns (BrowserConnectionInfo);
rpc testBrowserConnection(StringRequest) returns (BrowserConnection);
+1 -2
View File
@@ -1,11 +1,10 @@
syntax = "proto3";
package cline;
import "common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
service CheckpointsService {
rpc checkpointDiff(Int64Request) returns (Empty);
rpc checkpointRestore(CheckpointRestoreRequest) returns (Empty);
+1 -2
View File
@@ -1,11 +1,10 @@
syntax = "proto3";
package cline;
import "common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
// Service for file-related operations
service FileService {
// Copies text to clipboard
+77
View File
@@ -0,0 +1,77 @@
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);
// Get the contents of the diff view.
rpc getDocumentText(GetDocumentTextRequest) returns (GetDocumentTextResponse);
// Replace a text selection in the diff.
rpc replaceText(ReplaceTextRequest) returns (ReplaceTextResponse);
// Truncate the diff document.
rpc truncateDocument(TruncateDocumentRequest) returns (TruncateDocumentResponse);
// Save the diff document.
rpc saveDocument(SaveDocumentRequest) returns (SaveDocumentResponse);
// Close the diff editor UI.
rpc closeDiff(CloseDiffRequest) returns (CloseDiffResponse);
}
message OpenDiffRequest {
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 GetDocumentTextRequest {
optional cline.Metadata metadata = 1;
optional string diff_id = 2;
}
message GetDocumentTextResponse {
optional string content = 1;
}
message ReplaceTextRequest {
optional cline.Metadata metadata = 1;
optional string diff_id = 2;
optional string content = 3;
optional int32 start_line = 4;
optional int32 end_line = 5;
}
message ReplaceTextResponse {}
message TruncateDocumentRequest {
optional cline.Metadata metadata = 1;
optional string diff_id = 2;
optional int32 end_line = 5;
}
message TruncateDocumentResponse {}
message CloseDiffRequest {
optional cline.Metadata metadata = 1;
optional string diff_id = 2;
}
message CloseDiffResponse {}
message SaveDocumentRequest {
optional cline.Metadata metadata = 1;
optional string diff_id = 2;
}
message SaveDocumentResponse {}
+3
View File
@@ -13,4 +13,7 @@ service EnvService {
// Reads text from the system clipboard.
rpc clipboardReadText(cline.EmptyRequest) returns (cline.String);
// Opens a URL in the user's default browser or application.
rpc openExternal(cline.StringRequest) returns (cline.Empty);
}
-36
View File
@@ -1,36 +0,0 @@
syntax = "proto3";
package host;
option java_package = "bot.cline.host.proto";
option java_multiple_files = true;
import "common.proto";
// UriService provides methods for working with URIs in the IDE
service UriService {
// Create a new file URI from a file path
rpc file(cline.StringRequest) returns (Uri);
// Join a URI with additional path segments
rpc joinPath(JoinPathRequest) returns (Uri);
// Parse a string URI into a Uri object
rpc parse(cline.StringRequest) returns (Uri);
}
// Uri represents a URI in the IDE
message Uri {
string scheme = 1;
string authority = 2;
string path = 3;
string query = 4;
string fragment = 5;
string fs_path = 6;
}
// Request for joining path segments to a URI
message JoinPathRequest {
cline.Metadata metadata = 1;
Uri base = 2;
repeated string path_segments = 3;
}
+4
View File
@@ -6,6 +6,10 @@ option java_multiple_files = true;
import "common.proto";
/**
* The watch service is only here as example of a streaming rpc in the host bridge.
* This being replaced with a native JS file watcher.
*/
// WatchService provides methods for watching files in the IDE
service WatchService {
// Subscribe to file changes
+73
View File
@@ -0,0 +1,73 @@
syntax = "proto3";
package host;
option java_package = "bot.cline.host.proto";
option java_multiple_files = true;
import "common.proto";
// Provides methods for working with IDE windows and editors.
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 {
cline.Metadata metadata = 1;
string path = 2;
optional ShowTextDocumentOptions options = 3;
}
// See https://code.visualstudio.com/api/references/vscode-api#TextDocumentShowOptions
message ShowTextDocumentOptions {
optional bool preview = 1;
optional bool preserve_focus = 2;
optional int32 view_column = 3;
}
message TextEditorInfo {
string document_path = 1;
optional int32 view_column = 2;
bool is_active = 3;
}
message ShowOpenDialogueRequest {
cline.Metadata metadata = 1;
optional bool can_select_many = 2;
optional string open_label = 3;
optional ShowOpenDialogueFilterOption filters = 4;
}
message ShowOpenDialogueFilterOption {
repeated string files = 1;
}
message SelectedResources {
repeated string paths = 1;
}
enum ShowMessageType {
ERROR = 0;
INFORMATION = 1;
WARNING = 2;
}
message ShowMessageRequest {
cline.Metadata metadata = 1;
ShowMessageType type = 2;
string message = 3;
optional ShowMessageRequestOptions options = 4;
}
message ShowMessageRequestOptions {
repeated string items = 1;
optional bool modal = 2;
optional string detail = 3;
}
message SelectedResponse {
optional string selected_option = 1;
}
+14 -3
View File
@@ -1,16 +1,15 @@
syntax = "proto3";
package cline;
import "common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
service McpService {
rpc toggleMcpServer(ToggleMcpServerRequest) returns (McpServers);
rpc updateMcpTimeout(UpdateMcpTimeoutRequest) returns (McpServers);
rpc addRemoteMcpServer(AddRemoteMcpServerRequest) returns (McpServers);
rpc downloadMcp(StringRequest) returns (Empty);
rpc downloadMcp(StringRequest) returns (McpDownloadResponse);
rpc restartMcpServer(StringRequest) returns (McpServers);
rpc deleteMcpServer(StringRequest) returns (McpServers);
rpc toggleToolAutoApprove(ToggleToolAutoApproveRequest) returns (McpServers);
@@ -119,3 +118,15 @@ message McpMarketplaceItem {
message McpMarketplaceCatalog {
repeated McpMarketplaceItem items = 1;
}
message McpDownloadResponse {
string mcp_id = 1;
string github_url = 2;
string name = 3;
string author = 4;
string description = 5;
string readme_content = 6;
string llms_installation_content = 7;
bool requires_api_key = 8;
optional string error = 9;
}
+139 -95
View File
@@ -1,11 +1,10 @@
syntax = "proto3";
package cline;
import "common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
// Service for model-related operations
service ModelsService {
// Fetches available models from Ollama
@@ -16,6 +15,8 @@ service ModelsService {
rpc getVsCodeLmModels(EmptyRequest) returns (VsCodeLmModelsArray);
// Refreshes and returns OpenRouter models
rpc refreshOpenRouterModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
// Refreshes and returns Hugging Face models
rpc refreshHuggingFaceModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
// Refreshes and returns OpenAI models
rpc refreshOpenAiModels(OpenAiModelsRequest) returns (StringArray);
// Refreshes and returns Requesty models
@@ -24,6 +25,8 @@ service ModelsService {
rpc subscribeToOpenRouterModels(EmptyRequest) returns (stream OpenRouterCompatibleModelInfo);
// Updates API configuration
rpc updateApiConfigurationProto(UpdateApiConfigurationRequest) returns (Empty);
// Refreshes and returns Groq models
rpc refreshGroqModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
}
// List of VS Code LM models
@@ -105,25 +108,27 @@ enum ApiProvider {
OLLAMA = 5;
LMSTUDIO = 6;
GEMINI = 7;
GEMINI_CLI = 8;
OPENAI_NATIVE = 9;
REQUESTY = 10;
TOGETHER = 11;
DEEPSEEK = 12;
QWEN = 13;
DOUBAO = 14;
MISTRAL = 15;
VSCODE_LM = 16;
CLINE = 17;
LITELLM = 18;
NEBIUS = 19;
FIREWORKS = 20;
ASKSAGE = 21;
XAI = 22;
SAMBANOVA = 23;
CEREBRAS = 24;
OPENAI_NATIVE = 8;
REQUESTY = 9;
TOGETHER = 10;
DEEPSEEK = 11;
QWEN = 12;
DOUBAO = 13;
MISTRAL = 14;
VSCODE_LM = 15;
CLINE = 16;
LITELLM = 17;
NEBIUS = 18;
FIREWORKS = 19;
ASKSAGE = 20;
XAI = 21;
SAMBANOVA = 22;
CEREBRAS = 23;
GROQ = 24;
SAPAICORE = 25;
CLAUDE_CODE = 26;
MOONSHOT = 27;
HUGGINGFACE = 28;
}
// Model info for OpenAI-compatible models
@@ -163,80 +168,119 @@ message LiteLLMModelInfo {
// Main ApiConfiguration message
message ModelsApiConfiguration {
// From ApiHandlerOptions (excluding onRetryAttempt function)
optional string api_model_id = 1;
optional string api_key = 2;
optional string cline_api_key = 3;
optional string task_id = 4;
optional string lite_llm_base_url = 5;
optional string lite_llm_model_id = 6;
optional string lite_llm_api_key = 7;
optional bool lite_llm_use_prompt_cache = 8;
map<string, string> open_ai_headers = 9;
optional LiteLLMModelInfo lite_llm_model_info = 10;
optional string anthropic_base_url = 11;
optional string open_router_api_key = 12;
optional string open_router_model_id = 13;
optional OpenRouterModelInfo open_router_model_info = 14;
optional string open_router_provider_sorting = 15;
optional string aws_access_key = 16;
optional string aws_secret_key = 17;
optional string aws_session_token = 18;
optional string aws_region = 19;
optional bool aws_use_cross_region_inference = 20;
optional bool aws_bedrock_use_prompt_cache = 21;
optional bool aws_use_profile = 22;
optional string aws_profile = 23;
optional string aws_bedrock_endpoint = 24;
optional bool aws_bedrock_custom_selected = 25;
optional string aws_bedrock_custom_model_base_id = 26;
optional string vertex_project_id = 27;
optional string vertex_region = 28;
optional string open_ai_base_url = 29;
optional string open_ai_api_key = 30;
optional string open_ai_model_id = 31;
optional OpenAiCompatibleModelInfo open_ai_model_info = 32;
optional string ollama_model_id = 33;
optional string ollama_base_url = 34;
optional string ollama_api_options_ctx_num = 35;
optional string lm_studio_model_id = 36;
optional string lm_studio_base_url = 37;
optional string gemini_api_key = 38;
optional string gemini_base_url = 39;
optional string open_ai_native_api_key = 40;
optional string deep_seek_api_key = 41;
optional string requesty_api_key = 42;
optional string requesty_model_id = 43;
optional OpenRouterModelInfo requesty_model_info = 44;
optional string together_api_key = 45;
optional string together_model_id = 46;
optional string fireworks_api_key = 47;
optional string fireworks_model_id = 48;
optional int32 fireworks_model_max_completion_tokens = 49;
optional int32 fireworks_model_max_tokens = 50;
optional string qwen_api_key = 51;
optional string doubao_api_key = 52;
optional string mistral_api_key = 53;
optional string azure_api_version = 54;
optional LanguageModelChatSelector vs_code_lm_model_selector = 55;
optional string qwen_api_line = 56;
optional string nebius_api_key = 57;
optional string asksage_api_url = 58;
optional string asksage_api_key = 59;
optional string xai_api_key = 60;
optional int32 thinking_budget_tokens = 61;
optional string reasoning_effort = 62;
optional string sambanova_api_key = 63;
optional string cerebras_api_key = 64;
optional int32 request_timeout_ms = 65;
optional ApiProvider api_provider = 66;
repeated string favorited_model_ids = 67;
optional string sap_ai_core_client_id = 68;
optional string sap_ai_core_client_secret = 69;
optional string sap_ai_resource_group = 70;
optional string sap_ai_core_token_url = 71;
optional string sap_ai_core_base_url = 72;
optional string claude_code_path = 73;
optional string gemini_cli_oauth_path = 74;
optional string gemini_cli_project_id = 75;
// Global configuration fields (not mode-specific)
optional string api_key = 1;
optional string cline_api_key = 2;
optional string task_id = 3;
optional string lite_llm_base_url = 4;
optional string lite_llm_api_key = 5;
optional bool lite_llm_use_prompt_cache = 6;
map<string, string> open_ai_headers = 7;
optional string anthropic_base_url = 8;
optional string open_router_api_key = 9;
optional string open_router_provider_sorting = 10;
optional string aws_access_key = 11;
optional string aws_secret_key = 12;
optional string aws_session_token = 13;
optional string aws_region = 14;
optional bool aws_use_cross_region_inference = 15;
optional bool aws_bedrock_use_prompt_cache = 16;
optional bool aws_use_profile = 17;
optional string aws_profile = 18;
optional string aws_bedrock_endpoint = 19;
optional string claude_code_path = 20;
optional string vertex_project_id = 21;
optional string vertex_region = 22;
optional string open_ai_base_url = 23;
optional string open_ai_api_key = 24;
optional string ollama_base_url = 25;
optional string ollama_api_options_ctx_num = 26;
optional string lm_studio_base_url = 27;
optional string gemini_api_key = 28;
optional string gemini_base_url = 29;
optional string open_ai_native_api_key = 30;
optional string deep_seek_api_key = 31;
optional string requesty_api_key = 32;
optional string together_api_key = 33;
optional string fireworks_api_key = 34;
optional int32 fireworks_model_max_completion_tokens = 35;
optional int32 fireworks_model_max_tokens = 36;
optional string qwen_api_key = 37;
optional string doubao_api_key = 38;
optional string mistral_api_key = 39;
optional string azure_api_version = 40;
optional string qwen_api_line = 41;
optional string nebius_api_key = 42;
optional string asksage_api_url = 43;
optional string asksage_api_key = 44;
optional string xai_api_key = 45;
optional string sambanova_api_key = 46;
optional string cerebras_api_key = 47;
optional int32 request_timeout_ms = 48;
optional string sap_ai_core_client_id = 49;
optional string sap_ai_core_client_secret = 50;
optional string sap_ai_resource_group = 51;
optional string sap_ai_core_token_url = 52;
optional string sap_ai_core_base_url = 53;
optional string moonshot_api_key = 54;
optional string moonshot_api_line = 55;
optional string aws_authentication = 56;
optional string aws_bedrock_api_key = 57;
optional string cline_account_id = 58;
optional string groq_api_key = 59;
optional string hugging_face_api_key = 60;
// Plan mode configurations
optional ApiProvider plan_mode_api_provider = 100;
optional string plan_mode_api_model_id = 101;
optional int32 plan_mode_thinking_budget_tokens = 102;
optional string plan_mode_reasoning_effort = 103;
optional LanguageModelChatSelector plan_mode_vs_code_lm_model_selector = 104;
optional bool plan_mode_aws_bedrock_custom_selected = 105;
optional string plan_mode_aws_bedrock_custom_model_base_id = 106;
optional string plan_mode_open_router_model_id = 107;
optional OpenRouterModelInfo plan_mode_open_router_model_info = 108;
optional string plan_mode_open_ai_model_id = 109;
optional OpenAiCompatibleModelInfo plan_mode_open_ai_model_info = 110;
optional string plan_mode_ollama_model_id = 111;
optional string plan_mode_lm_studio_model_id = 112;
optional string plan_mode_lite_llm_model_id = 113;
optional LiteLLMModelInfo plan_mode_lite_llm_model_info = 114;
optional string plan_mode_requesty_model_id = 115;
optional OpenRouterModelInfo plan_mode_requesty_model_info = 116;
optional string plan_mode_together_model_id = 117;
optional string plan_mode_fireworks_model_id = 118;
optional string plan_mode_sap_ai_core_model_id = 119;
optional string plan_mode_groq_model_id = 120;
optional OpenRouterModelInfo plan_mode_groq_model_info = 121;
optional string plan_mode_hugging_face_model_id = 122;
optional OpenRouterModelInfo plan_mode_hugging_face_model_info = 123;
// Act mode configurations
optional ApiProvider act_mode_api_provider = 200;
optional string act_mode_api_model_id = 201;
optional int32 act_mode_thinking_budget_tokens = 202;
optional string act_mode_reasoning_effort = 203;
optional LanguageModelChatSelector act_mode_vs_code_lm_model_selector = 204;
optional bool act_mode_aws_bedrock_custom_selected = 205;
optional string act_mode_aws_bedrock_custom_model_base_id = 206;
optional string act_mode_open_router_model_id = 207;
optional OpenRouterModelInfo act_mode_open_router_model_info = 208;
optional string act_mode_open_ai_model_id = 209;
optional OpenAiCompatibleModelInfo act_mode_open_ai_model_info = 210;
optional string act_mode_ollama_model_id = 211;
optional string act_mode_lm_studio_model_id = 212;
optional string act_mode_lite_llm_model_id = 213;
optional LiteLLMModelInfo act_mode_lite_llm_model_info = 214;
optional string act_mode_requesty_model_id = 215;
optional OpenRouterModelInfo act_mode_requesty_model_info = 216;
optional string act_mode_together_model_id = 217;
optional string act_mode_fireworks_model_id = 218;
optional string act_mode_sap_ai_core_model_id = 219;
optional string act_mode_groq_model_id = 220;
optional OpenRouterModelInfo act_mode_groq_model_info = 221;
optional string act_mode_hugging_face_model_id = 222;
optional OpenRouterModelInfo act_mode_hugging_face_model_info = 223;
repeated string favorited_model_ids = 300;
}
-3
View File
@@ -1,3 +0,0 @@
{
"type": "module"
}
+1 -2
View File
@@ -1,11 +1,10 @@
syntax = "proto3";
package cline;
import "common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
// SlashService provides methods for managing slash
service SlashService {
// Sends button click message
+106 -111
View File
@@ -1,10 +1,9 @@
syntax = "proto3";
package cline;
import "common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
service StateService {
rpc getLatestState(EmptyRequest) returns (State);
rpc updateTerminalConnectionTimeout(Int64Request) returns (Int64);
@@ -18,6 +17,7 @@ service StateService {
rpc updateAutoApprovalSettings(AutoApprovalSettingsRequest) returns (Empty);
rpc updateSettings(UpdateSettingsRequest) returns (Empty);
rpc updateTelemetrySetting(TelemetrySettingRequest) returns (Empty);
rpc setWelcomeViewCompleted(BooleanRequest) returns (Empty);
}
message State {
@@ -112,124 +112,119 @@ 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;
}
// Complete API Configuration message
message ApiConfiguration {
// Core API fields
optional string api_provider = 1;
optional string api_model_id = 2;
optional string api_key = 3; // anthropic
optional string api_base_url = 4;
// Global configuration fields (not mode-specific)
optional string api_key = 1; // anthropic
optional string cline_api_key = 2;
optional string task_id = 3;
optional string lite_llm_base_url = 4;
optional string lite_llm_api_key = 5;
optional bool lite_llm_use_prompt_cache = 6;
optional string openai_headers = 7; // JSON string
optional string anthropic_base_url = 8;
optional string openrouter_api_key = 9;
optional string openrouter_provider_sorting = 10;
optional string aws_access_key = 11;
optional string aws_secret_key = 12;
optional string aws_session_token = 13;
optional string aws_region = 14;
optional bool aws_use_cross_region_inference = 15;
optional bool aws_bedrock_use_prompt_cache = 16;
optional bool aws_use_profile = 17;
optional string aws_profile = 18;
optional string aws_bedrock_endpoint = 19;
optional string claude_code_path = 20;
optional string vertex_project_id = 21;
optional string vertex_region = 22;
optional string openai_base_url = 23;
optional string openai_api_key = 24;
optional string ollama_base_url = 25;
optional string ollama_api_options_ctx_num = 26;
optional string lm_studio_base_url = 27;
optional string gemini_api_key = 28;
optional string gemini_base_url = 29;
optional string openai_native_api_key = 30;
optional string deep_seek_api_key = 31;
optional string requesty_api_key = 32;
optional string together_api_key = 33;
optional string fireworks_api_key = 34;
optional int64 fireworks_model_max_completion_tokens = 35;
optional int64 fireworks_model_max_tokens = 36;
optional string qwen_api_key = 37;
optional string doubao_api_key = 38;
optional string mistral_api_key = 39;
optional string azure_api_version = 40;
optional string qwen_api_line = 41;
optional string nebius_api_key = 42;
optional string asksage_api_url = 43;
optional string asksage_api_key = 44;
optional string xai_api_key = 45;
optional string sambanova_api_key = 46;
optional string cerebras_api_key = 47;
optional int64 request_timeout_ms = 48;
optional string sap_ai_core_client_id = 49;
optional string sap_ai_core_client_secret = 50;
optional string sap_ai_resource_group = 51;
optional string sap_ai_core_token_url = 52;
optional string sap_ai_core_base_url = 53;
optional string moonshot_api_key = 54;
optional string moonshot_api_line = 55;
// Provider-specific API keys
optional string cline_api_key = 5;
optional string openrouter_api_key = 6;
optional string anthropic_base_url = 7;
optional string openai_api_key = 8;
optional string openai_native_api_key = 9;
optional string gemini_api_key = 10;
optional string deepseek_api_key = 11;
optional string requesty_api_key = 12;
optional string together_api_key = 13;
optional string fireworks_api_key = 14;
optional string qwen_api_key = 15;
optional string doubao_api_key = 16;
optional string mistral_api_key = 17;
optional string nebius_api_key = 18;
optional string asksage_api_key = 19;
optional string xai_api_key = 20;
optional string sambanova_api_key = 21;
optional string cerebras_api_key = 22;
// Plan mode configurations
optional string plan_mode_api_provider = 100;
optional string plan_mode_api_model_id = 101;
optional int64 plan_mode_thinking_budget_tokens = 102;
optional string plan_mode_reasoning_effort = 103;
optional string plan_mode_vscode_lm_model_selector = 104; // JSON string
optional bool plan_mode_aws_bedrock_custom_selected = 105;
optional string plan_mode_aws_bedrock_custom_model_base_id = 106;
optional string plan_mode_openrouter_model_id = 107;
optional string plan_mode_openrouter_model_info = 108; // JSON string
optional string plan_mode_openai_model_id = 109;
optional string plan_mode_openai_model_info = 110; // JSON string
optional string plan_mode_ollama_model_id = 111;
optional string plan_mode_lm_studio_model_id = 112;
optional string plan_mode_lite_llm_model_id = 113;
optional string plan_mode_lite_llm_model_info = 114; // JSON string
optional string plan_mode_requesty_model_id = 115;
optional string plan_mode_requesty_model_info = 116; // JSON string
optional string plan_mode_together_model_id = 117;
optional string plan_mode_fireworks_model_id = 118;
optional string plan_mode_sap_ai_core_model_id = 119;
// Model IDs
optional string openrouter_model_id = 23;
optional string openai_model_id = 24;
optional string anthropic_model_id = 25;
optional string bedrock_model_id = 26;
optional string vertex_model_id = 27;
optional string gemini_model_id = 28;
optional string ollama_model_id = 29;
optional string lm_studio_model_id = 30;
optional string litellm_model_id = 31;
optional string requesty_model_id = 32;
optional string together_model_id = 33;
optional string fireworks_model_id = 34;
// AWS Bedrock fields
optional bool aws_bedrock_custom_selected = 35;
optional string aws_bedrock_custom_model_base_id = 36;
optional string aws_access_key = 37;
optional string aws_secret_key = 38;
optional string aws_session_token = 39;
optional string aws_region = 40;
optional bool aws_use_cross_region_inference = 41;
optional bool aws_bedrock_use_prompt_cache = 42;
optional bool aws_use_profile = 43;
optional string aws_profile = 44;
optional string aws_bedrock_endpoint = 45;
// Vertex AI fields
optional string vertex_project_id = 46;
optional string vertex_region = 47;
// Base URLs and endpoints
optional string openai_base_url = 48;
optional string ollama_base_url = 49;
optional string lm_studio_base_url = 50;
optional string gemini_base_url = 51;
optional string litellm_base_url = 52;
optional string asksage_api_url = 53;
// LiteLLM specific fields
optional string litellm_api_key = 54;
optional bool litellm_use_prompt_cache = 55;
// Model configuration
optional int64 thinking_budget_tokens = 56;
optional string reasoning_effort = 57;
optional int64 request_timeout_ms = 58;
// Fireworks specific
optional int64 fireworks_model_max_completion_tokens = 59;
optional int64 fireworks_model_max_tokens = 60;
// Azure specific
optional string azure_api_version = 61;
// Ollama specific
optional string ollama_api_options_ctx_num = 62;
// Qwen specific
optional string qwen_api_line = 63;
// OpenRouter specific
optional string openrouter_provider_sorting = 64;
// VSCode LM (stored as JSON string due to complex type)
optional string vscode_lm_model_selector = 65;
// Model info objects (stored as JSON strings)
optional string openrouter_model_info = 66;
optional string openai_model_info = 67;
optional string requesty_model_info = 68;
optional string litellm_model_info = 69;
// OpenAI headers (stored as JSON string)
optional string openai_headers = 70;
// Act mode configurations
optional string act_mode_api_provider = 200;
optional string act_mode_api_model_id = 201;
optional int64 act_mode_thinking_budget_tokens = 202;
optional string act_mode_reasoning_effort = 203;
optional string act_mode_vscode_lm_model_selector = 204; // JSON string
optional bool act_mode_aws_bedrock_custom_selected = 205;
optional string act_mode_aws_bedrock_custom_model_base_id = 206;
optional string act_mode_openrouter_model_id = 207;
optional string act_mode_openrouter_model_info = 208; // JSON string
optional string act_mode_openai_model_id = 209;
optional string act_mode_openai_model_info = 210; // JSON string
optional string act_mode_ollama_model_id = 211;
optional string act_mode_lm_studio_model_id = 212;
optional string act_mode_lite_llm_model_id = 213;
optional string act_mode_lite_llm_model_info = 214; // JSON string
optional string act_mode_requesty_model_id = 215;
optional string act_mode_requesty_model_info = 216; // JSON string
optional string act_mode_together_model_id = 217;
optional string act_mode_fireworks_model_id = 218;
optional string act_mode_sap_ai_core_model_id = 219;
// Favorited model IDs
repeated string favorited_model_ids = 71;
repeated string favorited_model_ids = 300;
// SAP AI Core specific
optional string sap_ai_core_client_id = 72;
optional string sap_ai_core_client_secret = 73;
optional string sap_ai_core_base_url = 74;
optional string sap_ai_core_token_url = 75;
optional string sap_ai_resource_group = 76;
// Extension fields for Bedrock Api Keys
optional string aws_authentication = 301;
optional string aws_bedrock_api_key = 302;
// Claude Code specific
optional string claude_code_path = 77;
optional string cline_account_id = 303;
}
+1 -2
View File
@@ -1,11 +1,10 @@
syntax = "proto3";
package cline;
import "common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
service TaskService {
// Cancels the currently running task
rpc cancelTask(EmptyRequest) returns (Empty);
+1 -2
View File
@@ -1,11 +1,10 @@
syntax = "proto3";
package cline;
import "common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
// Enum for webview provider types
enum WebviewProviderType {
SIDEBAR = 0;
+1 -2
View File
@@ -1,11 +1,10 @@
syntax = "proto3";
package cline;
import "common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
import "common.proto";
service WebService {
rpc checkIsImageUrl(StringRequest) returns (IsImageUrl);
rpc fetchOpenGraphData(StringRequest) returns (OpenGraphData);
@@ -26,5 +26,7 @@ export const hostServiceNameMap = {
watch: "host.WatchService",
workspace: "host.WorkspaceService",
env: "host.EnvService",
window: "host.WindowService",
diff: "host.DiffService",
// Add new host services here
}
+57 -201
View File
@@ -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 } 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,16 +37,13 @@ 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..."))
await cleanup()
// Check for Apple Silicon compatibility before proceeding
checkAppleSiliconCompatibility()
@@ -55,14 +52,12 @@ async function main() {
await fs.mkdir(dir, { recursive: true })
}
await cleanup()
// Check for missing proto files for services in serviceNameMap
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,9 @@ 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()
console.log(chalk.bold.blue("Finished Protocol Buffer code generation."))
}
@@ -102,7 +95,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 +115,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 +140,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 +151,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 +214,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 +236,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 +285,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 +320,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 +340,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 +360,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 +373,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,181 +410,40 @@ 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}`))
}
}
}
/**
* Generate method registration files for host services
*/
async function generateHostMethodRegistrations() {
log_verbose(chalk.cyan("Generating host method registration files..."))
// 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"))
for (const serviceDir of hostServiceDirs) {
const serviceName = path.basename(serviceDir)
const fullServiceName = hostServiceNameMap[serviceName]
const streamingMethods = streamingMethodsMap.get(fullServiceName) || []
log_verbose(chalk.cyan(`Generating method registrations for host ${serviceName}...`))
// Get all TypeScript files in the service directory
const files = await globby("*.ts", { cwd: serviceDir })
// Filter out index.ts and methods.ts
const implementationFiles = files.filter((file) => file !== "index.ts" && file !== "methods.ts")
// Create the methods.ts file with header
let methodsContent = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Import all method implementations
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`
}
// Add streaming methods information
if (streamingMethods.length > 0) {
methodsContent += `\n// Streaming methods for this service
export const streamingMethods = ${JSON.stringify(
streamingMethods.map((m) => m.name),
null,
2,
)}\n`
}
// Add registration function
methodsContent += `\n// Register all ${serviceName} service methods
export function registerAllMethods(): void {
\t// Register each method with the registry\n`
// Add registration statements
for (const file of implementationFiles) {
const baseName = path.basename(file, ".ts")
const isStreaming = streamingMethods.some((m) => m.name === baseName)
if (isStreaming) {
methodsContent += `\tregisterMethod("${baseName}", ${baseName}, { isStreaming: true })\n`
} else {
methodsContent += `\tregisterMethod("${baseName}", ${baseName})\n`
}
}
// Close the function
methodsContent += `}`
// Write the methods.ts file
const registryFile = path.join(serviceDir, "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
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../host-grpc-service"
import { StreamingResponseHandler } from "../host-grpc-handler"
import { registerAllMethods } from "./methods"
// Create ${serviceName} service registry
const ${serviceName}Service = createServiceRegistry("${serviceName}")
// Export the method handler types and registration function
export type ${capitalizedServiceName}MethodHandler = ServiceMethodHandler
export type ${capitalizedServiceName}StreamingMethodHandler = StreamingMethodHandler
export const registerMethod = ${serviceName}Service.registerMethod
// Export the request handlers
export const handle${capitalizedServiceName}ServiceRequest = ${serviceName}Service.handleRequest
export const handle${capitalizedServiceName}ServiceStreamingRequest = ${serviceName}Service.handleStreamingRequest
export const isStreamingMethod = ${serviceName}Service.isStreamingMethod
// Register all ${serviceName} methods
registerAllMethods()`
// Write the index.ts file
const indexFile = path.join(serviceDir, "index.ts")
await writeFileWithMkdirs(indexFile, indexContent)
log_verbose(chalk.green(`Generated ${indexFile}`))
}
log_verbose(chalk.green("Host method registration files generated successfully."))
}
/**
* Generate a service configuration file for host services
*/
async function generateHostServiceConfig() {
log_verbose(chalk.cyan("Generating host service configuration file..."))
const serviceImports = []
const serviceConfigs = []
// Add all services from the hostServiceNameMap
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"`,
)
serviceConfigs.push(`
"${fullServiceName}": {
requestHandler: handle${capitalizedName}ServiceRequest,
streamingHandler: handle${capitalizedName}ServiceStreamingRequest
}`)
}
const content = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { StreamingResponseHandler } from "./host-grpc-handler"
${serviceImports.join("\n")}
/**
* Configuration for a host service handler
*/
export interface HostServiceHandlerConfig {
requestHandler: (method: string, message: any) => Promise<any>;
streamingHandler: (method: string, message: any, responseStream: StreamingResponseHandler, requestId?: string) => Promise<void>;
}
/**
* Map of host service names to their handler configurations
*/
export const hostServiceHandlers: Record<string, HostServiceHandlerConfig> = {${serviceConfigs.join(",")}
};`
const filePath = path.join(ROOT_DIR, "src/hosts/vscode/host-grpc-service-config.ts")
await writeFileWithMkdirs(filePath, content)
log_verbose(chalk.green(`Generated host service configuration at ${filePath}`))
}
async function cleanup() {
// Clean up existing generated files
log_verbose(chalk.cyan("Cleaning up existing generated TypeScript files..."))
const existingFiles = await globby("**/*.ts", { cwd: TS_OUT_DIR })
for (const file of existingFiles) {
await fs.unlink(path.join(TS_OUT_DIR, file))
}
await rmdir(path.join(ROOT_DIR, "src", "generated"))
await rmrf(TS_OUT_DIR)
await rmrf("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(path.join(ROOT_DIR, "src/standalone/server-setup.ts"), { force: true })
await rmrf("src/standalone/services/host-grpc-client.ts")
await rmrf("src/standalone/server-setup.ts")
await rmrf("src/hosts/vscode/host-grpc-service-config.ts")
const oldhostbridgefiles = [
"src/hosts/vscode/workspace/methods.ts",
"src/hosts/vscode/workspace/index.ts",
"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 rmrf(file)
}
}
/**
@@ -616,6 +468,10 @@ async function rmdir(path) {
}
}
async function rmrf(path) {
await fs.rm(path, { force: true, recursive: true })
}
// Check for Apple Silicon compatibility
function checkAppleSiliconCompatibility() {
// Only run check on macOS
+89 -21
View File
@@ -6,10 +6,15 @@ import * as grpc from "@grpc/grpc-js"
import * as protoLoader from "@grpc/proto-loader"
import chalk from "chalk"
const IMPL_FILE = path.resolve("src/generated/standalone/host-bridge-clients.ts")
const INTERFACE_FILE = path.resolve("src/generated/hosts/host-bridge-client-types.ts")
const DESCRIPTOR_SET = path.resolve("dist-standalone/proto/descriptor_set.pb")
// Contains the interface definitions for the host bridge clients.
const TYPES_FILE = path.resolve("src/generated/hosts/host-bridge-client-types.ts")
// Contains the ExternalHostBridgeClientManager for the external host bridge clients (using nice-grpc).
const EXTERNAL_CLIENT_FILE = path.resolve("src/generated/hosts/standalone/host-bridge-clients.ts")
// Contains the handler map for the external host bridge clients (using the custom service registry).
const VSCODE_CLIENT_FILE = path.resolve("src/generated/hosts/vscode/hostbridge-grpc-service-config.ts")
const typeNameToFQN = new Map()
function addTypeNameToFqn(name, fqn) {
@@ -48,24 +53,23 @@ async function main() {
}
}
// Generate interfaces file
await generateInterfacesFile(hostServices)
// // Generate implementation file
await generateImplementationFile(hostServices)
await generateTypesFile(hostServices)
await generateExternalClientFile(hostServices)
await generateVscodeClientFile(hostServices)
console.log(`Generated host bridge client files at:`)
console.log(`- ${INTERFACE_FILE}`)
console.log(`- ${IMPL_FILE}`)
console.log(`- ${TYPES_FILE}`)
console.log(`- ${EXTERNAL_CLIENT_FILE}`)
console.log(`- ${VSCODE_CLIENT_FILE}`)
}
/**
* Generate the client interfaces file.
*/
async function generateInterfacesFile(hostServices) {
async function generateTypesFile(hostServices) {
const clientInterfaces = []
for (const [name, def] of Object.entries(hostServices)) {
const clientInterface = generateClientInterface(name, def)
const clientInterface = generateClientInterfaceType(name, def)
clientInterfaces.push(clientInterface)
}
const content = `// GENERATED CODE -- DO NOT EDIT!
@@ -76,14 +80,14 @@ import { StreamingCallbacks } from "@hosts/host-provider-types"
${clientInterfaces.join("\n\n")}
`
// Write output file
await fs.mkdir(path.dirname(INTERFACE_FILE), { recursive: true })
await fs.writeFile(INTERFACE_FILE, content)
await fs.mkdir(path.dirname(TYPES_FILE), { recursive: true })
await fs.writeFile(TYPES_FILE, content)
}
/**
* Generate a client interface for a service.
*/
function generateClientInterface(serviceName, serviceDefinition) {
function generateClientInterfaceType(serviceName, serviceDefinition) {
// Get the methods from the service definition
const methods = Object.entries(serviceDefinition.service)
.map(([methodName, methodDef]) => {
@@ -110,9 +114,9 @@ ${methods}
}
/**
* Generate the client implementations file.
* Generate the external client implementations file.
*/
async function generateImplementationFile(hostServices) {
async function generateExternalClientFile(hostServices) {
// Generate imports
const imports = []
// Add imports for the interfaces
@@ -121,7 +125,7 @@ async function generateImplementationFile(hostServices) {
}
const clientImplementations = []
for (const [name, def] of Object.entries(hostServices)) {
clientImplementations.push(generateClientImplementation(name, def))
clientImplementations.push(generateExternalClientSetup(name, def))
}
const content = `// GENERATED CODE -- DO NOT EDIT!
@@ -136,16 +140,15 @@ ${imports.join("\n")}
${clientImplementations.join("\n\n")}
`
// Write output file
await fs.mkdir(path.dirname(IMPL_FILE), { recursive: true })
await fs.writeFile(IMPL_FILE, content)
await fs.mkdir(path.dirname(EXTERNAL_CLIENT_FILE), { recursive: true })
await fs.writeFile(EXTERNAL_CLIENT_FILE, content)
}
/**
* Generate a client implementation class for a service
*/
function generateClientImplementation(serviceName, serviceDefinition) {
function generateExternalClientSetup(serviceName, serviceDefinition) {
// Get the methods from the service definition
const methods = Object.entries(serviceDefinition.service)
.map(([methodName, methodDef]) => {
@@ -185,6 +188,71 @@ ${methods}
}`
}
/**
* Generate the Vscode client setup file.
*/
async function generateVscodeClientFile(hostServices) {
const imports = []
const clientImplementations = []
const handlerMap = []
for (const [serviceName, serviceDefinition] of Object.entries(hostServices)) {
const name = serviceName.replace(/Service$/, "").toLowerCase()
for (const [methodName, _methodDef] of Object.entries(serviceDefinition.service)) {
imports.push(`import { ${methodName} } from "@/hosts/vscode/hostbridge/${name}/${methodName}"`)
}
imports.push("")
clientImplementations.push(generateVscodeClientImplementation(name, serviceDefinition))
handlerMap.push(` "host.${serviceName}": {
requestHandler: ${name}ServiceRegistry.handleRequest,
streamingHandler: ${name}ServiceRegistry.handleStreamingRequest,
},`)
}
const content = `// GENERATED CODE -- DO NOT EDIT!
// Generated by scripts/generate-host-bridge-client.mjs
import { createServiceRegistry } from "@hosts/vscode/hostbridge-grpc-service"
import { HostServiceHandlerConfig } from "@hosts/vscode/hostbridge-grpc-handler"
${imports.join("\n")}
${clientImplementations.join("\n\n")}
/**
* Map of host service names to their handler configurations
*/
export const hostServiceHandlers: Record<string, HostServiceHandlerConfig> = {
${handlerMap.join("\n")}
}
`
// Write output file
await fs.mkdir(path.dirname(VSCODE_CLIENT_FILE), { recursive: true })
await fs.writeFile(VSCODE_CLIENT_FILE, content)
}
function generateVscodeClientImplementation(serviceName, serviceDefinition) {
// Get the methods from the service definition
const name = serviceName.replace(/Service$/, "").toLowerCase()
const methods = Object.entries(serviceDefinition.service)
.map(([methodName, methodDef]) => {
// Get fully qualified type names
const isStreamingResponse = methodDef.responseStream
if (!isStreamingResponse) {
return `${name}ServiceRegistry.registerMethod("${methodName}", ${methodName})`
} else {
return `${name}ServiceRegistry.registerMethod("${methodName}", ${methodName}, { isStreaming: true })`
}
})
.join("\n")
// Generate the class
return `// Setup ${name} service registry
const ${name}ServiceRegistry = createServiceRegistry("${name}")
${methods}`
}
// Run the main function
main().catch((error) => {
console.error(chalk.red("Error:"), error)
+1 -1
View File
@@ -62,7 +62,7 @@ let output = `// GENERATED CODE -- DO NOT EDIT!
import * as grpc from "@grpc/grpc-js"
import { cline } from "@generated/grpc-js"
import { Controller } from "@core/controller"
import { GrpcHandlerWrapper, GrpcStreamingResponseHandlerWrapper } from "@/standalone/grpc-types"
import { GrpcHandlerWrapper, GrpcStreamingResponseHandlerWrapper } from "@hosts/external/grpc-types"
${imports}
export function addProtobusServices(
+16 -2
View File
@@ -5,17 +5,31 @@ DIR=${1:-src/}
DEST_DIR=dist-standalone
SDK_DEST=$DEST_DIR/vscode-sdk-uses.txt
CSS_DEST=$DEST_DIR/vscode-css-uses.txt
TMP=/tmp/vscode-sdk-uses.txt.tmp
mkdir -p $DEST_DIR
{
git grep -h 'vscode\.' $DIR |
grep -Ev '//.*vscode' | # remove commented out code
grep -v vscode.commands.executeCommand | # executeCommand is handled separately
grep -Ev '"vscode' | # remove command strings that get included because they start with vscode
sed 's|.*vscode\.|vscode.|'| # remove everything before vscode.
sed 's/[^a-zA-Z0-9_.].*$//' | # remove everything after last identifier
grep -E '\.[a-z][^.]+$' | # remove types (last part of identifier should be lowercase)
sort | uniq -c | sort -n | # Count occurrences
cat > $SDK_DEST
cat > $TMP
}
{
grep -rh vscode.commands.executeCommand $DIR |
perl -ne 'print if /["\x27"]/' | # Remove occurrences where the command is not on the same line (line doesnt contain quote chars) :(
sed -n 's|.*\(vscode.commands.executeCommand[^,]*\).*|\1|p'| # Remove all params after the first one
sed 's|\(".*"\).*|\1)|'| # Close the parantheses
cat >> $TMP
}
# Count occurrences
cat $TMP | sort | uniq -c | sort -n > $SDK_DEST
rm $TMP
echo Wrote uses of the vscode SDK to $(realpath $SDK_DEST)
{
Regular → Executable
+161 -69
View File
@@ -1,83 +1,175 @@
import fs from "fs"
import path from "path"
import { glob } from "glob"
#!/usr/bin/env node
import archiver from "archiver"
import { cp } from "fs/promises"
import { execSync } from "child_process"
import fs from "fs"
import { cp } from "fs/promises"
import { glob } from "glob"
import minimatch from "minimatch"
import path from "path"
const BUILD_DIR = "dist-standalone"
const SOURCE_DIR = "standalone/runtime-files"
const RUNTIME_DEPS_DIR = "standalone/runtime-files"
await cp(SOURCE_DIR, BUILD_DIR, { recursive: true })
// Run npm install in the distribution directory
console.log("Running npm install in distribution directory...")
const cwd = process.cwd()
process.chdir(BUILD_DIR)
try {
execSync("npm install", { stdio: "inherit" })
// Move the vscode directory into node_modules.
// It can't be installed using npm because it will create a symlink which is not portable.
fs.renameSync("vscode", path.join("node_modules", "vscode"))
} catch (error) {
console.error("Error during setup:", error)
process.exit(1)
} finally {
process.chdir(cwd)
async function main() {
await installNodeDependencies()
await zipDistribution()
}
// Check for native .node modules.
const nativeModules = await glob("**/*.node", { cwd: BUILD_DIR, nodir: true })
if (nativeModules.length > 0) {
console.error("Native node modules cannot be included in the standalone distribution:\n", nativeModules.join("\n"))
process.exit(1)
async function installNodeDependencies() {
await cpr(RUNTIME_DEPS_DIR, BUILD_DIR)
console.log("Running npm install in distribution directory...")
const cwd = process.cwd()
process.chdir(BUILD_DIR)
try {
execSync("npm install", { stdio: "inherit" })
// Move the vscode directory into node_modules.
// It can't be installed using npm because it will create a symlink which cannot be unzipped correctly on windows.
fs.renameSync("vscode", path.join("node_modules", "vscode"))
} catch (error) {
console.error("Error during setup:", error)
process.exit(1)
} finally {
process.chdir(cwd)
}
// Check for native .node modules.
const nativeModules = await glob("**/*.node", { cwd: BUILD_DIR, nodir: true })
if (nativeModules.length > 0) {
console.error("Native node modules cannot be included in the standalone distribution:\n", nativeModules.join("\n"))
process.exit(1)
}
}
// Zip the build directory (excluding any pre-existing output zip).
const zipPath = path.join(BUILD_DIR, "standalone.zip")
const output = fs.createWriteStream(zipPath)
const archive = archiver("zip", { zlib: { level: 3 } })
async function zipDistribution() {
// Zip the build directory (excluding any pre-existing output zip).
const zipPath = path.join(BUILD_DIR, "standalone.zip")
const output = fs.createWriteStream(zipPath)
const archive = archiver("zip", { zlib: { level: 3 } })
output.on("close", () => {
console.log(`Created ${zipPath} (${(archive.pointer() / 1024 / 1024).toFixed(1)} MB)`)
})
archive.on("warning", (err) => {
console.warn(`Warning: ${err}`)
})
archive.on("error", (err) => {
throw err
})
output.on("close", () => {
console.log(`Created ${zipPath} (${(archive.pointer() / 1024 / 1024).toFixed(1)} MB)`)
})
archive.on("warning", (err) => {
console.warn(`Warning: ${err}`)
})
archive.on("error", (err) => {
throw err
})
archive.pipe(output)
archive.glob("**/*", {
cwd: BUILD_DIR,
ignore: ["standalone.zip"],
})
archive.pipe(output)
// Add all the files from the standalone build dir.
archive.glob("**/*", {
cwd: BUILD_DIR,
ignore: ["standalone.zip"],
})
// Add the whole cline directory under "extension"
archive.directory(process.cwd(), "extension", (entry) => {
// Skip certain directories.
const exclude = [
BUILD_DIR + "/",
"node_modules/", // node_modules nearly 1GB.
"webview-ui/node_modules/", // node_modules nearly 1GB.
]
// These node modules are used at runtime as assets, they need to be included.
const include = ["node_modules/@vscode/", "webview-ui/node_modules/katex"]
const name = entry.name
// Exclude the same files as the VCE vscode extension packager.
// Also ignore the dist directory, the build directory for the extension.
const isIgnored = createIsIgnored(["dist/**"])
if (include.some((prefix) => name.startsWith(prefix))) {
// Add the whole cline directory under "extension", except the for the ignored files.
archive.directory(process.cwd(), "extension", (entry) => {
if (isIgnored(entry.name)) {
log_verbose("Ignoring", entry.name)
return false
}
return entry
}
if (exclude.some((prefix) => name.startsWith(prefix))) {
return false
}
if (name.match(/(^|\/)\./)) {
// exclude dot directories
return false
}
return entry
})
})
console.log("Zipping package...")
await archive.finalize()
console.log("Zipping package...")
await archive.finalize()
}
/**
* This is based on https://github.com/microsoft/vscode-vsce/blob/fafad8a63e9cf31179f918eb7a4eeb376834c904/src/package.ts#L1695
* because the .vscodeignore format is not compatible with the `ignore` npm module.
*/
function createIsIgnored(standaloneIgnores) {
const MinimatchOptions = { dot: true }
const defaultIgnore = [
".vscodeignore",
"package-lock.json",
"npm-debug.log",
"yarn.lock",
"yarn-error.log",
"npm-shrinkwrap.json",
".editorconfig",
".npmrc",
".yarnrc",
".gitattributes",
"*.todo",
"tslint.yaml",
".eslintrc*",
".babelrc*",
".prettierrc*",
".cz-config.js",
".commitlintrc*",
"webpack.config.js",
"ISSUE_TEMPLATE.md",
"CONTRIBUTING.md",
"PULL_REQUEST_TEMPLATE.md",
"CODE_OF_CONDUCT.md",
".github",
".travis.yml",
"appveyor.yml",
"**/.git",
"**/.git/**",
"**/*.vsix",
"**/.DS_Store",
"**/*.vsixmanifest",
"**/.vscode-test/**",
"**/.vscode-test-web/**",
]
const rawIgnore = fs.readFileSync(".vscodeignore", "utf8")
// Parse raw ignore by splitting output into lines and filtering out empty lines and comments
const parsedIgnore = rawIgnore
.split(/[\n\r]/)
.map((s) => s.trim())
.filter((s) => !!s)
.filter((i) => !/^\s*#/.test(i))
// Add '/**' to possible folder names
const expandedIgnore = [
...parsedIgnore,
...parsedIgnore.filter((i) => !/(^|\/)[^/]*\*[^/]*$/.test(i)).map((i) => (/\/$/.test(i) ? `${i}**` : `${i}/**`)),
]
// Combine with default ignore list
// Also ignore the dist directory- the build directory for the extension.
const allIgnore = [...defaultIgnore, ...expandedIgnore, ...standaloneIgnores]
// Split into ignore and negate list
const [ignore, negate] = allIgnore.reduce(
(r, e) => (!/^\s*!/.test(e) ? [[...r[0], e], r[1]] : [r[0], [...r[1], e]]),
[[], []],
)
function isIgnored(f) {
return (
ignore.some((i) => minimatch(f, i, MinimatchOptions)) &&
!negate.some((i) => minimatch(f, i.substr(1), MinimatchOptions))
)
}
return isIgnored
}
/* cp -r */
async function cpr(source, dest) {
await cp(source, dest, {
recursive: true,
preserveTimestamps: true,
dereference: false, // preserve symlinks instead of following them
})
}
function log_verbose(...args) {
if (process.argv.includes("-v") || process.argv.includes("--verbose")) {
console.log(...args)
}
}
await main()
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -eu #x
# This compiles the cline-core app, installs it to the user's home directory,
# and runs the service.
CORE_DIR=~/.cline/core
INSTALL_DIR=$CORE_DIR/0.0.1
# Build cline core
npm run compile-standalone
# Remove old unpacked versions to force reinstall
rm -rf $CORE_DIR/* || true
mkdir -p $INSTALL_DIR
cp dist-standalone/standalone.zip $INSTALL_DIR
cd $INSTALL_DIR
unp standalone.zip > /dev/null
pkill -f cline-core.js || true
NODE_PATH=./node_modules node cline-core.js
+227 -38
View File
@@ -8,7 +8,6 @@ import { OpenAiHandler } from "./providers/openai"
import { OllamaHandler } from "./providers/ollama"
import { LmStudioHandler } from "./providers/lmstudio"
import { GeminiHandler } from "./providers/gemini"
import { GeminiCliHandler } from "./providers/gemini-cli"
import { OpenAiNativeHandler } from "./providers/openai-native"
import { ApiStream, ApiStreamUsageChunk } from "./transform/stream"
import { DeepSeekHandler } from "./providers/deepseek"
@@ -28,6 +27,10 @@ import { SambanovaHandler } from "./providers/sambanova"
import { CerebrasHandler } from "./providers/cerebras"
import { SapAiCoreHandler } from "./providers/sapaicore"
import { ClaudeCodeHandler } from "./providers/claude-code"
import { MoonshotHandler } from "./providers/moonshot"
import { GroqHandler } from "./providers/groq"
import { Mode } from "../shared/ChatSettings"
import { HuggingFaceHandler } from "./providers/huggingface"
export interface ApiHandler {
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream
@@ -39,80 +42,266 @@ export interface SingleCompletionHandler {
completePrompt(prompt: string): Promise<string>
}
function createHandlerForProvider(apiProvider: string | undefined, options: any): ApiHandler {
function createHandlerForProvider(
apiProvider: string | undefined,
options: Omit<ApiConfiguration, "apiProvider">,
mode: Mode,
): ApiHandler {
switch (apiProvider) {
case "anthropic":
return new AnthropicHandler(options)
return new AnthropicHandler({
apiKey: options.apiKey,
anthropicBaseUrl: options.anthropicBaseUrl,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
})
case "openrouter":
return new OpenRouterHandler(options)
return new OpenRouterHandler({
openRouterApiKey: options.openRouterApiKey,
openRouterModelId: mode === "plan" ? options.planModeOpenRouterModelId : options.actModeOpenRouterModelId,
openRouterModelInfo: mode === "plan" ? options.planModeOpenRouterModelInfo : options.actModeOpenRouterModelInfo,
openRouterProviderSorting: options.openRouterProviderSorting,
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
})
case "bedrock":
return new AwsBedrockHandler(options)
return new AwsBedrockHandler({
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
awsAccessKey: options.awsAccessKey,
awsSecretKey: options.awsSecretKey,
awsSessionToken: options.awsSessionToken,
awsRegion: options.awsRegion,
awsAuthentication: options.awsAuthentication,
awsBedrockApiKey: options.awsBedrockApiKey,
awsUseCrossRegionInference: options.awsUseCrossRegionInference,
awsBedrockUsePromptCache: options.awsBedrockUsePromptCache,
awsUseProfile: options.awsUseProfile,
awsProfile: options.awsProfile,
awsBedrockEndpoint: options.awsBedrockEndpoint,
awsBedrockCustomSelected:
mode === "plan" ? options.planModeAwsBedrockCustomSelected : options.actModeAwsBedrockCustomSelected,
awsBedrockCustomModelBaseId:
mode === "plan" ? options.planModeAwsBedrockCustomModelBaseId : options.actModeAwsBedrockCustomModelBaseId,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
})
case "vertex":
return new VertexHandler(options)
return new VertexHandler({
vertexProjectId: options.vertexProjectId,
vertexRegion: options.vertexRegion,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
geminiApiKey: options.geminiApiKey,
geminiBaseUrl: options.geminiBaseUrl,
taskId: options.taskId,
})
case "openai":
return new OpenAiHandler(options)
return new OpenAiHandler({
openAiApiKey: options.openAiApiKey,
openAiBaseUrl: options.openAiBaseUrl,
azureApiVersion: options.azureApiVersion,
openAiHeaders: options.openAiHeaders,
openAiModelId: mode === "plan" ? options.planModeOpenAiModelId : options.actModeOpenAiModelId,
openAiModelInfo: mode === "plan" ? options.planModeOpenAiModelInfo : options.actModeOpenAiModelInfo,
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
})
case "ollama":
return new OllamaHandler(options)
return new OllamaHandler({
ollamaBaseUrl: options.ollamaBaseUrl,
ollamaModelId: mode === "plan" ? options.planModeOllamaModelId : options.actModeOllamaModelId,
ollamaApiOptionsCtxNum: options.ollamaApiOptionsCtxNum,
requestTimeoutMs: options.requestTimeoutMs,
})
case "lmstudio":
return new LmStudioHandler(options)
return new LmStudioHandler({
lmStudioBaseUrl: options.lmStudioBaseUrl,
lmStudioModelId: mode === "plan" ? options.planModeLmStudioModelId : options.actModeLmStudioModelId,
})
case "gemini":
return new GeminiHandler(options)
case "gemini-cli":
return new GeminiCliHandler(options)
return new GeminiHandler({
vertexProjectId: options.vertexProjectId,
vertexRegion: options.vertexRegion,
geminiApiKey: options.geminiApiKey,
geminiBaseUrl: options.geminiBaseUrl,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
taskId: options.taskId,
})
case "openai-native":
return new OpenAiNativeHandler(options)
return new OpenAiNativeHandler({
openAiNativeApiKey: options.openAiNativeApiKey,
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "deepseek":
return new DeepSeekHandler(options)
return new DeepSeekHandler({
deepSeekApiKey: options.deepSeekApiKey,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "requesty":
return new RequestyHandler(options)
return new RequestyHandler({
requestyApiKey: options.requestyApiKey,
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
requestyModelId: mode === "plan" ? options.planModeRequestyModelId : options.actModeRequestyModelId,
requestyModelInfo: mode === "plan" ? options.planModeRequestyModelInfo : options.actModeRequestyModelInfo,
})
case "fireworks":
return new FireworksHandler(options)
return new FireworksHandler({
fireworksApiKey: options.fireworksApiKey,
fireworksModelId: mode === "plan" ? options.planModeFireworksModelId : options.actModeFireworksModelId,
fireworksModelMaxCompletionTokens: options.fireworksModelMaxCompletionTokens,
fireworksModelMaxTokens: options.fireworksModelMaxTokens,
})
case "together":
return new TogetherHandler(options)
return new TogetherHandler({
togetherApiKey: options.togetherApiKey,
togetherModelId: mode === "plan" ? options.planModeTogetherModelId : options.actModeTogetherModelId,
})
case "qwen":
return new QwenHandler(options)
return new QwenHandler({
qwenApiKey: options.qwenApiKey,
qwenApiLine: options.qwenApiLine,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
})
case "doubao":
return new DoubaoHandler(options)
return new DoubaoHandler({
doubaoApiKey: options.doubaoApiKey,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "mistral":
return new MistralHandler(options)
return new MistralHandler({
mistralApiKey: options.mistralApiKey,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "vscode-lm":
return new VsCodeLmHandler(options)
return new VsCodeLmHandler({
vsCodeLmModelSelector:
mode === "plan" ? options.planModeVsCodeLmModelSelector : options.actModeVsCodeLmModelSelector,
})
case "cline":
return new ClineHandler(options)
return new ClineHandler({
clineAccountId: options.clineAccountId,
taskId: options.taskId,
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
openRouterProviderSorting: options.openRouterProviderSorting,
openRouterModelId: mode === "plan" ? options.planModeOpenRouterModelId : options.actModeOpenRouterModelId,
openRouterModelInfo: mode === "plan" ? options.planModeOpenRouterModelInfo : options.actModeOpenRouterModelInfo,
})
case "litellm":
return new LiteLlmHandler(options)
return new LiteLlmHandler({
liteLlmApiKey: options.liteLlmApiKey,
liteLlmBaseUrl: options.liteLlmBaseUrl,
liteLlmModelId: mode === "plan" ? options.planModeLiteLlmModelId : options.actModeLiteLlmModelId,
liteLlmModelInfo: mode === "plan" ? options.planModeLiteLlmModelInfo : options.actModeLiteLlmModelInfo,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
liteLlmUsePromptCache: options.liteLlmUsePromptCache,
taskId: options.taskId,
})
case "moonshot":
return new MoonshotHandler({
moonshotApiKey: options.moonshotApiKey,
moonshotApiLine: options.moonshotApiLine,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "huggingface":
return new HuggingFaceHandler({
huggingFaceApiKey: options.huggingFaceApiKey,
huggingFaceModelId: mode === "plan" ? options.planModeHuggingFaceModelId : options.actModeHuggingFaceModelId,
huggingFaceModelInfo:
mode === "plan" ? options.planModeHuggingFaceModelInfo : options.actModeHuggingFaceModelInfo,
})
case "nebius":
return new NebiusHandler(options)
return new NebiusHandler({
nebiusApiKey: options.nebiusApiKey,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "asksage":
return new AskSageHandler(options)
return new AskSageHandler({
asksageApiKey: options.asksageApiKey,
asksageApiUrl: options.asksageApiUrl,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "xai":
return new XAIHandler(options)
return new XAIHandler({
xaiApiKey: options.xaiApiKey,
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "sambanova":
return new SambanovaHandler(options)
return new SambanovaHandler({
sambanovaApiKey: options.sambanovaApiKey,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "cerebras":
return new CerebrasHandler(options)
return new CerebrasHandler({
cerebrasApiKey: options.cerebrasApiKey,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "groq":
return new GroqHandler({
groqApiKey: options.groqApiKey,
groqModelId: mode === "plan" ? options.planModeGroqModelId : options.actModeGroqModelId,
groqModelInfo: mode === "plan" ? options.planModeGroqModelInfo : options.actModeGroqModelInfo,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "sapaicore":
return new SapAiCoreHandler(options)
return new SapAiCoreHandler({
sapAiCoreClientId: options.sapAiCoreClientId,
sapAiCoreClientSecret: options.sapAiCoreClientSecret,
sapAiCoreTokenUrl: options.sapAiCoreTokenUrl,
sapAiResourceGroup: options.sapAiResourceGroup,
sapAiCoreBaseUrl: options.sapAiCoreBaseUrl,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "claude-code":
return new ClaudeCodeHandler(options)
return new ClaudeCodeHandler({
claudeCodePath: options.claudeCodePath,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
})
default:
return new AnthropicHandler(options)
return new AnthropicHandler({
apiKey: options.apiKey,
anthropicBaseUrl: options.anthropicBaseUrl,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
})
}
}
export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
const { apiProvider, ...options } = configuration
export function buildApiHandler(configuration: ApiConfiguration, mode: Mode): ApiHandler {
const { planModeApiProvider, actModeApiProvider, ...options } = configuration
const apiProvider = mode === "plan" ? planModeApiProvider : actModeApiProvider
// Validate thinking budget tokens against model's maxTokens to prevent API errors
// wrapped in a try-catch for safety, but this should never throw
try {
if (options.thinkingBudgetTokens && options.thinkingBudgetTokens > 0) {
const handler = createHandlerForProvider(apiProvider, options)
const thinkingBudgetTokens = mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens
if (thinkingBudgetTokens && thinkingBudgetTokens > 0) {
const handler = createHandlerForProvider(apiProvider, options, mode)
const modelInfo = handler.getModel().info
if (modelInfo.maxTokens && options.thinkingBudgetTokens > modelInfo.maxTokens) {
if (modelInfo.maxTokens && thinkingBudgetTokens > modelInfo.maxTokens) {
const clippedValue = modelInfo.maxTokens - 1
options.thinkingBudgetTokens = clippedValue
if (mode === "plan") {
options.planModeThinkingBudgetTokens = clippedValue
} else {
options.actModeThinkingBudgetTokens = clippedValue
}
} else {
return handler // don't rebuild unless its necessary
}
@@ -121,5 +310,5 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
console.error("buildApiHandler error:", error)
}
return createHandlerForProvider(apiProvider, options)
return createHandlerForProvider(apiProvider, options, mode)
}
+32 -13
View File
@@ -182,22 +182,41 @@ 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 = {
apiModelId: "anthropic.claude-3-7-sonnet-20250219-v1:0",
actModeApiModelId: "anthropic.claude-3-7-sonnet-20250219-v1:0",
awsRegion: "us-east-1",
awsAccessKey: "test-key",
awsSecretKey: "test-secret",
awsSessionToken: "",
awsUseProfile: false,
awsProfile: "",
awsBedrockApiKey: "",
awsBedrockUsePromptCache: false,
awsUseCrossRegionInference: false,
awsBedrockEndpoint: "",
awsBedrockCustomSelected: false,
awsBedrockCustomModelBaseId: undefined,
thinkingBudgetTokens: 1600,
actModeAwsBedrockCustomSelected: false,
actModeAwsBedrockCustomModelBaseId: undefined,
actModeThinkingBudgetTokens: 1600,
}
const mockModelInfo = {
@@ -597,8 +616,8 @@ describe("AwsBedrockHandler", () => {
it("should return raw model ID for custom models", async () => {
const customOptions: ApiHandlerOptions = {
...mockOptions,
awsBedrockCustomSelected: true,
apiModelId:
actModeAwsBedrockCustomSelected: true,
actModeApiModelId:
"arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
}
const customHandler = new AwsBedrockHandler(customOptions)
@@ -612,8 +631,8 @@ describe("AwsBedrockHandler", () => {
it("should not encode custom model IDs with slashes", async () => {
const customOptions: ApiHandlerOptions = {
...mockOptions,
awsBedrockCustomSelected: true,
apiModelId: "my-namespace/my-custom-model",
actModeAwsBedrockCustomSelected: true,
actModeApiModelId: "my-namespace/my-custom-model",
}
const customHandler = new AwsBedrockHandler(customOptions)
@@ -661,8 +680,8 @@ describe("AwsBedrockHandler", () => {
it("should not apply cross-region prefix for custom models even when enabled", async () => {
const customCrossRegionOptions: ApiHandlerOptions = {
...mockOptions,
awsBedrockCustomSelected: true,
apiModelId: "arn:aws:bedrock:us-west-2:123456789012:custom-model/my-model",
actModeAwsBedrockCustomSelected: true,
actModeApiModelId: "arn:aws:bedrock:us-west-2:123456789012:custom-model/my-model",
awsUseCrossRegionInference: true,
}
const customCrossRegionHandler = new AwsBedrockHandler(customCrossRegionOptions)
@@ -674,10 +693,10 @@ describe("AwsBedrockHandler", () => {
it("should handle UltraThink model ARN correctly", async () => {
const ultraThinkOptions: ApiHandlerOptions = {
...mockOptions,
awsBedrockCustomSelected: true,
apiModelId:
actModeAwsBedrockCustomSelected: true,
actModeApiModelId:
"arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
awsBedrockCustomModelBaseId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
actModeAwsBedrockCustomModelBaseId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
}
const ultraThinkHandler = new AwsBedrockHandler(ultraThinkOptions)
+7 -4
View File
@@ -26,7 +26,7 @@ describe("OllamaHandler", () => {
beforeEach(() => {
options = {
ollamaModelId: "llama2",
actModeOllamaModelId: "llama2",
ollamaBaseUrl: "http://localhost:11434",
}
handler = new OllamaHandler(options)
@@ -45,8 +45,10 @@ describe("OllamaHandler", () => {
this.skip()
}
this.timeout(5000)
// Ensure client is initialized
const client = (handler as any).ensureClient()
// Mock the Ollama client's chat method
const chatStub = sinon.stub(handler["client"], "chat").resolves({
const chatStub = sinon.stub(client, "chat").resolves({
[Symbol.asyncIterator]: async function* () {
yield {
message: { content: "Hello, world!" },
@@ -139,8 +141,9 @@ describe("OllamaHandler", () => {
// Restore real timers for this test
clock.restore()
// Mock the Ollama client's chat method to fail on first call and succeed on second
const chatStub = sinon.stub(handler["client"], "chat")
// Ensure client is initialized and mock the Ollama client's chat method to fail on first call and succeed on second
const client = (handler as any).ensureClient()
const chatStub = sinon.stub(client, "chat")
// First call throws an error
chatStub.onFirstCall().rejects(new Error("API Error"))
+32 -10
View File
@@ -5,20 +5,42 @@ import { anthropicDefaultModelId, AnthropicModelId, anthropicModels, ApiHandlerO
import { ApiHandler } from "../index"
import { ApiStream } from "../transform/stream"
export class AnthropicHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: Anthropic
interface AnthropicHandlerOptions {
apiKey?: string
anthropicBaseUrl?: string
apiModelId?: string
thinkingBudgetTokens?: number
}
constructor(options: ApiHandlerOptions) {
export class AnthropicHandler implements ApiHandler {
private options: AnthropicHandlerOptions
private client: Anthropic | undefined
constructor(options: AnthropicHandlerOptions) {
this.options = options
this.client = new Anthropic({
apiKey: this.options.apiKey,
baseURL: this.options.anthropicBaseUrl || undefined,
})
}
private ensureClient(): Anthropic {
if (!this.client) {
if (!this.options.apiKey) {
throw new Error("Anthropic API key is required")
}
try {
this.client = new Anthropic({
apiKey: this.options.apiKey,
baseURL: this.options.anthropicBaseUrl || undefined,
})
} catch (error) {
throw new Error(`Error creating Anthropic client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
let stream: AnthropicStream<Anthropic.RawMessageStreamEvent>
const modelId = model.id
@@ -44,7 +66,7 @@ export class AnthropicHandler implements ApiHandler {
)
const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
stream = await this.client.messages.create(
stream = await client.messages.create(
{
model: modelId,
thinking: reasoningOn ? { type: "enabled", budget_tokens: budget_tokens } : undefined,
@@ -118,7 +140,7 @@ export class AnthropicHandler implements ApiHandler {
break
}
default: {
stream = await this.client.messages.create({
stream = await client.messages.create({
model: modelId,
max_tokens: model.info.maxTokens || 8192,
temperature: 0,
+9 -10
View File
@@ -1,16 +1,15 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ApiHandler } from ".."
import {
ApiHandlerOptions,
ModelInfo,
AskSageModelId,
askSageModels,
askSageDefaultModelId,
askSageDefaultURL,
} from "@shared/api"
import { ModelInfo, AskSageModelId, askSageModels, askSageDefaultModelId, askSageDefaultURL } from "@shared/api"
import { ApiStream } from "../transform/stream"
import { withRetry } from "../retry"
interface AskSageHandlerOptions {
asksageApiKey?: string
asksageApiUrl?: string
apiModelId?: string
}
type AskSageRequest = {
system_prompt: string
message: {
@@ -31,11 +30,11 @@ type AskSageResponse = {
}
export class AskSageHandler implements ApiHandler {
private options: ApiHandlerOptions
private options: AskSageHandlerOptions
private apiUrl: string
private apiKey: string
constructor(options: ApiHandlerOptions) {
constructor(options: AskSageHandlerOptions) {
console.log("init api url", options.asksageApiUrl, askSageDefaultURL)
this.options = options
this.apiKey = options.asksageApiKey || ""
+43 -11
View File
@@ -2,7 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
import { withRetry } from "../retry"
import { ApiHandler } from "../"
import { convertToR1Format } from "../transform/r1-format"
import { ApiHandlerOptions, bedrockDefaultModelId, BedrockModelId, bedrockModels, ModelInfo } from "@shared/api"
import { bedrockDefaultModelId, BedrockModelId, bedrockModels, ModelInfo } from "@shared/api"
import { calculateApiCostOpenAI } from "../../utils/cost"
import { ApiStream } from "../transform/stream"
import { fromNodeProviderChain } from "@aws-sdk/credential-providers"
@@ -16,6 +16,24 @@ import {
// Import proper AWS SDK types
import type { Message, ContentBlock } from "@aws-sdk/client-bedrock-runtime"
interface AwsBedrockHandlerOptions {
apiModelId?: string
awsAccessKey?: string
awsSecretKey?: string
awsSessionToken?: string
awsRegion?: string
awsAuthentication?: string
awsBedrockApiKey?: string
awsUseCrossRegionInference?: boolean
awsBedrockUsePromptCache?: boolean
awsUseProfile?: boolean
awsProfile?: string
awsBedrockEndpoint?: string
awsBedrockCustomSelected?: boolean
awsBedrockCustomModelBaseId?: BedrockModelId
thinkingBudgetTokens?: number
}
// Extend AWS SDK types to include additionalModelResponseFields
interface ExtendedMetadata {
usage?: {
@@ -90,9 +108,9 @@ interface ProviderChainOptions {
// https://docs.anthropic.com/en/api/claude-on-amazon-bedrock
export class AwsBedrockHandler implements ApiHandler {
private options: ApiHandlerOptions
private options: AwsBedrockHandlerOptions
constructor(options: ApiHandlerOptions) {
constructor(options: AwsBedrockHandlerOptions) {
this.options = options
}
@@ -170,7 +188,10 @@ export class AwsBedrockHandler implements ApiHandler {
}> {
// Configure provider options
const providerOptions: ProviderChainOptions = {}
if (this.options.awsUseProfile) {
const useProfile =
(this.options.awsAuthentication === undefined && this.options.awsUseProfile) ||
this.options.awsAuthentication === "profile"
if (useProfile) {
// For profile-based auth, always use ignoreCache to detect credential file changes
// This solves the AWS Identity Manager issue where credential files change externally
providerOptions.ignoreCache = true
@@ -184,7 +205,7 @@ export class AwsBedrockHandler implements ApiHandler {
return await AwsBedrockHandler.withTempEnv(
() => {
AwsBedrockHandler.setEnv("AWS_REGION", this.options.awsRegion)
if (this.options.awsUseProfile) {
if (useProfile) {
AwsBedrockHandler.setEnv("AWS_PROFILE", this.options.awsProfile)
} else {
delete process.env["AWS_PROFILE"]
@@ -208,15 +229,26 @@ export class AwsBedrockHandler implements ApiHandler {
* Creates a BedrockRuntimeClient with the appropriate credentials
*/
private async getBedrockClient(): Promise<BedrockRuntimeClient> {
const credentials = await this.getAwsCredentials()
let auth: any
if (this.options.awsAuthentication === "apikey") {
auth = {
token: { token: this.options.awsBedrockApiKey },
authSchemePreference: ["httpBearerAuth"],
}
} else {
const credentials = await this.getAwsCredentials()
auth = {
credentials: {
accessKeyId: credentials.accessKeyId,
secretAccessKey: credentials.secretAccessKey,
sessionToken: credentials.sessionToken,
},
}
}
return new BedrockRuntimeClient({
region: this.getRegion(),
credentials: {
accessKeyId: credentials.accessKeyId,
secretAccessKey: credentials.secretAccessKey,
sessionToken: credentials.sessionToken,
},
...auth,
...(this.options.awsBedrockEndpoint && { endpoint: this.options.awsBedrockEndpoint }),
})
}
+47 -17
View File
@@ -1,38 +1,63 @@
import { Anthropic } from "@anthropic-ai/sdk"
import Cerebras from "@cerebras/cerebras_cloud_sdk"
import { withRetry } from "../retry"
import { ApiHandlerOptions, ModelInfo, CerebrasModelId, cerebrasDefaultModelId, cerebrasModels } from "@shared/api"
import { ModelInfo, CerebrasModelId, cerebrasDefaultModelId, cerebrasModels } from "@shared/api"
import { ApiHandler } from "../index"
import { ApiStream } from "@api/transform/stream"
interface CerebrasHandlerOptions {
cerebrasApiKey?: string
apiModelId?: string
}
export class CerebrasHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: Cerebras
private options: CerebrasHandlerOptions
private client: Cerebras | undefined
constructor(options: ApiHandlerOptions) {
constructor(options: CerebrasHandlerOptions) {
this.options = options
}
// Clean and validate the API key
const cleanApiKey = this.options.cerebrasApiKey?.trim()
private ensureClient(): Cerebras {
if (!this.client) {
// Clean and validate the API key
const cleanApiKey = this.options.cerebrasApiKey?.trim()
if (!cleanApiKey) {
throw new Error("Cerebras API key is required")
if (!cleanApiKey) {
throw new Error("Cerebras API key is required")
}
try {
this.client = new Cerebras({
apiKey: cleanApiKey,
timeout: 30000, // 30 second timeout
})
} catch (error) {
throw new Error(`Error creating Cerebras client: ${error.message}`)
}
}
this.client = new Cerebras({
apiKey: cleanApiKey,
timeout: 30000, // 30 second timeout
})
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
// Convert Anthropic messages to Cerebras format
const cerebrasMessages: Array<{
role: "system" | "user" | "assistant"
content: string
}> = [{ role: "system", content: systemPrompt }]
// Helper function to strip thinking tags from content
const stripThinkingTags = (content: string): string => {
return content.replace(/<think>[\s\S]*?<\/think>/g, "").trim()
}
// Check if this is a reasoning model that uses thinking tags
const modelId = this.getModel().id
const isReasoningModel = modelId.includes("qwen") || modelId.includes("deepseek-r1-distill")
// Convert Anthropic messages to Cerebras format
for (const message of messages) {
if (message.role === "user") {
@@ -50,7 +75,7 @@ export class CerebrasHandler implements ApiHandler {
: message.content
cerebrasMessages.push({ role: "user", content })
} else if (message.role === "assistant") {
const content = Array.isArray(message.content)
let content = Array.isArray(message.content)
? message.content
.map((block) => {
if (block.type === "text") {
@@ -60,12 +85,19 @@ export class CerebrasHandler implements ApiHandler {
})
.join("\n")
: message.content || ""
// Strip thinking tags from assistant messages for reasoning models
// so the model doesn't see its own thinking in the conversation history
if (isReasoningModel) {
content = stripThinkingTags(content)
}
cerebrasMessages.push({ role: "assistant", content })
}
}
try {
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: this.getModel().id,
messages: cerebrasMessages,
temperature: 0,
@@ -74,8 +106,6 @@ export class CerebrasHandler implements ApiHandler {
// Handle streaming response
let reasoning: string | null = null // Track reasoning content for models that support thinking
const modelId = this.getModel().id
const isReasoningModel = modelId.includes("qwen") || modelId.includes("deepseek-r1-distill")
for await (const chunk of stream as any) {
// Type assertion for the streaming chunk
+11 -4
View File
@@ -1,15 +1,21 @@
import type { Anthropic } from "@anthropic-ai/sdk"
import { claudeCodeDefaultModelId, ClaudeCodeModelId, claudeCodeModels, type ApiHandlerOptions } from "@/shared/api"
import { claudeCodeDefaultModelId, ClaudeCodeModelId, claudeCodeModels } from "@/shared/api"
import { type ApiHandler } from ".."
import { ApiStreamUsageChunk, type ApiStream } from "../transform/stream"
import { withRetry } from "../retry"
import { runClaudeCode } from "@/integrations/claude-code/run"
import { filterMessagesForClaudeCode } from "@/integrations/claude-code/message-filter"
export class ClaudeCodeHandler implements ApiHandler {
private options: ApiHandlerOptions
interface ClaudeCodeHandlerOptions {
claudeCodePath?: string
apiModelId?: string
thinkingBudgetTokens?: number
}
constructor(options: ApiHandlerOptions) {
export class ClaudeCodeHandler implements ApiHandler {
private options: ClaudeCodeHandlerOptions
constructor(options: ClaudeCodeHandlerOptions) {
this.options = options
}
@@ -27,6 +33,7 @@ export class ClaudeCodeHandler implements ApiHandler {
messages: filteredMessages,
path: this.options.claudeCodePath,
modelId: this.getModel().id,
thinkingBudgetTokens: this.options.thinkingBudgetTokens,
})
// Usage is included with assistant messages,
+154 -105
View File
@@ -1,143 +1,195 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { ApiHandler } from "../"
import { ApiHandlerOptions, ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "@shared/api"
import { ClineAccountService } from "@/services/account/ClineAccountService"
import { ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "@shared/api"
import { createOpenRouterStream } from "../transform/openrouter-stream"
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
import axios from "axios"
import { OpenRouterErrorResponse } from "./types"
import { withRetry } from "../retry"
import { AuthService } from "@/services/auth/AuthService"
import OpenAI from "openai"
import { version as extensionVersion } from "../../../package.json"
import { shouldSkipReasoningForModel } from "@utils/model-utils"
import { CLINE_ACCOUNT_AUTH_ERROR_MESSAGE } from "@/shared/ClineAccount"
import { clineEnvConfig } from "@/config"
interface ClineHandlerOptions {
taskId?: string
reasoningEffort?: string
thinkingBudgetTokens?: number
openRouterProviderSorting?: string
openRouterModelId?: string
openRouterModelInfo?: ModelInfo
clineAccountId?: string
}
export class ClineHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
private options: ClineHandlerOptions
private clineAccountService = ClineAccountService.getInstance()
private _authService: AuthService
private client: OpenAI | undefined
private readonly _baseUrl = clineEnvConfig.apiBaseUrl
lastGenerationId?: string
private counter = 0
constructor(options: ApiHandlerOptions) {
constructor(options: ClineHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://api.cline.bot/v1",
apiKey: this.options.clineApiKey || "",
defaultHeaders: {
"HTTP-Referer": "https://cline.bot", // Optional, for including your app on cline.bot rankings.
"X-Title": "Cline", // Optional. Shows in rankings on cline.bot.
"X-Task-ID": this.options.taskId || "", // Include the task ID in the request headers
},
})
this._authService = AuthService.getInstance()
}
private async ensureClient(): Promise<OpenAI> {
const clineAccountAuthToken = await this._authService.getAuthToken()
if (!clineAccountAuthToken) {
throw new Error(CLINE_ACCOUNT_AUTH_ERROR_MESSAGE)
}
if (!this.client) {
try {
this.client = new OpenAI({
baseURL: `${this._baseUrl}/api/v1`,
apiKey: clineAccountAuthToken,
defaultHeaders: {
"HTTP-Referer": "https://cline.bot",
"X-Title": "Cline",
"X-Task-ID": this.options.taskId || "",
"X-Cline-Version": extensionVersion,
},
})
} catch (error: any) {
throw new Error(`Error creating Cline client: ${error.message}`)
}
}
// Ensure the client is always using the latest auth token
this.client.apiKey = clineAccountAuthToken
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
this.lastGenerationId = undefined
try {
const client = await this.ensureClient()
const stream = await createOpenRouterStream(
this.client,
systemPrompt,
messages,
this.getModel(),
this.options.reasoningEffort,
this.options.thinkingBudgetTokens,
this.options.openRouterProviderSorting,
)
this.lastGenerationId = undefined
let didOutputUsage: boolean = false
let didOutputUsage: boolean = false
for await (const chunk of stream) {
// openrouter returns an error object instead of the openai sdk throwing an error
if ("error" in chunk) {
const error = chunk.error as OpenRouterErrorResponse["error"]
console.error(`Cline API Error: ${error?.code} - ${error?.message}`)
// Include metadata in the error message if available
const metadataStr = error.metadata ? `\nMetadata: ${JSON.stringify(error.metadata, null, 2)}` : ""
throw new Error(`Cline API Error ${error.code}: ${error.message}${metadataStr}`)
}
const stream = await createOpenRouterStream(
client,
systemPrompt,
messages,
this.getModel(),
this.options.reasoningEffort,
this.options.thinkingBudgetTokens,
this.options.openRouterProviderSorting,
)
if (!this.lastGenerationId && chunk.id) {
this.lastGenerationId = chunk.id
}
// Check for mid-stream error via finish_reason
const choice = chunk.choices?.[0]
// OpenRouter may return finish_reason = "error" with error details
if ((choice?.finish_reason as string) === "error") {
const choiceWithError = choice as any
if (choiceWithError.error) {
const error = choiceWithError.error
console.error(`Cline Mid-Stream Error: ${error.code || error.type || "Unknown"} - ${error.message}`)
throw new Error(`Cline Mid-Stream Error: ${error.code || error.type || "Unknown"} - ${error.message}`)
} else {
throw new Error("Cline Mid-Stream Error: Stream terminated with error status but no error details provided")
for await (const chunk of stream) {
// openrouter returns an error object instead of the openai sdk throwing an error
if ("error" in chunk) {
const error = chunk.error as OpenRouterErrorResponse["error"]
console.error(`Cline API Error: ${error?.code} - ${error?.message}`)
// Include metadata in the error message if available
const metadataStr = error.metadata ? `\nMetadata: ${JSON.stringify(error.metadata, null, 2)}` : ""
throw new Error(`Cline API Error ${error.code}: ${error.message}${metadataStr}`)
}
}
const delta = choice?.delta
if (delta?.content) {
yield {
type: "text",
text: delta.content,
if (!this.lastGenerationId && chunk.id) {
this.lastGenerationId = chunk.id
}
}
// Reasoning tokens are returned separately from the content
if ("reasoning" in delta && delta.reasoning) {
yield {
type: "reasoning",
// Check for mid-stream error via finish_reason
const choice = chunk.choices?.[0]
// OpenRouter may return finish_reason = "error" with error details
if ((choice?.finish_reason as string) === "error") {
const choiceWithError = choice as any
if (choiceWithError.error) {
const error = choiceWithError.error
console.error(`Cline Mid-Stream Error: ${error.code || error.type || "Unknown"} - ${error.message}`)
throw new Error(`Cline Mid-Stream Error: ${error.code || error.type || "Unknown"} - ${error.message}`)
} else {
throw new Error(
"Cline Mid-Stream Error: Stream terminated with error status but no error details provided",
)
}
}
const delta = choice?.delta
if (delta?.content) {
yield {
type: "text",
text: delta.content,
}
}
// Reasoning tokens are returned separately from the content
// Skip reasoning content for Grok 4 models since it only displays "thinking" without providing useful information
if ("reasoning" in delta && delta.reasoning && !shouldSkipReasoningForModel(this.options.openRouterModelId)) {
yield {
type: "reasoning",
// @ts-ignore-next-line
reasoning: delta.reasoning,
}
}
if (!didOutputUsage && chunk.usage) {
// @ts-ignore-next-line
reasoning: delta.reasoning,
}
}
let totalCost = (chunk.usage.cost || 0) + (chunk.usage.cost_details?.upstream_inference_cost || 0)
const modelId = this.getModel().id
if (!didOutputUsage && chunk.usage) {
// @ts-ignore-next-line
let totalCost = (chunk.usage.cost || 0) + (chunk.usage.cost_details?.upstream_inference_cost || 0)
const modelId = this.getModel().id
const provider = modelId.split("/")[0]
// const provider = modelId.split("/")[0]
// // If provider is x-ai, set totalCost to 0 (we're doing a promo)
// if (provider === "x-ai") {
// totalCost = 0
// }
// If provider is x-ai, set totalCost to 0 (we're doing a promo)
if (provider === "x-ai") {
totalCost = 0
}
if (modelId.includes("gemini")) {
yield {
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
inputTokens: (chunk.usage.prompt_tokens || 0) - (chunk.usage.prompt_tokens_details?.cached_tokens || 0),
outputTokens: chunk.usage.completion_tokens || 0,
// @ts-ignore-next-line
totalCost,
}
} else {
yield {
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
// @ts-ignore-next-line
totalCost,
if (modelId.includes("gemini")) {
yield {
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
inputTokens:
(chunk.usage.prompt_tokens || 0) - (chunk.usage.prompt_tokens_details?.cached_tokens || 0),
outputTokens: chunk.usage.completion_tokens || 0,
// @ts-ignore-next-line
totalCost,
}
} else {
yield {
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
// @ts-ignore-next-line
totalCost,
}
}
didOutputUsage = true
}
didOutputUsage = true
}
}
// Fallback to generation endpoint if usage chunk not returned
if (!didOutputUsage) {
const apiStreamUsage = await this.getApiStreamUsage()
if (apiStreamUsage) {
yield apiStreamUsage
// Fallback to generation endpoint if usage chunk not returned
if (!didOutputUsage) {
console.warn("Cline API did not return usage chunk, fetching from generation endpoint")
const apiStreamUsage = await this.getApiStreamUsage()
if (apiStreamUsage) {
yield apiStreamUsage
}
}
} catch (error) {
console.error("Cline API Error:", error)
throw error
}
}
async getApiStreamUsage(): Promise<ApiStreamUsageChunk | undefined> {
if (this.lastGenerationId) {
try {
const response = await axios.get(`https://api.cline.bot/v1/generation?id=${this.lastGenerationId}`, {
// TODO: replace this with firebase auth
// TODO: use global API Host
const response = await axios.get(`${this.clineAccountService.baseUrl}/generation?id=${this.lastGenerationId}`, {
headers: {
Authorization: `Bearer ${this.options.clineApiKey}`,
Authorization: `Bearer ${this.options.clineAccountId}`,
},
timeout: 15_000, // this request hangs sometimes
})
@@ -175,9 +227,6 @@ export class ClineHandler implements ApiHandler {
getModel(): { id: string; info: ModelInfo } {
let modelId = this.options.openRouterModelId
if (modelId === "x-ai/grok-3") {
modelId = "x-ai/grok-3-beta"
}
const modelInfo = this.options.openRouterModelInfo
if (modelId && modelInfo) {
return { id: modelId, info: modelInfo }
+28 -9
View File
@@ -8,16 +8,34 @@ import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { convertToR1Format } from "../transform/r1-format"
export class DeepSeekHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
interface DeepSeekHandlerOptions {
deepSeekApiKey?: string
apiModelId?: string
}
constructor(options: ApiHandlerOptions) {
export class DeepSeekHandler implements ApiHandler {
private options: DeepSeekHandlerOptions
private client: OpenAI | undefined
constructor(options: DeepSeekHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://api.deepseek.com/v1",
apiKey: this.options.deepSeekApiKey,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.deepSeekApiKey) {
throw new Error("DeepSeek API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://api.deepseek.com/v1",
apiKey: this.options.deepSeekApiKey,
})
} catch (error) {
throw new Error(`Error creating DeepSeek client: ${error.message}`)
}
}
return this.client
}
private async *yieldUsage(info: ModelInfo, usage: OpenAI.Completions.CompletionUsage | undefined): ApiStream {
@@ -54,6 +72,7 @@ export class DeepSeekHandler implements ApiHandler {
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
const isDeepseekReasoner = model.id.includes("deepseek-reasoner")
@@ -67,7 +86,7 @@ export class DeepSeekHandler implements ApiHandler {
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
}
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: model.id,
max_completion_tokens: model.info.maxTokens,
messages: openAiMessages,
+28 -9
View File
@@ -1,20 +1,38 @@
import { ApiHandler } from ".."
import { ApiHandlerOptions, doubaoDefaultModelId, DoubaoModelId, doubaoModels, ModelInfo } from "@shared/api"
import { doubaoDefaultModelId, DoubaoModelId, doubaoModels, ModelInfo } from "@shared/api"
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { withRetry } from "../retry"
interface DoubaoHandlerOptions {
doubaoApiKey?: string
apiModelId?: string
}
export class DoubaoHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
constructor(options: ApiHandlerOptions) {
private options: DoubaoHandlerOptions
private client: OpenAI | undefined
constructor(options: DoubaoHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://ark.cn-beijing.volces.com/api/v3/",
apiKey: this.options.doubaoApiKey,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.doubaoApiKey) {
throw new Error("Doubao API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://ark.cn-beijing.volces.com/api/v3/",
apiKey: this.options.doubaoApiKey,
})
} catch (error) {
throw new Error(`Error creating Doubao client: ${error.message}`)
}
}
return this.client
}
getModel(): { id: DoubaoModelId; info: ModelInfo } {
@@ -31,12 +49,13 @@ export class DoubaoHandler implements ApiHandler {
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: model.id,
max_completion_tokens: model.info.maxTokens,
messages: openAiMessages,
+31 -17
View File
@@ -2,31 +2,45 @@ import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { withRetry } from "../retry"
import { ApiHandler } from ".."
import {
ApiHandlerOptions,
DeepSeekModelId,
ModelInfo,
deepSeekDefaultModelId,
deepSeekModels,
openAiModelInfoSaneDefaults,
} from "../../shared/api"
import { ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
export class FireworksHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
interface FireworksHandlerOptions {
fireworksApiKey?: string
fireworksModelId?: string
fireworksModelMaxCompletionTokens?: number
fireworksModelMaxTokens?: number
}
constructor(options: ApiHandlerOptions) {
export class FireworksHandler implements ApiHandler {
private options: FireworksHandlerOptions
private client: OpenAI | undefined
constructor(options: FireworksHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://api.fireworks.ai/inference/v1",
apiKey: this.options.fireworksApiKey,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.fireworksApiKey) {
throw new Error("Fireworks API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://api.fireworks.ai/inference/v1",
apiKey: this.options.fireworksApiKey,
})
} catch (error) {
throw new Error(`Error creating Fireworks client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const modelId = this.options.fireworksModelId ?? ""
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
@@ -34,7 +48,7 @@ export class FireworksHandler implements ApiHandler {
...convertToOpenAiMessages(messages),
]
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: modelId,
...(this.options.fireworksModelMaxCompletionTokens
? { max_completion_tokens: this.options.fireworksModelMaxCompletionTokens }
-418
View File
@@ -1,418 +0,0 @@
/**
* Gemini CLI Provider - OAuth-based API Handler
*
* This implementation provides access to Google's Gemini models through OAuth authentication,
* leveraging the same authentication mechanism as the official Gemini CLI tool.
*
* Attribution: This implementation is inspired by and uses concepts from the Google Gemini CLI,
* which is licensed under the Apache License 2.0.
* Original project: https://github.com/google-gemini/gemini-cli
*
* Copyright 2025 Google LLC
* Licensed under the Apache License, Version 2.0
*
* Key features:
* - OAuth2 authentication (no API keys required)
* - Auto-discovery of Google Cloud project IDs
* - Real-time streaming via Server-Sent Events
* - Free tier access through Google's Code Assist API
* - Compatible with personal Google accounts only
*/
import type { Anthropic } from "@anthropic-ai/sdk"
import { OAuth2Client } from "google-auth-library"
import fs from "fs/promises"
import path from "path"
import os from "os"
import * as readline from "readline"
import { Readable } from "stream"
import { ApiHandler } from "../"
import { ApiHandlerOptions, GeminiCliModelId, geminiCliModels, ModelInfo, geminiCliDefaultModelId } from "@shared/api"
import { convertAnthropicMessageToGemini } from "../transform/gemini-format"
import { ApiStream } from "../transform/stream"
import { withRetry } from "../retry"
const CODE_ASSIST_ENDPOINT = "https://cloudcode-pa.googleapis.com"
const CODE_ASSIST_API_VERSION = "v1internal"
// OAuth configuration
const OAUTH_CLIENT_ID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com"
// Change this line in setup.js:
const OAUTH_CLIENT_SECRET = "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"
const OAUTH_REDIRECT_URI = "http://localhost:45289"
interface OAuthCredentials {
access_token: string
refresh_token: string
scope: string
token_type: string
expiry_date: number
}
interface GeminiCliHandlerOptions extends ApiHandlerOptions {
geminiCliOAuthPath?: string
geminiCliProjectId?: string
}
/**
* Handler for Google's Gemini API via OAuth (Gemini CLI style).
*
* This provider uses OAuth authentication instead of API keys, making it suitable
* for users who have already authenticated with the Gemini CLI tool.
* It automatically discovers project IDs and works with the free tier.
*/
export class GeminiCliHandler implements ApiHandler {
private options: GeminiCliHandlerOptions
private authClient: OAuth2Client
private projectId: string | null = null
private authInitialized: boolean = false
constructor(options: GeminiCliHandlerOptions) {
this.options = options
this.authClient = new OAuth2Client(OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET, OAUTH_REDIRECT_URI)
}
/**
* Load OAuth credentials from the file system
*/
private async loadOAuthCredentials(): Promise<OAuthCredentials> {
const credPath = this.options.geminiCliOAuthPath || path.join(os.homedir(), ".gemini", "oauth_creds.json")
try {
const data = await fs.readFile(credPath, "utf8")
return JSON.parse(data)
} catch (err) {
throw new Error(`Failed to load OAuth credentials from ${credPath}. Please authenticate with 'gemini auth' first.`)
}
}
/**
* Call a Code Assist API endpoint
*/
private async callEndpoint(method: string, body: any, retryAuth: boolean = true): Promise<any> {
console.log(`[GeminiCLI] Calling endpoint: ${method}`)
console.log(`[GeminiCLI] Request body:`, JSON.stringify(body, null, 2))
try {
const res = await this.authClient.request({
url: `${CODE_ASSIST_ENDPOINT}/${CODE_ASSIST_API_VERSION}:${method}`,
method: "POST",
headers: {
"Content-Type": "application/json",
},
responseType: "json",
body: JSON.stringify(body),
})
console.log(`[GeminiCLI] Response status:`, res.status)
console.log(`[GeminiCLI] Response data:`, JSON.stringify(res.data, null, 2))
return res.data
} catch (error: any) {
console.error(`[GeminiCLI] Error calling ${method}:`, error)
console.error(`[GeminiCLI] Error response:`, error.response?.data)
console.error(`[GeminiCLI] Error status:`, error.response?.status)
console.error(`[GeminiCLI] Error message:`, error.message)
// If we get a 401 and haven't retried yet, try refreshing auth
if (error.response?.status === 401 && retryAuth) {
console.log(`[GeminiCLI] Got 401, attempting to refresh authentication...`)
await this.initializeAuth(true) // Force refresh
return this.callEndpoint(method, body, false) // Retry without further auth retries
}
throw error
}
}
/**
* Discover or retrieve the project ID
*/
private async discoverProjectId(): Promise<string> {
// If we already have a project ID, use it
if (this.options.geminiCliProjectId) {
return this.options.geminiCliProjectId
}
// If we've already discovered it, return it
if (this.projectId) {
return this.projectId
}
// Start with a default project ID (can be anything for personal OAuth)
const initialProjectId = "default"
// Prepare client metadata
const clientMetadata = {
ideType: "IDE_UNSPECIFIED",
platform: "PLATFORM_UNSPECIFIED",
pluginType: "GEMINI",
duetProject: initialProjectId,
}
try {
// Call loadCodeAssist to discover the actual project ID
const loadRequest = {
cloudaicompanionProject: initialProjectId,
metadata: clientMetadata,
}
const loadResponse = await this.callEndpoint("loadCodeAssist", loadRequest)
// Check if we already have a project ID from the response
if (loadResponse.cloudaicompanionProject) {
this.projectId = loadResponse.cloudaicompanionProject
return this.projectId as string
}
// If no existing project, we need to onboard
const defaultTier = loadResponse.allowedTiers?.find((tier: any) => tier.isDefault)
const tierId = defaultTier?.id || "free-tier"
const onboardRequest = {
tierId: tierId,
cloudaicompanionProject: initialProjectId,
metadata: clientMetadata,
}
let lroResponse = await this.callEndpoint("onboardUser", onboardRequest)
// Poll until operation is complete
while (!lroResponse.done) {
await new Promise((resolve) => setTimeout(resolve, 2000))
lroResponse = await this.callEndpoint("onboardUser", onboardRequest)
}
const discoveredProjectId = lroResponse.response?.cloudaicompanionProject?.id || initialProjectId
this.projectId = discoveredProjectId
return this.projectId as string
} catch (error: any) {
console.error("Failed to discover project ID:", error.response?.data || error.message)
throw new Error("Could not discover project ID. Make sure you're authenticated with 'gemini auth'.")
}
}
/**
* Initialize the OAuth client with credentials
*/
private async initializeAuth(forceRefresh: boolean = false): Promise<void> {
// Check if we need to initialize or refresh
if (this.authInitialized && !forceRefresh) {
// Check if token is still valid
const credentials = this.authClient.credentials
if (credentials && credentials.expiry_date && Date.now() < credentials.expiry_date) {
console.log(`[GeminiCLI] Auth already initialized and token still valid`)
return
}
}
console.log(`[GeminiCLI] Initializing OAuth authentication...`)
const credentials = await this.loadOAuthCredentials()
const isExpired = credentials.expiry_date ? Date.now() > credentials.expiry_date : false
console.log(`[GeminiCLI] Loaded credentials:`, {
hasAccessToken: !!credentials.access_token,
hasRefreshToken: !!credentials.refresh_token,
tokenType: credentials.token_type,
expiryDate: credentials.expiry_date,
isExpired: isExpired,
})
this.authClient.setCredentials(credentials)
// If token is expired and we have a refresh token, try to refresh
if (isExpired && credentials.refresh_token) {
console.log(`[GeminiCLI] Token expired, attempting to refresh...`)
try {
const { credentials: newCredentials } = await this.authClient.refreshAccessToken()
console.log(`[GeminiCLI] Token refreshed successfully`)
// Note: In a real implementation, you'd want to save the new credentials back to the file
// For now, we'll just use them in memory
} catch (error) {
console.error(`[GeminiCLI] Failed to refresh token:`, error)
// Continue with the expired token - the API might still accept it
}
}
this.authInitialized = true
console.log(`[GeminiCLI] OAuth client configured`)
}
/**
* Parse Server-Sent Events from a stream
*/
private async *parseSSEStream(stream: Readable): AsyncGenerator<any> {
const rl = readline.createInterface({
input: stream,
crlfDelay: Infinity,
})
let bufferedLines: string[] = []
for await (const line of rl) {
// Blank lines separate JSON objects in the stream
if (line === "") {
if (bufferedLines.length === 0) {
continue
}
try {
const jsonData = JSON.parse(bufferedLines.join("\n"))
yield jsonData
} catch (parseError) {
console.error("Error parsing JSON chunk:", parseError)
}
bufferedLines = []
} else if (line.startsWith("data: ")) {
bufferedLines.push(line.slice(6).trim())
}
}
// Process any remaining buffered content
if (bufferedLines.length > 0) {
try {
const jsonData = JSON.parse(bufferedLines.join("\n"))
yield jsonData
} catch (parseError) {
console.error("Error parsing final buffered content:", parseError)
}
}
}
/**
* Create a message using the Gemini CLI OAuth API
*/
@withRetry({
maxRetries: 2,
baseDelay: 2000,
maxDelay: 10000,
})
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
// Initialize auth if not already done
await this.initializeAuth()
// Discover project ID if needed
const projectId = await this.discoverProjectId()
// Convert messages to Gemini format
const contents = messages.map(convertAnthropicMessageToGemini)
// Get the selected model
const { id: modelId, info: modelInfo } = this.getModel()
// Build the request
const streamRequest = {
model: modelId,
project: projectId,
request: {
contents: [
{
role: "user",
parts: [{ text: systemPrompt }],
},
...contents,
],
generationConfig: {
temperature: 0.7,
maxOutputTokens: modelInfo.maxTokens || 8192,
},
},
}
let totalContent = ""
let promptTokens = 0
let outputTokens = 0
let lastUsageMetadata: any = null
try {
// Make the streaming request
const response = await this.authClient.request({
url: `${CODE_ASSIST_ENDPOINT}/${CODE_ASSIST_API_VERSION}:streamGenerateContent`,
method: "POST",
params: { alt: "sse" },
headers: {
"Content-Type": "application/json",
},
responseType: "stream",
body: JSON.stringify(streamRequest),
})
// Process the SSE stream
for await (const jsonData of this.parseSSEStream(response.data as Readable)) {
// Extract content from the response
const candidate = jsonData.response?.candidates?.[0]
if (candidate?.content?.parts?.[0]?.text) {
const content = candidate.content.parts[0].text
totalContent += content
// Yield text chunk
yield {
type: "text",
text: content,
}
}
// Store usage metadata for final reporting
if (jsonData.response?.usageMetadata) {
lastUsageMetadata = jsonData.response.usageMetadata
promptTokens = lastUsageMetadata.promptTokenCount || promptTokens
outputTokens = lastUsageMetadata.candidatesTokenCount || outputTokens
}
// Check if this is the final chunk
if (candidate?.finishReason) {
break
}
}
// Yield usage information
if (lastUsageMetadata) {
yield {
type: "usage",
inputTokens: promptTokens,
outputTokens: outputTokens,
totalCost: 0, // Free tier
}
}
} catch (error) {
// Handle rate limit errors similar to the Gemini provider
if (error instanceof Error) {
// Check for rate limit patterns in the error message
const rateLimitPatterns = [
/got status: 429/i,
/429 Too Many Requests/i,
/rate limit exceeded/i,
/too many requests/i,
/quota exceeded/i,
/resource exhausted/i,
/code 429/i,
]
const isRateLimit = rateLimitPatterns.some((pattern) => pattern.test(error.message))
if (isRateLimit) {
const rateLimitError = Object.assign(new Error(error.message), {
...error,
status: 429,
})
throw rateLimitError
}
}
// Re-throw the original error if it's not a rate limit error
throw error
}
}
/**
* Get the model ID and info
*/
getModel(): { id: GeminiCliModelId; info: ModelInfo } {
const modelId = this.options.apiModelId as GeminiCliModelId
if (modelId && modelId in geminiCliModels) {
return { id: modelId, info: geminiCliModels[modelId] }
}
return {
id: geminiCliDefaultModelId,
info: geminiCliModels[geminiCliDefaultModelId],
}
}
}
+56 -37
View File
@@ -12,8 +12,15 @@ import { telemetryService } from "@services/posthog/telemetry/TelemetryService"
// Define a default TTL for the cache (e.g., 15 minutes in seconds)
const DEFAULT_CACHE_TTL_SECONDS = 900
interface GeminiHandlerOptions extends ApiHandlerOptions {
interface GeminiHandlerOptions {
isVertex?: boolean
vertexProjectId?: string
vertexRegion?: string
geminiApiKey?: string
geminiBaseUrl?: string
thinkingBudgetTokens?: number
apiModelId?: string
taskId?: string
}
/**
@@ -37,31 +44,46 @@ interface GeminiHandlerOptions extends ApiHandlerOptions {
* 4. Separating immediate costs from ongoing costs to avoid double-counting
*/
export class GeminiHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: GoogleGenAI
private options: GeminiHandlerOptions
private client: GoogleGenAI | undefined
constructor(options: GeminiHandlerOptions) {
// Store the options
this.options = options
}
if (options.isVertex) {
// Initialize with Vertex AI configuration
const project = this.options.vertexProjectId ?? "not-provided"
const location = this.options.vertexRegion ?? "not-provided"
private ensureClient(): GoogleGenAI {
if (!this.client) {
const options = this.options as GeminiHandlerOptions
this.client = new GoogleGenAI({
vertexai: true,
project,
location,
})
} else {
// Initialize with standard API key
if (!options.geminiApiKey) {
throw new Error("API key is required for Google Gemini when not using Vertex AI")
if (options.isVertex) {
// Initialize with Vertex AI configuration
const project = this.options.vertexProjectId ?? "not-provided"
const location = this.options.vertexRegion ?? "not-provided"
try {
this.client = new GoogleGenAI({
vertexai: true,
project,
location,
})
} catch (error) {
throw new Error(`Error creating Gemini Vertex AI client: ${error.message}`)
}
} else {
// Initialize with standard API key
if (!options.geminiApiKey) {
throw new Error("API key is required for Google Gemini when not using Vertex AI")
}
try {
this.client = new GoogleGenAI({ apiKey: options.geminiApiKey })
} catch (error) {
throw new Error(`Error creating Gemini client: ${error.message}`)
}
}
this.client = new GoogleGenAI({ apiKey: options.geminiApiKey })
}
return this.client
}
/**
@@ -80,6 +102,7 @@ export class GeminiHandler implements ApiHandler {
maxDelay: 15000,
})
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const { id: modelId, info } = this.getModel()
const contents = messages.map(convertAnthropicMessageToGemini)
@@ -117,7 +140,7 @@ export class GeminiHandler implements ApiHandler {
let lastUsageMetadata: GenerateContentResponseUsageMetadata | undefined
try {
const result = await this.client.models.generateContentStream({
const result = await client.models.generateContentStream({
model: modelId,
contents: contents,
config: {
@@ -233,23 +256,18 @@ export class GeminiHandler implements ApiHandler {
totalDurationSdkMs > 0 && outputTokens > 0 ? outputTokens / (totalDurationSdkMs / 1000) : undefined
if (this.options.taskId) {
telemetryService.captureGeminiApiPerformance(
this.options.taskId,
modelId,
{
ttftSec: ttftSdkMs !== undefined ? ttftSdkMs / 1000 : undefined,
totalDurationSec: totalDurationSdkMs / 1000,
promptTokens,
outputTokens,
cacheReadTokens,
cacheHit,
cacheHitPercentage,
apiSuccess,
apiError,
throughputTokensPerSec: throughputTokensPerSecSdk,
},
true,
)
telemetryService.captureGeminiApiPerformance(this.options.taskId, modelId, {
ttftSec: ttftSdkMs !== undefined ? ttftSdkMs / 1000 : undefined,
totalDurationSec: totalDurationSdkMs / 1000,
promptTokens,
outputTokens,
cacheReadTokens,
cacheHit,
cacheHitPercentage,
apiSuccess,
apiError,
throughputTokensPerSec: throughputTokensPerSecSdk,
})
} else {
console.warn("GeminiHandler: taskId not available for telemetry in createMessage.")
}
@@ -351,6 +369,7 @@ export class GeminiHandler implements ApiHandler {
*/
async countTokens(content: Array<any>): Promise<number> {
try {
const client = this.ensureClient()
const { id: model } = this.getModel()
// Convert content to Gemini format
@@ -362,7 +381,7 @@ export class GeminiHandler implements ApiHandler {
})
// Use Gemini's token counting API
const response = await this.client.models.countTokens({
const response = await client.models.countTokens({
model,
contents: [{ parts: geminiContent }],
})
+290
View File
@@ -0,0 +1,290 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { withRetry } from "../retry"
import { ApiHandler } from "../"
import { GroqModelId, ModelInfo, groqDefaultModelId, groqModels } from "@shared/api"
import { calculateApiCostOpenAI } from "../../utils/cost"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
interface GroqHandlerOptions {
groqApiKey?: string
groqModelId?: string
groqModelInfo?: ModelInfo
apiModelId?: string // For backward compatibility
}
// Model family definitions for enhanced behavior
interface GroqModelFamily {
name: string
supportedFeatures: {
streaming: boolean
temperature: boolean
vision: boolean
tools: boolean
}
maxTokensOverride?: number
specialParams?: Record<string, any>
}
const MODEL_FAMILIES: Record<string, GroqModelFamily> = {
// Moonshort 4 Family - Latest generation with vision support
"kimi-k2": {
name: "kimi-k2",
supportedFeatures: { streaming: true, temperature: true, vision: true, tools: true },
maxTokensOverride: 8192,
},
// Llama 4 Family - Latest generation with vision support
llama4: {
name: "Llama 4",
supportedFeatures: { streaming: true, temperature: true, vision: true, tools: true },
maxTokensOverride: 8192,
},
// Llama 3.3 Family - Balanced performance
"llama3.3": {
name: "Llama 3.3",
supportedFeatures: { streaming: true, temperature: true, vision: false, tools: true },
maxTokensOverride: 32768,
},
// Llama 3.1 Family - Fast inference
"llama3.1": {
name: "Llama 3.1",
supportedFeatures: { streaming: true, temperature: true, vision: false, tools: true },
maxTokensOverride: 131072,
},
// DeepSeek Family - Reasoning-optimized
deepseek: {
name: "DeepSeek",
supportedFeatures: { streaming: true, temperature: true, vision: false, tools: true },
maxTokensOverride: 8192,
specialParams: {
top_p: 0.95,
reasoning_format: "parsed",
},
},
// Qwen Family - Enhanced for Q&A
qwen: {
name: "Qwen",
supportedFeatures: { streaming: true, temperature: true, vision: false, tools: true },
maxTokensOverride: 32768,
},
// Compound Models - Hybrid architectures
compound: {
name: "Compound",
supportedFeatures: { streaming: true, temperature: true, vision: false, tools: true },
maxTokensOverride: 8192,
},
}
export class GroqHandler implements ApiHandler {
private options: GroqHandlerOptions
private client: OpenAI | undefined
constructor(options: GroqHandlerOptions) {
this.options = options
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.groqApiKey) {
throw new Error("Groq API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://api.groq.com/openai/v1",
apiKey: this.options.groqApiKey,
})
} catch (error) {
throw new Error(`Error creating Groq client: ${error.message}`)
}
}
return this.client
}
private async *yieldUsage(info: ModelInfo, usage: OpenAI.Completions.CompletionUsage | undefined): ApiStream {
const inputTokens = usage?.prompt_tokens || 0
const outputTokens = usage?.completion_tokens || 0
const totalCost = calculateApiCostOpenAI(info, inputTokens, outputTokens)
yield {
type: "usage",
inputTokens,
outputTokens,
cacheWriteTokens: 0,
cacheReadTokens: 0,
totalCost,
}
}
/**
* Detects the model family based on the model ID
*/
private detectModelFamily(modelId: string): GroqModelFamily {
if (modelId.includes("kimi-k2")) {
return MODEL_FAMILIES["kimi-k2"]
}
// Llama 4 variants
if (modelId.includes("llama-4") || modelId.includes("llama/llama-4")) {
return MODEL_FAMILIES.llama4
}
// Llama 3.3 variants
if (modelId.includes("llama-3.3")) {
return MODEL_FAMILIES["llama3.3"]
}
// Llama 3.1 variants
if (modelId.includes("llama-3.1")) {
return MODEL_FAMILIES["llama3.1"]
}
// DeepSeek variants
if (modelId.includes("deepseek")) {
return MODEL_FAMILIES.deepseek
}
// Qwen variants
if (modelId.includes("qwen")) {
return MODEL_FAMILIES.qwen
}
// Compound variants
if (modelId.includes("compound")) {
return MODEL_FAMILIES.compound
}
// Default fallback to Llama 3.3 behavior
return MODEL_FAMILIES["kimi-k2"]
}
/**
* Gets the optimal max_tokens based on model family and capabilities
*/
private getOptimalMaxTokens(model: { id: string; info: ModelInfo }, modelFamily: GroqModelFamily): number {
// Use model-specific max tokens if available
if (model.info.maxTokens && model.info.maxTokens > 0) {
return model.info.maxTokens
}
// Use family override if available
if (modelFamily.maxTokensOverride) {
return modelFamily.maxTokensOverride
}
// Default fallback
return 8192
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
const modelFamily = this.detectModelFamily(model.id)
// Optimize parameters based on model family
const temperature = 0
const maxTokens = this.getOptimalMaxTokens(model, modelFamily)
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
// Build request parameters with model-specific optimizations
const requestParams: OpenAI.Chat.ChatCompletionCreateParamsStreaming & {
reasoning_format?: "parsed" | "raw" | "hidden"
top_p?: number
} = {
model: model.id,
max_tokens: maxTokens,
messages: openAiMessages,
stream: true,
stream_options: { include_usage: true },
temperature,
}
// Add any special parameters for specific model families
if (modelFamily.specialParams) {
Object.assign(requestParams, modelFamily.specialParams)
}
const stream = await client.chat.completions.create(requestParams)
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
// Handle reasoning field if present (for reasoning models with parsed output)
if ((delta as any)?.reasoning) {
const reasoningContent = (delta as any).reasoning as string
yield {
type: "reasoning",
reasoning: reasoningContent,
}
continue
}
// Handle content field - trust the parsed output from Groq
if (delta?.content) {
yield {
type: "text",
text: delta.content,
}
}
// Handle usage information
if (chunk.usage) {
yield* this.yieldUsage(model.info, chunk.usage)
}
}
}
/**
* Checks if the current model supports vision/images
*/
supportsImages(): boolean {
const model = this.getModel()
return model.info.supportsImages === true
}
/**
* Checks if the current model supports tools
*/
supportsTools(): boolean {
const model = this.getModel()
const modelFamily = this.detectModelFamily(model.id)
return modelFamily.supportedFeatures.tools
}
/**
* Gets model information with enhanced family detection
*/
getModel(): { id: string; info: ModelInfo } {
// First priority: groqModelId and groqModelInfo (like Requesty does)
const groqModelId = this.options.groqModelId
const groqModelInfo = this.options.groqModelInfo
if (groqModelId && groqModelInfo) {
return { id: groqModelId, info: groqModelInfo }
}
// Second priority: groqModelId with static model info
if (groqModelId && groqModelId in groqModels) {
const id = groqModelId as GroqModelId
return { id, info: groqModels[id] }
}
// Third priority: apiModelId (for backward compatibility)
const apiModelId = this.options.apiModelId
if (apiModelId && apiModelId in groqModels) {
const id = apiModelId as GroqModelId
return { id, info: groqModels[id] }
}
// Default fallback
return {
id: groqDefaultModelId,
info: groqModels[groqDefaultModelId],
}
}
/**
* Gets model family information for debugging/introspection
*/
getModelFamily(): GroqModelFamily {
const model = this.getModel()
return this.detectModelFamily(model.id)
}
}
+142
View File
@@ -0,0 +1,142 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { withRetry } from "../retry"
import { ApiHandler } from "../"
import { ApiHandlerOptions, HuggingFaceModelId, ModelInfo, huggingFaceDefaultModelId, huggingFaceModels } from "@shared/api"
import { calculateApiCostOpenAI } from "../../utils/cost"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
interface HuggingFaceHandlerOptions {
huggingFaceApiKey?: string
huggingFaceModelId?: string
huggingFaceModelInfo?: ModelInfo
}
export class HuggingFaceHandler implements ApiHandler {
private options: HuggingFaceHandlerOptions
private client: OpenAI | undefined
private cachedModel: { id: HuggingFaceModelId; info: ModelInfo } | undefined
constructor(options: HuggingFaceHandlerOptions) {
this.options = options
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.huggingFaceApiKey) {
throw new Error("Hugging Face API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://router.huggingface.co/v1",
apiKey: this.options.huggingFaceApiKey,
defaultHeaders: {
"User-Agent": "Cline/1.0",
},
})
} catch (error: any) {
throw new Error(`Error creating Hugging Face client: ${error.message}`)
}
}
return this.client
}
private async *yieldUsage(info: ModelInfo, usage: OpenAI.Completions.CompletionUsage | undefined): ApiStream {
if (!usage) {
return
}
const inputTokens = usage.prompt_tokens || 0
const outputTokens = usage.completion_tokens || 0
const totalCost = calculateApiCostOpenAI(info, inputTokens, outputTokens)
const usageData = {
type: "usage" as const,
inputTokens: inputTokens,
outputTokens: outputTokens,
cacheWriteTokens: 0,
cacheReadTokens: 0,
totalCost: totalCost,
}
yield usageData
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
try {
const client = this.ensureClient()
const model = this.getModel()
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
const requestParams = {
model: model.id,
max_tokens: model.info.maxTokens,
messages: openAiMessages,
stream: true,
stream_options: { include_usage: true },
temperature: 0,
}
const stream = (await client.chat.completions.create(requestParams)) as any
let chunkCount = 0
let totalContent = ""
for await (const chunk of stream) {
chunkCount++
const delta = chunk.choices[0]?.delta
if (delta?.content) {
totalContent += delta.content
yield {
type: "text",
text: delta.content,
}
}
if (chunk.usage) {
yield* this.yieldUsage(model.info, chunk.usage)
}
}
} catch (error: any) {
throw error
}
}
getModel(): { id: HuggingFaceModelId; info: ModelInfo } {
// Return cached model if available
if (this.cachedModel) {
return this.cachedModel
}
const modelId = this.options.huggingFaceModelId
// List all available models for debugging
const availableModels = Object.keys(huggingFaceModels)
let result: { id: HuggingFaceModelId; info: ModelInfo }
if (modelId && modelId in huggingFaceModels) {
const id = modelId as HuggingFaceModelId
const modelInfo = huggingFaceModels[id]
result = { id, info: modelInfo }
} else {
const defaultInfo = huggingFaceModels[huggingFaceDefaultModelId]
result = {
id: huggingFaceDefaultModelId,
info: defaultInfo,
}
}
// Cache the result for future calls
this.cachedModel = result
return result
}
}
+38 -19
View File
@@ -1,28 +1,52 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { ApiHandlerOptions, liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults } from "@shared/api"
import { liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults, LiteLLMModelInfo } from "@shared/api"
import { ApiHandler } from ".."
import { ApiStream } from "../transform/stream"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { withRetry } from "../retry"
export class LiteLlmHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
interface LiteLlmHandlerOptions {
liteLlmApiKey?: string
liteLlmBaseUrl?: string
liteLlmModelId?: string
liteLlmModelInfo?: LiteLLMModelInfo
thinkingBudgetTokens?: number
liteLlmUsePromptCache?: boolean
taskId?: string
}
constructor(options: ApiHandlerOptions) {
export class LiteLlmHandler implements ApiHandler {
private options: LiteLlmHandlerOptions
private client: OpenAI | undefined
constructor(options: LiteLlmHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: this.options.liteLlmBaseUrl || "http://localhost:4000",
apiKey: this.options.liteLlmApiKey || "noop",
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.liteLlmApiKey) {
throw new Error("LiteLLM API key is required")
}
try {
this.client = new OpenAI({
baseURL: this.options.liteLlmBaseUrl || "http://localhost:4000",
apiKey: this.options.liteLlmApiKey || "noop",
})
} catch (error) {
throw new Error(`Error creating LiteLLM client: ${error.message}`)
}
}
return this.client
}
async calculateCost(prompt_tokens: number, completion_tokens: number): Promise<number | undefined> {
// Reference: https://github.com/BerriAI/litellm/blob/122ee634f434014267af104814022af1d9a0882f/litellm/proxy/spend_tracking/spend_management_endpoints.py#L1473
const client = this.ensureClient()
const modelId = this.options.liteLlmModelId || liteLlmDefaultModelId
try {
const response = await fetch(`${this.client.baseURL}/spend/calculate`, {
const response = await fetch(`${client.baseURL}/spend/calculate`, {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -54,6 +78,7 @@ export class LiteLlmHandler implements ApiHandler {
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const formattedMessages = convertToOpenAiMessages(messages)
const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
role: "system",
@@ -101,21 +126,15 @@ export class LiteLlmHandler implements ApiHandler {
return message
})
const requestPayload: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming & {
metadata?: { cline_task_id: string }
} = {
const stream = await client.chat.completions.create({
model: this.options.liteLlmModelId || liteLlmDefaultModelId,
messages: [enhancedSystemMessage, ...enhancedMessages],
temperature,
stream: true,
stream_options: { include_usage: true },
...(thinkingConfig && { thinking: thinkingConfig }), // Add thinking configuration when applicable
...(this.options.taskId && {
metadata: { cline_task_id: this.options.taskId },
}),
}
const stream = await this.client.chat.completions.create(requestPayload)
...(this.options.taskId && { litellm_session_id: `cline-${this.options.taskId}` }), // Add session ID for LiteLLM tracking
})
const inputCost = (await this.calculateCost(1e6, 0)) || 0
const outputCost = (await this.calculateCost(0, 1e6)) || 0
+25 -9
View File
@@ -6,27 +6,43 @@ import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { withRetry } from "../retry"
export class LmStudioHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
interface LmStudioHandlerOptions {
lmStudioBaseUrl?: string
lmStudioModelId?: string
}
constructor(options: ApiHandlerOptions) {
export class LmStudioHandler implements ApiHandler {
private options: LmStudioHandlerOptions
private client: OpenAI | undefined
constructor(options: LmStudioHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: (this.options.lmStudioBaseUrl || "http://localhost:1234") + "/v1",
apiKey: "noop",
})
}
private ensureClient(): OpenAI {
if (!this.client) {
try {
this.client = new OpenAI({
baseURL: (this.options.lmStudioBaseUrl || "http://localhost:1234") + "/v1",
apiKey: "noop",
})
} catch (error) {
throw new Error(`Error creating LM Studio client: ${error.message}`)
}
}
return this.client
}
@withRetry({ retryAllErrors: true })
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
try {
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: this.getModel().id,
messages: openAiMessages,
stream: true,
+28 -9
View File
@@ -2,24 +2,43 @@ import { Anthropic } from "@anthropic-ai/sdk"
import { Mistral } from "@mistralai/mistralai"
import { withRetry } from "../retry"
import { ApiHandler } from "../"
import { ApiHandlerOptions, mistralDefaultModelId, MistralModelId, mistralModels, ModelInfo } from "@shared/api"
import { mistralDefaultModelId, MistralModelId, mistralModels, ModelInfo } from "@shared/api"
import { convertToMistralMessages } from "../transform/mistral-format"
import { ApiStream } from "../transform/stream"
export class MistralHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: Mistral
interface MistralHandlerOptions {
mistralApiKey?: string
apiModelId?: string
}
constructor(options: ApiHandlerOptions) {
export class MistralHandler implements ApiHandler {
private options: MistralHandlerOptions
private client: Mistral | undefined
constructor(options: MistralHandlerOptions) {
this.options = options
this.client = new Mistral({
apiKey: this.options.mistralApiKey,
})
}
private ensureClient(): Mistral {
if (!this.client) {
if (!this.options.mistralApiKey) {
throw new Error("Mistral API key is required")
}
try {
this.client = new Mistral({
apiKey: this.options.mistralApiKey,
})
} catch (error) {
throw new Error(`Error creating Mistral client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const stream = await this.client.chat
const client = this.ensureClient()
const stream = await client.chat
.stream({
model: this.getModel().id,
// max_completion_tokens: this.getModel().info.maxTokens,
+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] }
}
}
+26 -8
View File
@@ -5,27 +5,45 @@ import { ApiHandler } from "../index"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { convertToR1Format } from "../transform/r1-format"
import { nebiusDefaultModelId, nebiusModels, type ModelInfo, type ApiHandlerOptions, type NebiusModelId } from "../../shared/api"
import { nebiusDefaultModelId, nebiusModels, type ModelInfo, type NebiusModelId } from "../../shared/api"
interface NebiusHandlerOptions {
nebiusApiKey?: string
apiModelId?: string
}
export class NebiusHandler implements ApiHandler {
private client: OpenAI
private client: OpenAI | undefined
constructor(private readonly options: ApiHandlerOptions) {
this.client = new OpenAI({
baseURL: "https://api.studio.nebius.ai/v1",
apiKey: this.options.nebiusApiKey,
})
constructor(private readonly options: NebiusHandlerOptions) {}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.nebiusApiKey) {
throw new Error("Nebius API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://api.studio.nebius.ai/v1",
apiKey: this.options.nebiusApiKey,
})
} catch (error) {
throw new Error(`Error creating Nebius client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = model.id.includes("DeepSeek-R1")
? convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)]
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: model.id,
messages: openAiMessages,
temperature: 0,
+24 -6
View File
@@ -6,17 +6,35 @@ import { convertToOllamaMessages } from "../transform/ollama-format"
import { ApiStream } from "../transform/stream"
import { withRetry } from "../retry"
export class OllamaHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: Ollama
interface OllamaHandlerOptions {
ollamaBaseUrl?: string
ollamaModelId?: string
ollamaApiOptionsCtxNum?: string
requestTimeoutMs?: number
}
constructor(options: ApiHandlerOptions) {
export class OllamaHandler implements ApiHandler {
private options: OllamaHandlerOptions
private client: Ollama | undefined
constructor(options: OllamaHandlerOptions) {
this.options = options
this.client = new Ollama({ host: this.options.ollamaBaseUrl || "http://localhost:11434" })
}
private ensureClient(): Ollama {
if (!this.client) {
try {
this.client = new Ollama({ host: this.options.ollamaBaseUrl || "http://localhost:11434" })
} catch (error) {
throw new Error(`Error creating Ollama client: ${error.message}`)
}
}
return this.client
}
@withRetry({ retryAllErrors: true })
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const ollamaMessages: Message[] = [{ role: "system", content: systemPrompt }, ...convertToOllamaMessages(messages)]
try {
@@ -27,7 +45,7 @@ export class OllamaHandler implements ApiHandler {
})
// Create the actual API request promise
const apiPromise = this.client.chat({
const apiPromise = client.chat({
model: this.getModel().id,
messages: ollamaMessages,
stream: true,
+30 -10
View File
@@ -8,15 +8,34 @@ import { calculateApiCostOpenAI } from "../../utils/cost"
import { ApiStream } from "../transform/stream"
import type { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
export class OpenAiNativeHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
interface OpenAiNativeHandlerOptions {
openAiNativeApiKey?: string
reasoningEffort?: string
apiModelId?: string
}
constructor(options: ApiHandlerOptions) {
export class OpenAiNativeHandler implements ApiHandler {
private options: OpenAiNativeHandlerOptions
private client: OpenAI | undefined
constructor(options: OpenAiNativeHandlerOptions) {
this.options = options
this.client = new OpenAI({
apiKey: this.options.openAiNativeApiKey,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.openAiNativeApiKey) {
throw new Error("OpenAI API key is required")
}
try {
this.client = new OpenAI({
apiKey: this.options.openAiNativeApiKey,
})
} catch (error: any) {
throw new Error(`Error creating OpenAI client: ${error.message}`)
}
}
return this.client
}
private async *yieldUsage(info: ModelInfo, usage: OpenAI.Completions.CompletionUsage | undefined): ApiStream {
@@ -38,6 +57,7 @@ export class OpenAiNativeHandler implements ApiHandler {
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
switch (model.id) {
@@ -45,7 +65,7 @@ export class OpenAiNativeHandler implements ApiHandler {
case "o1-preview":
case "o1-mini": {
// o1 doesn't support streaming, non-1 temp, or system prompt
const response = await this.client.chat.completions.create({
const response = await client.chat.completions.create({
model: model.id,
messages: [{ role: "user", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
})
@@ -61,7 +81,7 @@ export class OpenAiNativeHandler implements ApiHandler {
case "o4-mini":
case "o3":
case "o3-mini": {
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: model.id,
messages: [{ role: "developer", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
stream: true,
@@ -85,7 +105,7 @@ export class OpenAiNativeHandler implements ApiHandler {
break
}
default: {
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: model.id,
// max_completion_tokens: this.getModel().info.maxTokens,
temperature: 0,
+50 -26
View File
@@ -1,44 +1,68 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI, { AzureOpenAI } from "openai"
import { withRetry } from "../retry"
import { ApiHandlerOptions, azureOpenAiDefaultApiVersion, ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
import { azureOpenAiDefaultApiVersion, ModelInfo, openAiModelInfoSaneDefaults, OpenAiCompatibleModelInfo } from "@shared/api"
import { ApiHandler } from "../index"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { convertToR1Format } from "../transform/r1-format"
import type { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
export class OpenAiHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
interface OpenAiHandlerOptions {
openAiApiKey?: string
openAiBaseUrl?: string
azureApiVersion?: string
openAiHeaders?: Record<string, string>
openAiModelId?: string
openAiModelInfo?: OpenAiCompatibleModelInfo
reasoningEffort?: string
}
constructor(options: ApiHandlerOptions) {
export class OpenAiHandler implements ApiHandler {
private options: OpenAiHandlerOptions
private client: OpenAI | undefined
constructor(options: OpenAiHandlerOptions) {
this.options = options
// Azure API shape slightly differs from the core API shape: https://github.com/openai/openai-node?tab=readme-ov-file#microsoft-azure-openai
// Use azureApiVersion to determine if this is an Azure endpoint, since the URL may not always contain 'azure.com'
if (
this.options.azureApiVersion ||
((this.options.openAiBaseUrl?.toLowerCase().includes("azure.com") ||
this.options.openAiBaseUrl?.toLowerCase().includes("azure.us")) &&
!this.options.openAiModelId?.toLowerCase().includes("deepseek"))
) {
this.client = new AzureOpenAI({
baseURL: this.options.openAiBaseUrl,
apiKey: this.options.openAiApiKey,
apiVersion: this.options.azureApiVersion || azureOpenAiDefaultApiVersion,
defaultHeaders: this.options.openAiHeaders,
})
} else {
this.client = new OpenAI({
baseURL: this.options.openAiBaseUrl,
apiKey: this.options.openAiApiKey,
defaultHeaders: this.options.openAiHeaders,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.openAiApiKey) {
throw new Error("OpenAI API key is required")
}
try {
// Azure API shape slightly differs from the core API shape: https://github.com/openai/openai-node?tab=readme-ov-file#microsoft-azure-openai
// Use azureApiVersion to determine if this is an Azure endpoint, since the URL may not always contain 'azure.com'
if (
this.options.azureApiVersion ||
((this.options.openAiBaseUrl?.toLowerCase().includes("azure.com") ||
this.options.openAiBaseUrl?.toLowerCase().includes("azure.us")) &&
!this.options.openAiModelId?.toLowerCase().includes("deepseek"))
) {
this.client = new AzureOpenAI({
baseURL: this.options.openAiBaseUrl,
apiKey: this.options.openAiApiKey,
apiVersion: this.options.azureApiVersion || azureOpenAiDefaultApiVersion,
defaultHeaders: this.options.openAiHeaders,
})
} else {
this.client = new OpenAI({
baseURL: this.options.openAiBaseUrl,
apiKey: this.options.openAiApiKey,
defaultHeaders: this.options.openAiHeaders,
})
}
} catch (error: any) {
throw new Error(`Error creating OpenAI client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const modelId = this.options.openAiModelId ?? ""
const isDeepseekReasoner = modelId.includes("deepseek-reasoner")
const isR1FormatRequired = this.options.openAiModelInfo?.isR1FormatRequired ?? false
@@ -68,7 +92,7 @@ export class OpenAiHandler implements ApiHandler {
reasoningEffort = (this.options.reasoningEffort as ChatCompletionReasoningEffort) || "medium"
}
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: modelId,
messages: openAiMessages,
temperature,
+48 -17
View File
@@ -3,35 +3,68 @@ import axios from "axios"
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
import OpenAI from "openai"
import { ApiHandler } from "../"
import { ApiHandlerOptions, ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "@shared/api"
import { ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "@shared/api"
import { withRetry } from "../retry"
import { createOpenRouterStream } from "../transform/openrouter-stream"
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
import { OpenRouterErrorResponse } from "./types"
import { shouldSkipReasoningForModel } from "@utils/model-utils"
interface OpenRouterHandlerOptions {
openRouterApiKey?: string
openRouterModelId?: string
openRouterModelInfo?: ModelInfo
openRouterProviderSorting?: string
reasoningEffort?: string
thinkingBudgetTokens?: number
}
interface OpenRouterHandlerOptions {
openRouterApiKey?: string
openRouterModelId?: string
openRouterModelInfo?: ModelInfo
openRouterProviderSorting?: string
reasoningEffort?: string
thinkingBudgetTokens?: number
}
export class OpenRouterHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
private options: OpenRouterHandlerOptions
private client: OpenAI | undefined
lastGenerationId?: string
constructor(options: ApiHandlerOptions) {
constructor(options: OpenRouterHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://openrouter.ai/api/v1",
apiKey: this.options.openRouterApiKey,
defaultHeaders: {
"HTTP-Referer": "https://cline.bot", // Optional, for including your app on openrouter.ai rankings.
"X-Title": "Cline", // Optional. Shows in rankings on openrouter.ai.
},
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.openRouterApiKey) {
throw new Error("OpenRouter API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://openrouter.ai/api/v1",
apiKey: this.options.openRouterApiKey,
defaultHeaders: {
"HTTP-Referer": "https://cline.bot", // Optional, for including your app on openrouter.ai rankings.
"X-Title": "Cline", // Optional. Shows in rankings on openrouter.ai.
},
})
} catch (error: any) {
throw new Error(`Error creating OpenRouter client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
this.lastGenerationId = undefined
const stream = await createOpenRouterStream(
this.client,
client,
systemPrompt,
messages,
this.getModel(),
@@ -89,7 +122,8 @@ export class OpenRouterHandler implements ApiHandler {
}
// Reasoning tokens are returned separately from the content
if ("reasoning" in delta && delta.reasoning) {
// Skip reasoning content for Grok 4 models since it only displays "thinking" without providing useful information
if ("reasoning" in delta && delta.reasoning && !shouldSkipReasoningForModel(this.options.openRouterModelId)) {
yield {
type: "reasoning",
// @ts-ignore-next-line
@@ -190,9 +224,6 @@ export class OpenRouterHandler implements ApiHandler {
getModel(): { id: string; info: ModelInfo } {
let modelId = this.options.openRouterModelId
if (modelId === "x-ai/grok-3") {
modelId = "x-ai/grok-3-beta"
}
const modelInfo = this.options.openRouterModelInfo
if (modelId && modelInfo) {
return { id: modelId, info: modelInfo }

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