Compare commits

..
Author SHA1 Message Date
arafatkatze 2a95dcd2b5 Support prompt caching and thinking for Opus 4.1 2025-08-05 22:07:21 -07:00
arafatkatze 1619b7c971 Support prompt caching and thinking for Opus 4.1 2025-08-05 22:00:51 -07:00
Jim Tang 29ae2c286d Update index.ts while tree possible as a null. (#5285) 2025-08-05 20:03:22 -07:00
github-actions[bot] 82aee44a9a v3.20.11 Release Notes (#5377) 2025-08-05 16:07:23 -07:00
omercelik 031604ddf6 feat: Added Claude Opus 4.1 to Bedrock (#5381)
* feat: Added Claude Opus 4.1 to Bedrock

* Create ninety-owls-develop.md
2025-08-05 15:03:15 -07:00
Bee f2101e375f fix: update Playwright config and teardown error handling (#5383)
- Remove teardown dependency on e2e tests to fix execution order
- Move server cleanup before file operations in teardown
- Add proper error handling and logging for cleanup operations
2025-08-05 14:22:19 -07:00
Tomás Barreiro 8a65f0c68b feat: Add Opus 4.1 to claude-code (#5382)
* Add opus-4-1 to claude-code

* Add changeset
2025-08-05 13:42:40 -07:00
Bee 32b8fa44cb refactor: Integrate Posthog into Feature Flags & Telemetry & Error Services (#5275)
* refactor: posthog services: feature flags + error + telemtry

- Convert PostHogClientProvider to singleton with lifecycle management
- Update ErrorServices to use PostHogClientProvider
- Update Telementry Service
- Update and enable Feature Flags service

* replace logger

* fix imports - part 1

* update distinct ID

* update

* update

* clean up

* revert autoformat

* fix merge conflicts

* clean up autoformat

* revert autoformatter

* clean up logs
2025-08-05 13:00:44 -07:00
Sarah Fortune 0d067f7470 Add getCallbackUri to the HostProvider (#5361) 2025-08-05 12:06:08 -07:00
Kevin Taylor de6166392c Update Cerebras gpt-oss-120b (#5376)
* Add Cerebras gpt-oss-120b

* Change completion tokens
2025-08-05 12:00:02 -07:00
Kevin Taylorandellipsis-dev[bot] 5f21a9162a Add Cerebras gpt-oss-120b (#5375)
* Add Cerebras gpt-oss-120b

* Update src/shared/api.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-08-05 11:27:11 -07:00
github-actions[bot] 4de991f1b0 v3.20.10 Release Notes (#5374) 2025-08-05 11:22:52 -07:00
pashpashpashandarafatkatze 6e5d4a3f9e openai model in hugging face correct maxtokens (#5371)
* openai model in hugging face correct maxtokens

* maxtokens

* maxtokens 131k i guess?

* maxtokens swap

* changeset

* Adding Gpt-oss through groq

---------

Co-authored-by: arafatkatze <arafat.da.khan@gmail.com>
2025-08-05 11:08:53 -07:00
95af95badf v3.20.9 Release Notes (#5354)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md and package.json for version 3.20.9 patch release

---------

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-08-05 10:12:09 -07:00
Ara 61dcbd390c Adding Anthropic opus 4.1 (#5369)
* Adding Anthropic opus 4.1

* Adding Anthropic opus 4.1
2025-08-05 09:59:15 -07:00
pashpashpash 6255ac0a51 added provider flag to diff edit cli (#5334)
* added provider flag to diff edit cli

* dashboard ux

* more dashboard improvmeents

* native handler instead of just openai
2025-08-05 09:21:12 -07:00
Sarah Fortune 616800fcb9 Refactoring: move vscode specific property out of the WebviewProvider into the VscodeViewProvider (#5364) 2025-08-05 06:48:12 +01:00
Sarah Fortune df3826a59f In the webview grpc client, JSON encode/decode the messages when not running in Vscode (#5362) 2025-08-04 22:39:32 -07:00
Sarah Fortune 873917810d Remove left code from ProtoBus migration (#5363)
There is one place in the McpHub that sends messages mcp notification messages directly to the webview (not using the ProtoBus).

There is nowhere in the webview that is listening for this message, so this code is not doing anything.
2025-08-04 22:39:20 -07:00
e3c966f4e9 Baseten provider (#5238)
* kimi working

* fixed description rendering

* nit

* changeset

* revert openai version in package.json

* revert package-lock.json

* added space back in

* maintained previous protofield map order

* fixed import error due to location change from main

* updated Mode import for BasetenModelPicker

* revert readme since baseten is openai compatible

* refactored extensionStateContext

* added didOutputUsage flag

* fixed frontend loading

* no support for images on llama

* shifted VSCode Option order

* deleted typo

---------

Co-authored-by: Alex Ker <alexker@mac.mynetworksettings.com>
Co-authored-by: Alex Ker <alexker@Alexs-MacBook-Pro.local>
2025-08-04 20:07:59 -07:00
Sarah Fortune 7eeb43ab41 Simplify the GrpcHandler and add tests (#5356)
* Simplify the GrpcHandler

* Use two functions handleUnaryRequest and handleStreamingRequest, instead of creating a GrpcHandler object and calling class methods on it.
* Remove redundant try/catch and empty finally blocks. Each of the two handler functions has it's own try/catch.
* Each of the two functions is responsible for posting the result to the webview- Instead of unary and streaming responses being handled at different levels.
* Use the GrpcRequest and GrpcCancel types.

* Update comment
2025-08-04 19:15:29 -07:00
Bee 7620f177ac fix: clear streamingFailedMessage when user manually retries (#5222)
* fix: clear streamingFailedMessage when user manually retries

- Clear streamingFailedMessage when user manually retries
- Convert imports to type-only where appropriate
- Reorder imports for better organization
- Add explicit type annotations for better type safety
- Move node:timers/promises import to top

* add changeset

* merge main and reset fail flag

* revert autoformat
2025-08-04 17:04:42 -07:00
Bee 67bab94911 Revert "Add getCallUri to the HostProvider (#5322)" (#5359)
This reverts commit b8227c19c3.
2025-08-04 16:30:04 -07:00
Bee eb91bfd738 Update ChatView footer background to use sidebar theme (#5357)
Change footer background from editor to sidebar background color
and remove border styling.
2025-08-04 16:06:02 -07:00
Sarah Fortune b8227c19c3 Add getCallUri to the HostProvider (#5322)
**Centralize callback URI management** through the HostProvider instead of having it in multiple places in the codebase.

**Simplify error handling** by making the callback URI required rather than optional

The changes are related to **authentication callback URI handling** in the Cline extension. Here's what's being modified:

  - Simplified callback URI retrieval
- Changed return type from `Promise<string | undefined>` to `Promise<string>`
- Now throws an error if AuthHandler is not enabled instead of returning undefined

- Added a new `getCallbackUri` property that returns a `Promise<string>`
- This allows the host provider to supply callback URIs for authentication

  - Implemented callback URI provider

  - Updated to use HostProvider for callback URI

  - Updated to match new signature
2025-08-05 00:03:07 +01:00
Sarah Fortune 8fee09f09e Add logging to the webview ProtoBus client if it recieves a badly formed message (#5353) 2025-08-04 22:34:08 +01:00
Bee 1c026c26d2 fix: chatbox position styling (#5352)
* fix: chatbox position styling

* add changeset
2025-08-04 14:18:53 -07:00
Sarah Fortune 5bc4e5a4a0 Add comments for HostBridge RPC showSaveDialog (#5351) 2025-08-04 21:15:45 +01:00
Ara 2cfce5734e Change Vscode LM token counts to use approx counting method (#5280) 2025-08-04 12:41:34 -07:00
Bee e2045bf5c3 fix: flaky check for editor search bar (#5347)
* fix: flaky check for editor search bar

* remove disabling notification
2025-08-04 12:37:33 -07:00
Sarah Fortune 0d933e804f Support mentions for filenames with spaces (#5309)
* feat: support file mentions with spaces using quoted syntax

This change allows users to reference files with spaces in their names, which was previously impossible due to the space-delimited mention syntax.
File names with spaces can be @ mentioned by quoting the file name, e.g. @"/path with spaces/file.txt".

- Update mention regex in `src/shared/context-mentions.ts` to accept quoted file paths
  - Add support for quoted file paths that can contain spaces.
  - Allow multiple trailing punctuation chars; previously only a single limited punctuation characters were allowed.
  - Maintain support for unquoted paths, URLs, git hashes, and special keywords

- Update `src/core/mentions/index.ts` to handle quoted file names in mention parsing
  - Process quoted file paths by removing quotes when accessing the file system
  - Preserve existing functionality for all other mention types

- Update `webview-ui/src/utils/context-mentions.ts` to auto-quote file names with spaces
  - `insertMention()` and `insertMentionDirectly()` now wrap file paths containing spaces in quotes
  - Non-file mentions (URLs, keywords) remain unquoted

- Add comprehensive unit tests:
  - New test file `src/core/mentions/__tests__/index.test.ts` covering all mention types
  - New test file `webview-ui/src/utils/__tests__/context-mentions.test.ts` for webview mention insertion
  - Expanded `src/shared/__tests__/context-mentions.test.ts` to cover quoted paths and edge cases

* Use const instead of var
2025-08-04 19:42:44 +01:00
Sarah Fortune 16f73532f4 Remove things that were left over from the ProtoBus migration. (#5321) 2025-08-04 18:04:54 +01:00
Sarah Fortune 88bea8eeb4 Update saveOpenDocumentIfDirty to return if the doc was saved or not (#5343) 2025-08-04 18:04:36 +01:00
Sarah Fortune 1a570e98ba Update the GitHub test action to produce more readable output (#5333) 2025-08-04 18:04:21 +01:00
Toshii d86b7dd036 Add optional way to enforce no file edits in plan mode (#5299)
* base implementation

* base messaging implementation & ui

* update state
2025-08-04 09:55:59 -07:00
Sarah Fortune 0178c3fa90 Fix flakey test getOpenTabs and re-enable unit tests (#5332)
- Replace fixed 100ms timeout with pWaitFor polling mechanism
- Set 2-second timeout with 50ms polling interval
- Test now waits exactly as long as needed for tabs to be created
2025-08-03 20:51:05 -07:00
a107f45c6a v3.20.8 Release Notes (#5330)
* 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-08-03 17:17:54 -07:00
Saoud Rizwan 3dd2ed9161 Add comment about testing fix (#5329)
* Add comment about testing fix

* Create cool-cherries-brush.md
2025-08-03 17:15:04 -07:00
Saoud Rizwan 9a6603fdfb Disable unit tests in publish pipeline (#5327) 2025-08-03 17:09:59 -07:00
Sarah Fortune 6d5c3e6aa4 Switch remaining uses of vscode.window.show*Message to the HostBridge (#5324)
* Move remaining uses of vscode.window.show*Message to the HostBridge

Switch over the remaining uses.

Turn on the linter check to prevent these APIs being reintroduced later.

Exclude test files from the linter check.

* Update unit test
2025-08-04 01:03:43 +01:00
Sarah Fortune 24b9e821bb refactor: update navbar styling and remove shadow (#5323)
- Replace database icon with MCP server icon (codicon-server)
- Remove shadow-sm class for a flatter appearance
- Maintain consistent button styling with VSCodeButton components
- Add tooltips using HeroTooltip
2025-08-04 00:59:04 +01:00
9960a3c57c v3.20.7 Release Notes (#5328)
* 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-08-03 16:57:50 -07:00
Sarah FortuneandSaoud Rizwan 88947592f0 Fix errors in tests (#5294)
* Fix errors in tests:

```
[TerminalProcess] Terminal ID: Cline
Error capturing terminal output: Error: Failed to read from clipboard: HostProvider not setup. Call HostProvider.initialize() first.
  at readTextFromClipboard (/Users/sjf/cline/out/src/utils/env.js:39:15)
  at getLatestTerminalOutput (/Users/sjf/cline/out/src/integrations/terminal/get-latest-output.js:35:69)
  at TerminalProcess.emitCurrentTerminalContents (/Users/sjf/cline/out/src/integrations/terminal/TerminalProcess.js:26:92)
  at TerminalProcess.runWithoutShellIntegration (/Users/sjf/cline/out/src/integrations/terminal/TerminalProcess.js:366:20)
  at async TerminalProcess.run (/Users/sjf/cline/out/src/integrations/terminal/TerminalProcess.js:47:13)
      ✔ should execute a command that lists files
[TerminalProcess] Starting command: "sleep 0.5 && echo 'Done sleeping'"
[TerminalProcess] Shell integration available: false
[TerminalProcess] Terminal ID: Cline
Error capturing terminal output: Error: Failed to read from clipboard: HostProvider not setup. Call HostProvider.initialize() first.
  at readTextFromClipboard (/Users/sjf/cline/out/src/utils/env.js:39:15)
  at getLatestTerminalOutput (/Users/sjf/cline/out/src/integrations/terminal/get-latest-output.js:35:69)
  at TerminalProcess.emitCurrentTerminalContents (/Users/sjf/cline/out/src/integrations/terminal/TerminalProcess.js:26:92)
  at TerminalProcess.runWithoutShellIntegration (/Users/sjf/cline/out/src/integrations/terminal/TerminalProcess.js:366:20)
  at async TerminalProcess.run (/Users/sjf/cline/out/src/integrations/terminal/TerminalProcess.js:47:13)
  at async Context.<anonymous> (/Users/sjf/cline/out/src/integrations/terminal/TerminalProcess.test.js:119:13)
FakeTimers: clearTimeout was invoked to clear a native timer instead of one created by this library.
To automatically clean-up native timers, use `shouldClearNativeTimers`.
      ✔ should handle a longer running command (3007ms)
[TerminalProcess] Starting command: "echo 'Line 1' 'Line 2'"
[TerminalProcess] Shell integration available: false
[TerminalProcess] Terminal ID: Cline
Error capturing terminal output: Error: Failed to read from clipboard: HostProvider not setup. Call HostProvider.initialize() first.
  at readTextFromClipboard (/Users/sjf/cline/out/src/utils/env.js:39:15)
  at getLatestTerminalOutput (/Users/sjf/cline/out/src/integrations/terminal/get-latest-output.js:35:69)
  at TerminalProcess.emitCurrentTerminalContents (/Users/sjf/cline/out/src/integrations/terminal/TerminalProcess.js:26:92)
  at TerminalProcess.runWithoutShellIntegration (/Users/sjf/cline/out/src/integrations/terminal/TerminalProcess.js:366:20)
  at async TerminalProcess.run (/Users/sjf/cline/out/src/integrations/terminal/TerminalProcess.js:47:13)
      ✔ should execute a command with arguments
[TerminalProcess] Starting command: "echo "Line 1" && echo 'Line 2'"
[TerminalProcess] Shell integration available: false
[TerminalProcess] Terminal ID: Cline
Error capturing terminal output: Error: Failed to read from clipboard: HostProvider not setup. Call HostProvider.initialize() first.
  at readTextFromClipboard (/Users/sjf/cline/out/src/utils/env.js:39:15)
  at getLatestTerminalOutput (/Users/sjf/cline/out/src/integrations/terminal/get-latest-output.js:35:69)
  at TerminalProcess.emitCurrentTerminalContents (/Users/sjf/cline/out/src/integrations/terminal/TerminalProcess.js:26:92)
  at TerminalProcess.runWithoutShellIntegration (/Users/sjf/cline/out/src/integrations/terminal/TerminalProcess.js:366:20)
  at async TerminalProcess.run (/Users/sjf/cline/out/src/integrations/terminal/TerminalProcess.js:47:13)
      ✔ should execute a command with quotes
  ```

* Create brown-papayas-protect.md

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-08-03 16:55:21 -07:00
yuvalman ef4d11df19 fix: circular dependency that affect the github workflow Tests / test (pull_request) (#5317)
* fix: circular dependency that affect test env

* fix: circular dependency that affect test env
2025-08-04 00:38:41 +01:00
23dec509bc v3.20.6 Release Notes (#5326)
* 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-08-03 16:24:15 -07:00
celestial-vault 07ab6b19b8 check auth after initialize cacheservice (#5325)
* initialize cachService in controller constructor; remove authService as a class level variable on Controller

* changeset
2025-08-03 16:19:40 -07:00
Bee 0ddef94d1f feat: use auth callback handling with custom AuthHandler (#5223)
* feat: use auth callback handling with custom AuthHandler

- Add AuthHandler class to manage OAuth flow with local HTTP server
- Move callback logic from extension.ts to SharingUriHandler, making that shared between the original and new authentication ways
- Enabling Custom HTTP for "core only" environments
- Async starting and stopping HTTP server
2025-08-02 15:10:16 -07:00
BeeandCopilot b9ae83b1cd fix: standalone navbar style with chat layout refactor (#5308)
* fix: standalone navbar style with chat layout refactor

* Update webview-ui/src/components/chat/chat-view/components/layout/MessagesArea.tsx

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-02 14:17:38 -07:00
Sarah Fortuneandellipsis-dev[bot] e4eaf34827 test: Fix and re-enable unit tests (#5298)
* test: Fix and re-enable unit tests

Re-enable unit tests in CI workflow that were previously disabled

The cline-api test requires VSCode SDK which cannot be easily mocked in unit tests,
so it has been moved to integration tests where the full VSCode environment is available.

The @google/genai module is ES6-only which causes issues when running integration tests
compiled to CommonJS. A mock implementation has been added and the module resolution
is intercepted in test-setup.js to use the mock instead.

The bedrock unit tests for getModelId() functionality are removed as they were failing
and fixing them is out of scope for this PR.

- Move cline-api.test.ts from exports to test directory as it depends on VSCode SDK
- Add gemini-mock.test.ts to mock @google/genai ES6 module for CommonJS compatibility
- Add module interception in test-setup.js to redirect @google/genai to mock
- Remove failing bedrock unit tests introduced in PR #4209 (out of scope)

* Update src/api/providers/__tests__/bedrock.test.ts

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

* Formatting

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-08-02 01:47:27 +01:00
6d3ed43c74 v3.20.5 Release Notes (#5297)
* changeset version bump

* Updating CHANGELOG.md format

* Update CHANGELOG.md for version 3.20.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: pashpashpash <nik@cline.bot>
2025-08-01 16:50:04 -07:00
celestial-vault cbb67b48f2 fix secrets persistence (#5296) 2025-08-01 16:41:14 -07:00
Sarah Fortune a5f6a97be8 Fix eslint unit tests (#5295) 2025-08-01 23:16:18 +01:00
github-actions[bot] f309b062e7 v3.20.4 Release Notes
v3.20.4 Release Notes
2025-08-01 13:20:27 -07:00
canvrnoandKevin Bond 768df130ab Fix for delete task popup (#5260)
Co-authored-by: Kevin Bond <kevin@Kevins-MacBook-Pro.local>
2025-08-01 13:05:21 -07:00
Toshii 9980cb0938 fix grok browser_user (#5278) 2025-08-01 10:14:19 -07:00
AraandKevin Taylor aca4f842fa Update Cerebras models (#5282)
* Update Cerebras models

* Add changeset

* Modify completion token limits

* Split qwen3 coder into free/paid

* Change -paid to base model name

* Update Cerebras models

* Update Cerebras models

* Update Cerebras models

* Update api.ts

* Update Cerebras models

---------

Co-authored-by: Kevin Taylor <kevin.taylor@cerebras.net>
2025-08-01 01:11:53 -07:00
Bee 3fc91e2afe fix: E2E test stability by reordering sidebar and notification setup (#5279)
* fix: E2E test stability by reordering sidebar and notification setup

- Extract editor menu locator to variable for better readability
- Move sidebar opening to page fixture to ensure it's available earlier
- Wait for chat input visibility before disabling notifications
- Prevents race conditions in test initialization

* fix
2025-07-31 18:03:14 -07:00
celestial-vault 5f4700ce95 Move apiconfiguration to cache layer (#5210)
* remove chatSettings object

* use cache for apiCongfiguration state

* add state persistence debounced, batch state updates, make setters synchronous

* fix types after merge conflicts

* fix global state reset

* remove clearCache; make dispose function private; remove vscode api dependency; call reInitialize in reset functions instead of dispose/initialize
2025-07-31 16:58:47 -07:00
Jim Tang dbaf5e3ee3 Update system.ts for formating the code. (#5270) 2025-07-31 16:19:01 -07:00
Toshii 576176c24f add grok4 to advanced list (#5276) 2025-07-31 14:54:45 -07:00
Akshay Raj Gollahalli c8abcbfdf9 Do not ignore pkg folder (#4483) (#4505) 2025-07-31 12:11:31 -07:00
celestial-vault 8e984f2d98 clean up getStateToPostToWebview in preparation for migration to StateManager service (#5266) 2025-07-31 11:47:06 -07:00
81564faa4e v3.20.3 Release Notes (#5185)
* changeset version bump

* Updating CHANGELOG.md format

* releaseee

---------

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-30 21:49:57 -07:00
pashpashpash f8b5f1fd72 adding redirectUrl to credits purchasing experience (#5158) 2025-07-30 17:43:04 -07:00
celestial-vault 2eb57384ab remove useEffect (#5261) 2025-07-30 17:11:01 -07:00
Sarah Fortune c80bae504a Add a flag to build the webview without minification, compact, etc. (#5262)
When the webview is built wuth build:test:
* don't compact the compiled code
* don't minify
* use inline source maps (the embedded JCEF browser can't load source maps from .map files).
2025-07-30 19:53:36 -04:00
Sarah Fortune 80f955be9e Add vscode.workspace.findFiles to no-direct-vscode-api eslint rule. (#5263)
Add `vscode.workspace.findFiles` to the list of the Vscode API calls that should not be re-introduced to the extension unintentionally.
2025-07-30 19:49:15 -04:00
wangyijing130andwangyj20 7435ffcd2f fix: use Uri.from to generate valid diff URI (#4882)
* fix: use Uri.from to generate valid diff URI

* fix the conflicts for VscodeDiffViewProvider.ts has been moved

---------

Co-authored-by: wangyj20 <wangyj20@asiainfo.com>
2025-07-30 10:42:19 -07:00
Bee a05d438612 refactor: setup e2e tests to use shared mock server (#5245)
* refactor: e2e test setup to use Playwright projects with global server

- Replace globalSetup/globalTeardown with Playwright projects configuration
- Rename setup.ts to global.setup.ts and teardown.ts to global.teardown.ts
- Convert ClineApiServerMock to use shared global server instance
- Add proper dependency management between setup, tests, and cleanup phases
- Improve server connection tracking and cleanup handling

* Rename Playwright test project names to match

* IS_DEV

* update helpers
2025-07-30 00:16:08 -07:00
Daniel Steigman b4b7512d9f Improve Cline accounts support telemetry (#5242)
* fixed linter rule and added identified telemtry stuff

* Updated the error handling

* fixed error handling
2025-07-29 17:23:50 -07:00
ToshiiandDing Fei e08c65618e read_file can read images (png, jpg, jpeg, webp) (#4411)
* base

* throw

* chat ui

Co-authored-by: Ding Fei <fding@feysh.com>

* chat row logic for image file reads

* dim check change

---------

Co-authored-by: Ding Fei <fding@feysh.com>
2025-07-29 15:41:31 -07:00
Bee d653f1cc27 test: update Playwright test timeouts (#5241)
- Rename isGitHubAction to isCI for broader CI detection
- Adjust timeout logic to use CI or Windows conditions
- Reduce expect timeout from 40s/20s to 5s/2s for faster feedback
- Decrease streaming chunk delay from 50ms to 20ms in server mock
2025-07-29 15:27:46 -07:00
Bee c3a97c3eda e2e test: add mock service for cline API & new test for diff editor (#5196)
* Add mock api service and E2E test infrastructure

- Create AuthServiceMock for testing with mock user data and API responses
- Add AuthProvider interface to standardize authentication providers
- Implement E2E test fixtures with mock server and workspace setup
- Add comprehensive E2E tests for authentication and core functionality
- Export DEFAULT_CLINE_APP_URL config and make getEnvironmentConfig more flexible
- Update AuthService to use mock implementation during E2E tests

* format

* import

* refactor mock server

* rename data

* wait for text

* wait for edit

* increase timeout for windows

* clean up

* rename test and add orgs
2025-07-29 12:39:49 -07:00
celestial-vault 0e56272d65 remove chatSettings object (#5178)
* remove chatSettings object

* fix types after merge conflicts
2025-07-29 12:31:52 -07:00
Kevin Taylor fdc2e2655a Add Cerebras model Qwen 3 235b instruct (#5236) 2025-07-29 11:55:48 -07:00
Wintertee 6050413b8b fix: remove duplicate tool registration for claude4-experimental (#4748) 2025-07-29 10:48:41 -07:00
Bee c54f0da737 feat: adds navigation bar component and restructure app layout (#5220) 2025-07-29 13:05:01 -04:00
Sarah Fortune 6cbfb2b8b0 Remove duplication define property in esbuild.json (#5234) 2025-07-29 08:54:12 -07:00
Sarah Fortune 22788f0f12 Move the OutputChannel to the HostProvider (#5189)
* Move the OutputChannel to the HostProvider

Replace `OutputChannel.appendLine` with `HostProvider.logToChannel`.

Remove places where the cline OutputChannel was being passed around. Now it is stored in the HostProvider, so we don't need to do this.

# Conflicts:
#	src/hosts/vscode/VscodeWebviewProvider.ts

* Dont log the timestamp in logger.ts, the cline-core logger already outputs the timestamp

* Fix imports
2025-07-28 22:56:43 -07:00
DongDong Ling 708b785a97 Add Huawei Cloud MaaS Provider (#5071)
* Add Huawei Cloud MaaS Provider

* Fix case error

* Add missing modelid

* add huawei specific modelId and modelInfo

* add huawei specific model id and model info in state.proto

* more huawei maas specific change
2025-07-28 21:47:21 -07:00
Jose R. Perez 099bc44d42 docs: fix Global Rules directory location for Linux/WSL systems (#5219) 2025-07-28 23:54:03 -04:00
Toshii b9f4678dba add try-catch handling (#5227) 2025-07-28 20:21:51 -07:00
Bee f7d17384f6 refactor & fix: improve account view with better states management (#5182)
* refactor & fix: improve account view with better states management

The previous AccountView implementation suffered from several critical state management issues:

- Incorrect info on display: The active account is not ready when component is mounted because the fetching only start on mount but doesn't get reset correctly
- Excessive re-renders: All data was fetched on component mount, causing cascading state updates
- Race conditions: Multiple concurrent API calls and state dependencies created unpredictable behavior, e.g. 403 rate limits errors
- Monolithic state management: All account data, organizations, and auth state was managed in a single massive component
- Poor user experience: Users saw empty states and loading flickers when switching between organizations
- Tight coupling: User and org info logic was deeply embedded within the account view that cause Effect dependency loops

Solution: Centralized Authentication Context

- Extracted auth logic into dedicated ClineAuthContext with organizations state management
- Eliminated prop drilling by providing clineUser, organizations, and activeOrganization at the context level
- Reduced component re-renders by managing auth state separately from UI state
- Performed authentication guard at higher level and only displays user account to authenticated user. The component will get dismounted when user is not autheticated.
- Move handleSignIn and handleSignout into individual functions instead as they are regular functions with no state dependency

* 60secs

* Optimize state updates in AccountView to prevent unnecessary re-renders

Remove conditional checks before setState calls and use functional updates
with deep equality comparison to avoid redundant state changes and
dependency array bloat in useCallback hooks.

* add docs

* fix format

* fix error test

* setuser on logout
2025-07-28 15:46:23 -07:00
Sarah Fortune bb5a64afb3 Quiet spammy MCP debug logs (#5224) 2025-07-28 15:44:38 -07:00
Sarah Fortune 61224734f8 Change timeout, token budget and line limit fields in the ProtoBus from int64 to int32. (#5221) 2025-07-28 17:40:15 -04:00
Bee a28b995ab1 Fix styled-components prop warnings (#5181)
* Fix styled-components prop warnings

- Fix styled-components shouldForwardProp warnings by filtering non-DOM props
- Clean up unused imports in ChatTextArea and other components

* use mjs

* later

* remove unused imports
2025-07-28 11:19:32 -07:00
Bee a91878efc6 Fix: webview panel state change steals focus (#5193)
* Fix: webview panel state change steals focus

Fix webview visibility detection to check both visible and active states before taking focus. If a panel is visible but not active (focused), it should not steals editor focus.

Also removes unused import & add type imports

* add changeset
2025-07-28 10:59:00 -07:00
Jonathan BarazanyandJonathan Barazany 65c21e7b7d Bug fix: VSCode LM API token counting for Claude models (#5051)
* Improve token counting for Claude models in VSCode LM provider

- Reorder imports for better organization
- Add extractTextFromMessage helper method
- Add isClaudeModel detection method
- Use 4:1 character-to-token ratio for Claude models instead of VSCode's inaccurate counting
- Fallback to existing VSCode LM token counting for non-Claude models

* Update version to 3.18.3-r1 and refactor token calculation in VsCodeLmHandler

* 3.19.5-r1

* Add smart jobs impress changeset for VSCode LM API token counting fix

---------

Co-authored-by: Jonathan Barazany <jbarazany@microsoft.com>
2025-07-28 02:09:40 -07:00
Sarah Fortuneandellipsis-dev[bot] 56e388c90f Add a check to the proto scripts to warn about using int64 types. (#5174)
* Add a check to the proto scripts to warn about using int64 types.

Javascript cannot represent the full range of int64. So, when the protos are deserialized from JSON int64's are converted to strings. The typescript code is expecting a number and not a string, and this causes errors.

This was noticed before now because in the vscode protobus and hostbridge, the proto messages are not serialized and deserialized, they are just passed around as JS objects.

However, in IntelliJ the protos are serialized when they are sent through the ProtoBus. When the response messages contains and int64, it is deserialized to a string instead of a number for safety. This is causes parts of Cline to fail in IntelliJ, e.g. the task history view won't load because `Task.getTotalTasksSize()` returns a string when it is expecting a number.

* Make checkProtos shorter

* Update scripts/build-proto.mjs

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

* Update scripts/build-proto.mjs

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

* Update scripts/build-proto.mjs

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

* Fix typo

* Fix typo

* Fix bad merge

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-07-27 22:02:12 -04:00
pashpashpash 586d804a01 Revert "Spruce up HistoryPreview (#4101)" (#5207)
This reverts commit cdfffb8464.
2025-07-27 16:48:49 -07:00
Saoud Rizwan 85fbbcbe3f Revise contributing guidelines and fix PR template link to feature requests board (#5195)
* Revise contributing guidelines and fix PR template link to feature requests board

* Fix wording
2025-07-26 12:43:59 -07:00
ZhangZhiheng 25c5310383 Fix url no trim (#4641)
* fix: trim input value for URL fields in BaseUrlField and DebouncedTextField components (#4051)

* chore: add changeset
2025-07-26 12:23:09 -07:00
Saoud Rizwan 19ef843d4f Revert "feat: update Gemini models - remove deprecated and add 2.5 Flash-Lite…" (#5194)
This reverts commit f6273e0661.
2025-07-26 11:53:06 -07:00
dependabot[bot] 2b3dd14271 Bump the npm_and_yarn group with 3 updates (#4186)
Bumps the npm_and_yarn group with 3 updates: [brace-expansion](https://github.com/juliangruber/brace-expansion), [tar-fs](https://github.com/mafintosh/tar-fs) and [undici](https://github.com/nodejs/undici).


Updates `brace-expansion` from 1.1.11 to 1.1.12
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/1.1.11...v1.1.12)

Updates `tar-fs` from 3.0.8 to 3.0.9
- [Commits](https://github.com/mafintosh/tar-fs/compare/v3.0.8...v3.0.9)

Updates `undici` from 6.21.1 to 6.21.3
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v6.21.1...v6.21.3)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.12
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: tar-fs
  dependency-version: 3.0.9
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: undici
  dependency-version: 6.21.3
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-26 08:03:48 -07:00
Karan Vaidya 721e7ae305 Add Composio to adding-mcp-servers-from-github.mdx (#4624)
* Add Composio to adding-mcp-servers-from-github.mdx

* Update adding-mcp-servers-from-github.mdx
2025-07-26 07:52:27 -07:00
Eugene Demkin f6273e0661 feat: update Gemini models - remove deprecated and add 2.5 Flash-Lite (#4681)
- Remove deprecated experimental models:
  - gemini-1.5-flash-8b-exp-0827
  - gemini-1.5-flash-exp-0827
  - gemini-1.5-pro-exp-0827
- Add gemini-2.5-flash-lite-preview-06-17 with latest pricing
2025-07-26 07:45:29 -07:00
CellenLee 68d0af2afc feat: moonshot provider ui polish (#5034) 2025-07-26 07:21:50 -07:00
Saoud Rizwan 4286f301d2 Remove feature_contribution issue type (#5184) 2025-07-26 06:18:41 -07:00
dependabot[bot] 28536084bf Bump form-data in /webview-ui in the npm_and_yarn group (#5095)
---
updated-dependencies:
- dependency-name: form-data
  dependency-version: 4.0.4
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-26 06:07:58 -07:00
Utkarsh dfbac3eefd Support for Deepseek R1 0528 (#3903) (#5183) 2025-07-26 05:45:31 -07:00
Saoud Rizwan 04f0710a03 Update bug report template to make system information and logs fields optional (#5159) 2025-07-26 01:58:48 -07:00
kvyb 195e15db32 Move vscode.commands.executeCommand("vscode.open") to Hostbridge (#5173)
* feat: add openFile host bridge for vscode.open command

* fix: simplify openFile hostbridge to follow gRPC best practices:
- Remove success boolean field from OpenFileResponse proto
- Use gRPC exceptions for error handling instead of success/failure booleans
- Simplify hostbridge implementation to just move existing vscode.open code

* fix: remove create wrapper from openFile call

* fix proto merge conflict
2025-07-26 04:19:03 -04:00
19cb70de55 Use npm moduleopen to open URLs in the external browser (instead of the host bridge) (#5013)
* Use npm `open` to open URLs in the external browser

# Conflicts:
#	src/utils/env.ts

# Conflicts:
#	src/utils/env.ts

* Change log statement

* Use the simple-open-url module to open URLs in the system browser.

Log failures of ProtoBus RPCs

* Remove vscode hostbridge handler for openExternal

* Rm unused imports

* Switch back to `open` module.

Update esbuild.js to ES6 and move to esbuild.mjs

* Update src/utils/env.ts

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

* remove IS_DEV from e2e setup build

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
Co-authored-by: abeatrix <beatrix@cline.bot>
2025-07-26 04:06:53 -04:00
e2a2ecde44 v3.20.2 Release Notes (#5155)
* 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-26 00:00:19 -07:00
Saoud Rizwan 1a466c0a44 Fix cursor state after restoring files to be disabled after checked out (#5179)
* Fix cursor state after restoring files to be disabled after checked out

* Create silver-llamas-pretend.md
2025-07-25 23:51:34 -07:00
Sarah Fortune 31cbf489c4 Fix launch configuration for the cline-core (#5170) 2025-07-26 02:38:38 -04:00
Saoud Rizwan c667d34f27 Fix issue where checkpointing blocked UI (#5177)
* Fix issue where checkpointing blocked UI

* Create wet-fishes-study.md
2025-07-25 23:24:39 -07:00
Sarah Fortune a1bf1f95a3 refactor(proto): Align proto directory structure with package names to follow best practices (#5171)
* Reorganized proto directory structure to match package naming convention

Moved cline package protos from the proto directory to proto/cline/ directory
Host package protos remain in proto/host/ directory

Updated all import statements across codebase to reflect new proto paths

Removed proto linter exception for package/directory mismatch rule

Fix Vscode proto indexing errors by setting the proto path in the Vscode settings.

* Update imports to use new package

Update imports from @shared/proto/<thing> to @share/proto/cline/<thing>
2025-07-26 00:58:16 -04:00
385 changed files with 9189 additions and 3766 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Fixed issue with sap ai core client credentials storage
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fix prompt caching and support extended thinking for Opus 4.1
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Fix Qwen Api option inconsistency between UI and API layer
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
feat: Added Claude Opus 4.1 to Bedrock
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Fix credit balance out of sync issue on account switching
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Fix Claude Code CLAUDE_CODE_MAX_OUTPUT_TOKENS
+1 -1
View File
@@ -716,7 +716,7 @@ The Controller class manages MCP servers through the McpHub service:
class Controller {
mcpHub?: McpHub
constructor(context: vscode.ExtensionContext, outputChannel: vscode.OutputChannel, webviewProvider: WebviewProvider) {
constructor(context: vscode.ExtensionContext, webviewProvider: WebviewProvider) {
this.mcpHub = new McpHub(this)
}
+12 -18
View File
@@ -5,7 +5,7 @@ body:
- type: markdown
attributes:
value: |
**Important:** All bug reports must be reproducible using Claude 3.5 Sonnet. Cline uses complex prompts so less capable models may not work as expected.
**Important:** All bug reports must be reproducible using Claude 4 Sonnet. Cline uses complex prompts so less capable models may not work as expected.
- type: textarea
id: what-happened
attributes:
@@ -24,7 +24,7 @@ body:
2.
3.
validations:
required: true
required: false
- type: textarea
id: logs
attributes:
@@ -39,20 +39,19 @@ body:
placeholder: "e.g., cline:anthropic/claude-3.7-sonnet, gemini:gemini-2.5-pro-exp-03-25"
validations:
required: true
- type: input
id: operating-system
attributes:
label: Operating System
description: What operating system are you using?
placeholder: "e.g., Windows 11, macOS Sonoma, Ubuntu 22.04"
validations:
required: true
- type: textarea
id: system-info
attributes:
label: System Info
description: What system information is relevant to the issue?
placeholder: "e.g., CPU: Intel Core i7-11700K, GPU: NVIDIA GeForce RTX 3070, RAM: 32GB DDR4"
label: System Information
description: What operating system and hardware are you using?
placeholder: |
Operating System: Windows 11, macOS Sonoma, Ubuntu 22.04, etc.
Hardware: CPU, GPU, RAM specifications if relevant
e.g.,
OS: Windows 11
CPU: Intel Core i7-11700K
GPU: NVIDIA GeForce RTX 3070
RAM: 32GB DDR4
validations:
required: true
- type: input
@@ -63,8 +62,3 @@ body:
placeholder: "e.g., 1.2.3"
validations:
required: true
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Add any other context about the problem here, such as screenshots or related issues.
@@ -1,116 +0,0 @@
name: 💡 Feature Proposal & Contribution
description: Propose a new feature or improvement, and optionally offer to implement feature as a contributor
labels: ["proposal"]
body:
- type: markdown
attributes:
value: |
**Feature Proposal & Contribution for Cline**
Thank you for proposing a feature or improvement for Cline! This template helps us understand the problem, evaluate the solution, and coordinate implementation.
**For detailed proposals:** Please provide comprehensive information to enable fast prioritization and discussion.
**For contribution offers:** You can indicate your willingness to implement the feature yourself.
Before submitting:
- Search existing [Issues](https://github.com/cline/cline/issues) and [Discussions](https://github.com/cline/cline/discussions) to avoid duplicates
- Read the [Contributing Guide](https://github.com/cline/cline/blob/main/CONTRIBUTING.md) if you plan to contribute
- Don't start implementation until the proposal is reviewed and approved
- type: textarea
id: problem-description
attributes:
label: What problem does this solve?
description: |
Describe the problem clearly from a user's point of view. Focus on why this matters, who it affects, and when it occurs.
✅ Good examples:
- "LLM provider returns 400 error when nearing the context window instead of truncating"
- "Submit button is invisible in dark mode"
- "Users can't easily share their Cline configurations with team members"
❌ Avoid vague descriptions:
- "Performance is bad"
- "UI needs work"
Your description should include:
- Who is affected?
- When does it happen?
- What's the current vs expected behavior?
- What is the impact?
placeholder: Be specific about the problem, who it affects, and the impact.
validations:
required: true
- type: textarea
id: proposed-solution
attributes:
label: What's the proposed solution?
description: |
Describe how the problem should be solved. Be specific about UX, system behavior, and any flows that would change.
✅ Good examples:
- "Add error handling immediately after attempting to create the llm stream and retry after manually truncating"
- "Update button styling to ensure contrast in all themes"
- "Add export/import functionality in settings with JSON format"
❌ Avoid vague solutions:
- "Improve performance"
- "Fix the bug"
Your solution should include:
- What exactly will change?
- How will users interact with it?
- What's the expected outcome?
placeholder: Describe the proposed changes and how they solve the problem.
validations:
required: false
- type: dropdown
id: contribution-intent
attributes:
label: Are you interested in implementing this?
description: Let us know if you'd like to contribute to this feature
options:
- "No, just proposing the idea"
- "Yes, I'd like to implement this myself"
- "Yes, I'd like to collaborate with others"
- "Maybe, depending on complexity and guidance"
validations:
required: false
- type: textarea
id: implementation-approach
attributes:
label: Implementation approach (if contributing)
description: |
**Only fill this out if you selected "Yes" above.**
How do you plan to implement this? Include:
- High-level technical approach
- Files/components that would be affected
- Any new dependencies required
- Potential challenges or considerations you've identified
This helps us provide better guidance and ensures alignment before you start coding.
placeholder: "My implementation approach would be..."
- type: checkboxes
id: checklist
attributes:
label: Proposal checklist
options:
- label: I've checked for existing issues or related proposals
required: true
- label: I understand this needs review before implementation can start
required: true
- type: checkboxes
id: contribution-checklist
attributes:
label: Contribution checklist (if contributing)
description: Only check these if you plan to contribute
options:
- label: I've read the [Contributing Guide](https://github.com/cline/cline/blob/main/CONTRIBUTING.md)
- label: I'm willing to make changes based on feedback
- label: I understand the code review process and requirements
+4 -5
View File
@@ -2,15 +2,14 @@
Thank you for contributing to Cline!
⚠️ Important: Before submitting this PR, please ensure you have:
- Opened an issue and discussed your proposed changes with the community / contributors
- Received approval from a core Cline contributor prior to proceeding with the implementation
- Link the associated issue in the "Related Issue" section
- For feature requests: Created a discussion in our Feature Requests discussions board https://github.com/cline/cline/discussions/categories/feature-requests and received approval from core maintainers before implementation
- For all changes: Link the associated issue/discussion in the "Related Issue" section below
Limited exceptions:
Small bug fixes, typo corrections, minor wording improvements, or simple type fixes that don't change functionality may be submitted directly.
Small bug fixes, typo corrections, minor wording improvements, or simple type fixes that don't change functionality may be submitted directly without prior discussion.
Why this requirement?
We deeply appreciate all community contributions - they are the core reason we're able to operate successfully and keep innovating! We welcome community input and want to make it as easy as possible for people to submit quality work. This process helps our core maintainers review new ideas faster and saves contributor time by ensuring you have the go-ahead before spending time on implementation.
We deeply appreciate all community contributions - they are essential to Cline's success! To ensure the best use of everyone's time and maintain project direction, we use our Feature Requests discussions board to gauge community interest and validate feature ideas before implementation begins. This helps us focus development efforts on features that will benefit the most users.
-->
### Related Issue
+12 -15
View File
@@ -96,16 +96,15 @@ jobs:
- name: Build Tests and Extension
run: npm run pretest
# Unit Tests disabled due to module system conflicts between backend and webview-ui
# - name: Unit Tests
# run: npm run test:unit
- name: Unit Tests
run: npm run test:unit
# Run extension tests with coverage
- name: Extension Tests with Coverage
- name: Extension Integration Tests with Coverage
id: extension_coverage
continue-on-error: true
run: |
node ./scripts/test-ci.js > extension_coverage.txt 2>&1
node ./scripts/test-ci.js 2>&1 | tee extension_coverage.txt
# Default the encoding to UTF-8 - It's not the default on Windows
PYTHONUTF8=1 PYTHONPATH=.github/scripts python -m coverage_check extract-coverage extension_coverage.txt --type=extension --github-output --verbose
@@ -117,7 +116,7 @@ jobs:
cd webview-ui
# Ensure coverage dependency is installed
npm install --no-save @vitest/coverage-v8
npm run test:coverage > webview_coverage.txt 2>&1
npm run test:coverage 2>&1 | tee webview_coverage.txt
cd ..
# Default the encoding to UTF-8 - It's not the default on Windows
PYTHONUTF8=1 PYTHONPATH=.github/scripts python -m coverage_check extract-coverage webview-ui/webview_coverage.txt --type=webview --github-output --verbose
@@ -132,21 +131,19 @@ jobs:
path: |
extension_coverage.txt
webview-ui/webview_coverage.txt
retention-period: workflow # Artifacts are automatically deleted when the workflow completes
# Set the check as failed if any of the tests failed
- name: Print test results and check for failures
- name: Check for test failures
run: |
echo "Extension Tests Result: ${{ steps.extension_coverage.outcome }}"
cat extension_coverage.txt
echo "Webview Tests Result: ${{ steps.webview_coverage.outcome }}"
cat webview-ui/webview_coverage.txt
# Check if any of the test steps failed
# https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs#steps-context
if [ "${{ steps.extension_coverage.outcome }}" != "success" ]; then
echo "Extension Integration Tests failed, see previous step for test output."
fi
if [ "${{ steps.webview_coverage.outcome }}" != "success" ]; then
echo "Webview Tests failed, see previous step for test output."
fi
if [ "${{ steps.extension_coverage.outcome }}" != "success" ] || [ "${{ steps.webview_coverage.outcome }}" != "success" ]; then
echo "Tests failed."
exit 1
fi
+3 -5
View File
@@ -71,7 +71,7 @@
{
"type": "node",
"request": "launch",
"name": "Run Standalone Service",
"name": "Run cline-core service",
"skipFiles": ["<node_internals>/**"],
"sourceMaps": true,
"resolveSourceMapLocations": ["${workspaceFolder}/**", "!**/node_modules/**"],
@@ -82,11 +82,9 @@
// Turns on grpc debug log.
//"GRPC_TRACE": "all",
//"GRPC_VERBOSITY": "DEBUG",
"NODE_PATH": "${workspaceFolder}/dist-standalone/node_modules",
"HOST_BRIDGE_ADDRESS": "localhost:50052"
"NODE_PATH": "${workspaceFolder}/dist-standalone/node_modules"
},
"program": "standalone.js"
"program": "cline-core.js"
}
]
}
+5 -1
View File
@@ -9,5 +9,9 @@
"dist": true // set this to false to include "dist" folder in search results
},
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
"typescript.tsc.autoDetect": "off"
"typescript.tsc.autoDetect": "off",
// Protobuf settings
"protoc": {
"options": ["--proto_path=proto"]
}
}
+58
View File
@@ -1,5 +1,63 @@
# Changelog
## [3.20.11]
Add gpt-oss-120b as a Cerebras model
Add Opus 4.1 through Claude Code
## [3.20.10]
- Add OpenAI's new open-source models (GPT-OSS-120B and GPT-OSS-20B) to Hugging Face and Groq providers
## [3.20.9]
- Add support for Claude Opus 4.1 model in Anthropic provider
- Add Baseten as a new API provider with support for DeepSeek, Llama, and Kimi K2 models (Thanks @AlexKer!)
- Fix error messages not clearing from UI when retrying failed tasks
- Fix chat input box positioning issues
## [3.20.8]
- Add navbar tooltips on hover
## [3.20.7]
- Fix circular dependency that affect the github workflow Tests / test (pull_request)
## [3.20.6]
- Fix login check on extension restart
## [3.20.5]
- Fix authentication persistence issues that could cause users to be logged out unexpectedly
## [3.20.4]
- Add new Cerebras models
- Update rate limits for existing Cerebras models
- Fix for delete task dialog
## [3.20.3]
- Add Huawei Cloud MaaS Provider (Thanks @ddling!)
- Add Cerebras Qwen 3 235B instruct model (Thanks @kevint-cerebras!)
- Add DeepSeek R1 0528 support under Hugging Face (Thanks @0ne0rZer0!)
- Fix Global Rules directory documentation for Linux/WSL systems
- Fix token counting when using VSCode LM API provider
- Fix input field stealing focus issue by only focusing on visible and active editor panels
- Fix duplicate tool registration for claude4-experimental
- Trim input value for URL fields
## [3.20.2]
- Fixed issue with sap ai core client credentials storage
- Fix Qwen Api option inconsistency between UI and API layer
- Fix credit balance out of sync issue on account switching
- Fix Claude Code CLAUDE_CODE_MAX_OUTPUT_TOKENS
- Fix cursor state after restoring files to be disabled after checked out
- Fix issue where checkpointing blocked UI
## [3.20.1]
- Fix for files being deleted when switching modes or closing tasks
+5 -8
View File
@@ -14,14 +14,11 @@ Bug reports help make Cline better for everyone! Before creating a new issue, pl
## Before Contributing
All contributions must begin with a GitHub Issue, unless the change is for small bug fixes, typo corrections, minor wording improvements, or simple type fixes that don't change functionality.
- **Check existing issues**: Search [GitHub Issues](https://github.com/cline/cline/issues).
- **Create an issue**: Use appropriate templates:
- **Contributions:** Use the "Contribution Request" template to propose what you'd like to work on.
- **Bugs:** "Bug Report" template for reporting issues.
- **Features:** "Detailed Feature Proposal" template for suggesting new features.
- **Wait for approval**: A core Cline contributor must approve your contribution request before you start implementation.
- **Claim issues**: Once approved, the issue will be assigned to you.
**For features and contributions**:
- First check the [Feature Requests discussions board](https://github.com/cline/cline/discussions/categories/feature-requests) for similar ideas
- If your idea is new, create a new feature request
- Wait for approval from core maintainers before starting implementation
- Once approved, feel free to begin working on a PR with the help of our community!
**PRs without approved issues may be closed.**
-1
View File
@@ -9,7 +9,6 @@ lint:
except: # Add exceptions for current patterns that contradict STANDARD settings
- RPC_PASCAL_CASE # rpcs are camel case (start with lowercase)
- PACKAGE_DIRECTORY_MATCH # the protos in the cline package are not in a dir named cline.
- RPC_REQUEST_RESPONSE_UNIQUE # request messages are not unique.
- RPC_REQUEST_STANDARD_NAME # request messages dont all end with Request
- RPC_RESPONSE_STANDARD_NAME # response messages dont all end with Response
+13 -1
View File
@@ -11,7 +11,19 @@ You can create a rule by clicking the `+` button in the Rules tab. This will ope
Once you save the file:
- Your rule will be stored in the `.clinerules/` directory in your project (if it's a Workspace Rule)
- Or in the `Documents/Cline/Rules` directory (if it's a Global Rule).
- Or in the Global Rules directory (if it's a Global Rule):
### Global Rules Directory Location
The location of your Global Rules directory depends on your operating system:
| Operating System | Default Location | Notes |
|------------------|------------------|-------|
| **Windows** | `Documents\Cline\Rules` | Uses system Documents folder |
| **macOS** | `~/Documents/Cline/Rules` | Uses user Documents folder |
| **Linux/WSL** | `~/Documents/Cline/Rules` | May fall back to `~/Cline/Rules` on some systems |
> **Note for Linux/WSL users**: If you don't find your global rules in `~/Documents/Cline/Rules`, check `~/Cline/Rules` as the location may vary depending on your system configuration and whether the Documents directory exists.
You can also have Cline create a rule for you by using the [`/newrule` slash command](/features/slash-commands/new-rule) in the chat.
@@ -17,6 +17,7 @@ There are multiple places online to find MCP servers:
- [mcpservers.org](https://mcpservers.org/)
- [mcp.so](https://mcp.so/)
- [glama.ai/mcp/servers](https://glama.ai/mcp/servers)
- [mcp.composio.dev](https://mcp.composio.dev/)
These directories allow users to sort the servers by various criteria such as downloads, date, stars, and use case. Each entry provides information such as features, tools, and configuration instructions.
+4 -4
View File
@@ -4,17 +4,17 @@ title: "Telemetry"
### Overview
To help make Cline better for everyone, we collect anonymous usage data that helps us understand how developers are using our open-source AI coding agent. This feedback loop is crucial for improving Cline's capabilities and user experience.
To help make Cline better for everyone, we collect usage data that helps us understand how developers are using our open-source AI coding agent. This feedback loop is crucial for improving Cline's capabilities and user experience.
We use PostHog, an open-source analytics platform, for data collection and analysis. Our telemetry implementation is fully transparent - you can review the [source code](https://github.com/cline/cline/blob/main/src/services/posthog/telemetry/TelemetryService.ts) to see exactly what we track.
### Tracking Policy
Privacy is our priority. All collected data is anonymized before being sent to PostHog, with no personally identifiable information (PII) included. Your code, prompts, and conversation content always remain private and are never collected.
Privacy is our priority. By default, all collected data is anonymized. If you log in with a Cline account, your telemetry data will be associated with your account to help us improve the product and provide better support when you encounter issues. Your code, prompts, and conversation content always remain private and are never collected.
### What We Track
We collect basic anonymous usage data including:
We collect basic usage data including:
**Task Interactions:** When tasks start and finish, conversation flow (without content)\
**Mode and Tool Usage:** Switches between plan/act modes, which tools are being used\
@@ -28,7 +28,7 @@ For complete transparency, you can inspect our [telemetry implementation](https:
Telemetry in Cline is entirely optional:
- When you update or install our VS Code extension, you'll see a message about our anonymous telemetry
- When you update or install our VS Code extension, you'll see a message about our telemetry
- You can change your preference anytime in settings
Cline also respects VS Code's global telemetry settings. If you've disabled telemetry at the VS Code level, Cline's telemetry will automatically be disabled as well.
+12 -7
View File
@@ -1,6 +1,10 @@
const esbuild = require("esbuild")
const fs = require("fs")
const path = require("path")
import fs from "node:fs"
import * as esbuild from "esbuild"
import path from "node:path"
import { fileURLToPath } from "node:url"
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const production = process.argv.includes("--production")
const watch = process.argv.includes("--watch")
@@ -127,10 +131,8 @@ const baseConfig = {
sourcemap: !production,
logLevel: "silent",
define: production
? {
"process.env.IS_DEV": JSON.stringify(!production),
}
: undefined,
? { "import.meta.url": "_importMetaUrl", "process.env.IS_DEV": JSON.stringify(!production) }
: { "import.meta.url": "_importMetaUrl" },
tsconfig: path.resolve(__dirname, "tsconfig.json"),
plugins: [
copyWasmFiles,
@@ -141,6 +143,9 @@ const baseConfig = {
format: "cjs",
sourcesContent: false,
platform: "node",
banner: {
js: "const _importMetaUrl=require('url').pathToFileURL(__filename)",
},
}
// Extension-specific configuration
@@ -34,25 +34,30 @@ directApiRuleTester.run("no-direct-vscode-api", noDirectVscodeApiRule, {
},
// Should allow other vscode API calls
{
code: `vscode.window.showInformationMessage("Hello")`,
filename: "test.ts",
code: `vscode.commands.registerCommand("Hello")`,
filename: "/foo/bar.ts",
},
// Should allow postMessage calls on other objects
{
code: `window.postMessage({ type: "test" }, "*")`,
filename: "test.ts",
filename: "/foo/bar.ts",
},
// Should allow variables named vscode but not calling postMessage
{
code: `const vscode = { other: "method" }; vscode.other()`,
filename: "test.ts",
filename: "/foo/bar.ts",
},
// Should allow vscode.postMessage in test files
{
code: `vscode.postMessage({ type: "newTask", text: message.text })`,
filename: "/foo/bar.test.ts",
},
],
invalid: [
// Should disallow vscode.postMessage in regular files
{
code: `vscode.postMessage({ type: "test", data: {} })`,
filename: "test.ts",
filename: "/foo/bar.ts",
errors: [
{
messageId: "useGrpcClient",
@@ -69,23 +74,13 @@ directApiRuleTester.run("no-direct-vscode-api", noDirectVscodeApiRule, {
},
],
},
// 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",
messageId: "useHostBridgeWorkspace",
},
],
},
+17 -7
View File
@@ -35,18 +35,24 @@ const disallowedApis = {
"vscode.env.openExternal": {
messageId: "useUtils",
},
// "vscode.window.showWarningMessage": {
// messageId: "useHostBridgeShowMessage",
// },
"vscode.window.showWarningMessage": {
messageId: "useHostBridgeShowMessage",
},
"vscode.window.showOpenDialog": {
messageId: "useHostBridgeShowMessage",
},
"vscode.window.showErrorMessage": {
messageId: "useHostBridgeShowMessage",
},
// "vscode.window.showInformationMessage": {
// messageId: "useHostBridgeShowMessage",
// },
"vscode.window.showInformationMessage": {
messageId: "useHostBridgeShowMessage",
},
"vscode.window.showInputBox": {
messageId: "useHostBridge",
},
"vscode.workspace.findFiles": {
messageId: "useNative",
},
}
module.exports = createRule({
@@ -87,6 +93,10 @@ module.exports = createRule({
"Use utilities in @/utils instead of calling vscode APIs directly.\n" +
"This provides a consistent abstraction across VSCode and standalone environments.\n" +
"Found: {{code}}",
useNative:
"Use a native Javascript API instead of calling the vscode API.\n" +
"This provides a consistent abstraction across VSCode and standalone environments.\n" +
"Found: {{code}}",
},
schema: [],
},
@@ -187,7 +197,7 @@ module.exports = createRule({
if (filename.includes("/standalone/runtime-files/")) {
return true
}
// Skip unit tests
// Skip checking test files
if (filename.endsWith(".test.ts")) {
return true
}
+3
View File
@@ -10,6 +10,7 @@ interface RunDiffEvalOptions {
parsingFunction: string
diffEditFunction: string
thinkingBudget: number
provider: string
parallel: boolean
verbose: boolean
testPath: string
@@ -39,6 +40,8 @@ export async function runDiffEvalHandler(options: RunDiffEvalOptions) {
options.parsingFunction,
"--diff-edit-function",
options.diffEditFunction,
"--provider",
options.provider,
]
// Conditionally add the optional arguments
+1
View File
@@ -92,6 +92,7 @@ program
.option("--parsing-function <name>", "The parsing function to use", "parseAssistantMessageV2")
.option("--diff-edit-function <name>", "The diff editing function to use", "constructNewFileContentV2")
.option("--thinking-budget <tokens>", "Set the thinking tokens budget", "0")
.option("--provider <provider>", "API provider to use (openrouter, openai)", "openrouter")
.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")
+30 -17
View File
@@ -1,4 +1,5 @@
import { OpenRouterHandler } from "../../src/api/providers/openrouter"
import { OpenAiNativeHandler } from "../../src/api/providers/openai-native"
import { ApiHandlerOptions } from "../../src/shared/api"
import { Anthropic } from "@anthropic-ai/sdk"
@@ -54,7 +55,7 @@ interface StreamResult {
* Process the stream and return full response with timing data
*/
async function processStream(
handler: OpenRouterHandler,
handler: OpenRouterHandler | OpenAiNativeHandler,
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
): Promise<StreamResult> {
@@ -190,19 +191,7 @@ export async function runSingleEvaluation(input: TestInput): Promise<TestResult>
}
}
const options: ApiHandlerOptions = {
openRouterApiKey: apiKey,
openRouterModelId: modelId,
thinkingBudgetTokens: thinkingBudgetTokens,
openRouterModelInfo: {
maxTokens: 10_000,
contextWindow: 1_000_000,
supportsImages: true,
supportsPromptCache: true, // may need to turn this on
inputPrice: 0,
outputPrice: 0,
},
}
const provider = input.provider || "openrouter"
// Get the output of streaming output of this llm call
let streamResult: StreamResult
@@ -214,10 +203,34 @@ export async function runSingleEvaluation(input: TestInput): Promise<TestResult>
usage: { inputTokens: 0, outputTokens: 0, cacheWriteTokens: 0, cacheReadTokens: 0, totalCost: 0 },
}
} else {
// Live mode: existing API call logic
// Live mode: provider-specific API call logic
try {
const openRouterHandler = new OpenRouterHandler(options)
streamResult = await processStream(openRouterHandler, systemPrompt, messages)
let handler: OpenRouterHandler | OpenAiNativeHandler
if (provider === "openai") {
const openAiOptions = {
openAiNativeApiKey: apiKey,
apiModelId: modelId,
}
handler = new OpenAiNativeHandler(openAiOptions)
} else {
const openRouterOptions = {
openRouterApiKey: apiKey,
openRouterModelId: modelId,
thinkingBudgetTokens: thinkingBudgetTokens,
openRouterModelInfo: {
maxTokens: 10_000,
contextWindow: 1_000_000,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 0,
outputPrice: 0,
},
}
handler = new OpenRouterHandler(openRouterOptions)
}
streamResult = await processStream(handler, systemPrompt, messages)
} catch (error: any) {
return {
success: false,
+17 -6
View File
@@ -49,16 +49,25 @@ type TestResultSet = { [test_id: string]: (TestResult & { test_id?: string })[]
class NodeTestRunner {
private apiKey: string | undefined
private provider: string
private currentRunId: string | null = null
private systemPromptHash: string | null = null
private processingFunctionsHash: string | null = null
private caseIdMap: Map<string, string> = new Map() // test_id -> case_id mapping
constructor(isReplay: boolean) {
constructor(isReplay: boolean, provider: string = "openrouter") {
this.provider = provider
if (!isReplay) {
this.apiKey = process.env.OPENROUTER_API_KEY
if (!this.apiKey) {
throw new Error("OPENROUTER_API_KEY environment variable not set for a non-replay run.")
if (provider === "openai") {
this.apiKey = process.env.OPENAI_API_KEY
if (!this.apiKey) {
throw new Error("OPENAI_API_KEY environment variable not set for a non-replay run with OpenAI provider.")
}
} else {
this.apiKey = process.env.OPENROUTER_API_KEY
if (!this.apiKey) {
throw new Error("OPENROUTER_API_KEY environment variable not set for a non-replay run with OpenRouter provider.")
}
}
}
}
@@ -635,6 +644,7 @@ class NodeTestRunner {
thinkingBudgetTokens: testConfig.thinking_tokens_budget,
originalDiffEditToolCallMessage: testConfig.replay ? testCase.original_diff_edit_tool_call_message : undefined,
diffApplyFile: testConfig.diff_apply_file,
provider: this.provider,
isVerbose: isVerbose,
}
@@ -927,6 +937,7 @@ async function main() {
.option("--parsing-function <name>", "The parsing function to use", "parseAssistantMessageV2")
.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("--provider <provider>", "API provider to use (openrouter, openai)", "openrouter")
.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")
@@ -959,7 +970,7 @@ async function main() {
? parseInt(options.maxAttemptsPerCase, 10)
: validAttemptsPerCase * 10;
const runner = new NodeTestRunner(options.replay || !!options.replayRunId)
const runner = new NodeTestRunner(options.replay || !!options.replayRunId, options.provider)
if (options.replayRunId) {
if (!options.diffApplyFile) {
@@ -979,7 +990,7 @@ async function main() {
log(isVerbose, "Warning: Could not load OpenRouter model data. Context window filtering might be affected for OpenRouter models.");
}
const runner = new NodeTestRunner(options.replay)
const runner = new NodeTestRunner(options.replay, options.provider)
let allLoadedTestCases = runner.loadTestCases(testPath, isVerbose) // Pass isVerbose
const allProcessedTestCasesGlobal: ProcessedTestCase[] = allLoadedTestCases.map((tc) => ({
+107 -7
View File
@@ -331,6 +331,42 @@ def get_performance_grade(success_rate):
else:
return "C", "poor"
def get_error_description(error_enum, error_string=None):
"""Map error enum values to user-friendly descriptions"""
error_map = {
1: "No tool calls - Model didn't use the replace_in_file tool",
2: "Multiple tool calls - Model called multiple tools instead of one",
3: "Wrong tool call - Model used wrong tool (not replace_in_file)",
4: "Missing parameters - Tool call missing required path or diff",
5: "Wrong file edited - Model edited different file than expected",
6: "Wrong tool call - Model used wrong tool type",
7: "Wrong file edited - Model targeted incorrect file path",
8: "API/Stream error - Problem with model API connection",
9: "Configuration error - Invalid evaluation parameters",
10: "Function error - Invalid parsing/diff functions",
11: "Other error - Unexpected failure"
}
base_description = error_map.get(error_enum, f"Unknown error (code: {error_enum})")
if error_string:
return f"{base_description}: {error_string}"
return base_description
def get_error_guidance(error_enum):
"""Provide specific guidance based on error type"""
guidance_map = {
1: "💡 The model provided a response but didn't use the replace_in_file tool. Check the raw output to see what the model actually said.",
2: "💡 The model called multiple tools when it should only call replace_in_file once. Check the parsed tool call section.",
3: "💡 The model used a different tool instead of replace_in_file. This might indicate confusion about the task.",
4: "💡 The model called replace_in_file but didn't provide the required 'path' or 'diff' parameters.",
5: "💡 The model tried to edit a different file than expected. Check the parsed tool call to see which file it targeted.",
6: "💡 The model used the wrong tool type. Check the raw output to see what tool it attempted to use.",
7: "💡 The model tried to edit a different file path than expected. This could indicate path confusion or hallucination.",
}
return guidance_map.get(error_enum, "")
def render_hero_section(current_run, model_performance):
"""Render the hero section with key metrics"""
run_title = current_run['description'] if current_run['description'] else f"Run {current_run['run_id'][:8]}..."
@@ -570,12 +606,16 @@ def render_result_detail(result):
"""Render detailed view of a single result"""
st.markdown("### 🔬 Result Deep Dive")
# Check if this is a valid result
is_valid = (result['error_enum'] not in [1, 6, 7]) if not pd.isna(result['error_enum']) else True
# Check if this is a valid result (only invalid if no tool calls or wrong file)
is_valid = True
if not pd.isna(result['error_enum']):
# Only these specific errors make a result "invalid" for the benchmark:
# 1 = no_tool_calls, 5 = wrong_file_edited, 7 = wrong_file_edited
is_valid = result['error_enum'] not in [1, 5, 7]
# Show validity warning if needed
if not is_valid:
st.warning("⚠️ **This is an invalid result** - The model didn't properly call the diff edit tool or edited the wrong file. This result is excluded from success rate calculations.")
st.warning("⚠️ **This is an invalid result** - The model didn't call the replace_in_file tool or edited the wrong file. This result is excluded from success rate calculations.")
# Result metadata
col1, col2, col3, col4 = st.columns(4)
@@ -591,7 +631,10 @@ def render_result_detail(result):
st.markdown(f"**Round Trip:** {result['time_round_trip_ms']:.0f}ms")
with col4:
st.markdown(f"**Cost:** ${result['cost_usd']:.4f}")
if pd.notna(result['cost_usd']) and result['cost_usd'] is not None:
st.markdown(f"**Cost:** ${result['cost_usd']:.4f}")
else:
st.markdown(f"**Cost:** Free")
# Tabbed interface for different views
tab1, tab2, tab3, tab4 = st.tabs(["📄 File & Edits", "🤖 Raw Output", "🔧 Parsed Tool Call", "📊 Metrics"])
@@ -693,8 +736,46 @@ def render_file_and_edits_view(result):
# Show error information
st.error("❌ **Edit Failed**")
# Show detailed error reason
if not pd.isna(result['error_enum']):
st.markdown(f"**Error Code:** {result['error_enum']}")
error_description = get_error_description(
result['error_enum'],
result.get('error_string')
)
st.markdown(f"**Reason:** {error_description}")
# Show specific guidance based on error type
guidance = get_error_guidance(result['error_enum'])
if guidance:
st.info(guidance)
# For valid results that failed, check for diff application failures
elif not result['succeeded']:
# This is a valid result that failed - likely due to diff application issues
raw_output = result.get('raw_model_output', '')
# Check if we have specific error information in the raw output
if 'does not match anything in the file' in str(raw_output).lower():
st.warning("⚠️ **Diff Application Failed**")
st.info("💡 The SEARCH block in the diff didn't match any content in the original file. This usually means the model hallucinated code that doesn't exist.")
elif 'malformatted' in str(raw_output).lower() or 'malformed' in str(raw_output).lower():
st.warning("⚠️ **Diff Format Error**")
st.info("💡 The diff format was incorrect. Check the raw tool call to see the formatting issues.")
elif 'error:' in str(raw_output).lower():
# Try to extract the specific error message
lines = str(raw_output).split('\n')
error_lines = [line for line in lines if 'error:' in line.lower()]
if error_lines:
error_msg = error_lines[0].strip()
st.warning("⚠️ **Diff Application Failed**")
st.info(f"💡 {error_msg}")
else:
st.warning("⚠️ **Diff Application Failed**")
st.info("💡 The diff couldn't be applied to the original file. Check the raw output and parsed tool call for more details.")
else:
# Generic diff application failure
st.warning("⚠️ **Diff Application Failed**")
st.info("💡 The model made a valid tool call but the diff couldn't be applied to the original file. This usually indicates a mismatch between the expected and actual file content.")
else:
# Show successful edit information
st.success("✅ **Edit Successful**")
@@ -725,8 +806,25 @@ def render_file_and_edits_view(result):
if len(edited_lines) > 50:
st.text(f"... ({len(edited_lines) - 50} more lines)")
# Show parsed tool call if available
# Show raw and parsed tool calls if available
if not pd.isna(result['parsed_tool_call_json']):
with st.expander("View Raw Tool Call"):
# Extract the raw tool call text from the model output
raw_output = result['raw_model_output'] if not pd.isna(result['raw_model_output']) else ""
# Try to extract just the tool call portion
if raw_output and '<replace_in_file>' in raw_output:
# Find the tool call block
start_idx = raw_output.find('<replace_in_file>')
end_idx = raw_output.find('</replace_in_file>') + len('</replace_in_file>')
if start_idx != -1 and end_idx != -1:
raw_tool_call = raw_output[start_idx:end_idx]
st.code(raw_tool_call, language='xml')
else:
st.text("Tool call not found in raw output")
else:
st.text("No raw tool call available")
with st.expander("View Parsed Tool Call"):
try:
parsed_call = json.loads(result['parsed_tool_call_json'])
@@ -795,8 +893,10 @@ def render_metrics_view(result):
if not pd.isna(result['completion_tokens']):
st.metric("Completion Tokens", int(result['completion_tokens']))
if not pd.isna(result['cost_usd']):
if pd.notna(result['cost_usd']) and result['cost_usd'] is not None:
st.metric("Cost", f"${result['cost_usd']:.4f}")
else:
st.metric("Cost", "Free")
if not pd.isna(result['tokens_in_context']):
st.metric("Context Tokens", int(result['tokens_in_context']))
+1
View File
@@ -104,5 +104,6 @@ export interface TestInput {
thinkingBudgetTokens: number
originalDiffEditToolCallMessage?: string
diffApplyFile?: string
provider?: string
isVerbose: boolean
}
+20 -23
View File
@@ -1,12 +1,12 @@
{
"name": "claude-dev",
"version": "3.20.0",
"version": "3.20.11",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.20.0",
"version": "3.20.11",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/sdk": "^0.37.0",
@@ -7907,10 +7907,9 @@
"integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA=="
},
"node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"license": "MIT",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dependencies": {
"balanced-match": "^1.0.0"
}
@@ -17786,10 +17785,9 @@
}
},
"node_modules/tar-fs": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.8.tgz",
"integrity": "sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==",
"license": "MIT",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.0.tgz",
"integrity": "sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==",
"dependencies": {
"pump": "^3.0.0",
"tar-stream": "^3.1.5"
@@ -18418,10 +18416,9 @@
"license": "MIT"
},
"node_modules/undici": {
"version": "6.21.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz",
"integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==",
"license": "MIT",
"version": "6.21.3",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz",
"integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==",
"engines": {
"node": ">=18.17"
}
@@ -25163,9 +25160,9 @@
"integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA=="
},
"brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"requires": {
"balanced-match": "^1.0.0"
}
@@ -31801,9 +31798,9 @@
}
},
"tar-fs": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.8.tgz",
"integrity": "sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.0.tgz",
"integrity": "sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==",
"requires": {
"bare-fs": "^4.0.1",
"bare-path": "^3.0.0",
@@ -32226,9 +32223,9 @@
"integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g=="
},
"undici": {
"version": "6.21.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz",
"integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ=="
"version": "6.21.3",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz",
"integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw=="
},
"undici-types": {
"version": "5.26.5",
+8 -7
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.20.1",
"version": "3.20.11",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -119,7 +119,8 @@
{
"type": "webview",
"id": "claude-dev.SidebarProvider",
"name": ""
"name": "",
"icon": "assets/icons/icon.svg"
}
]
},
@@ -338,14 +339,14 @@
},
"scripts": {
"vscode:prepublish": "npm run package",
"compile": "npm run check-types && npm run lint && node esbuild.js",
"compile-standalone": "npm run check-types && npm run lint && node esbuild.js --standalone",
"compile": "npm run check-types && npm run lint && node esbuild.mjs",
"compile-standalone": "npm run check-types && npm run lint && node esbuild.mjs --standalone",
"postcompile-standalone": "node scripts/package-standalone.mjs",
"watch": "npm-run-all -p watch:*",
"watch:esbuild": "node esbuild.js --watch",
"watch:esbuild": "node esbuild.mjs --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 scripts/build-proto.mjs && node scripts/generate-protobus-setup.mjs && node scripts/generate-host-bridge-client.mjs",
"package": "npm run check-types && npm run build:webview && npm run lint && node esbuild.mjs --production",
"protos": "node scripts/build-proto.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",
+20 -6
View File
@@ -1,17 +1,31 @@
import { defineConfig } from "@playwright/test"
const isGitHubAction = !!process.env.CI
const isCI = !!process?.env?.CI
const isWindow = process?.platform?.startsWith("win")
export default defineConfig({
workers: 1,
retries: 1,
testDir: "src/test/e2e",
timeout: 20000,
timeout: isCI || isWindow ? 40000 : 20000,
expect: {
timeout: 20000,
timeout: isCI || isWindow ? 5000 : 2000,
},
fullyParallel: true,
reporter: isGitHubAction ? [["github"], ["list"]] : [["list"]],
globalSetup: require.resolve("./src/test/e2e/utils/setup"),
globalTeardown: require.resolve("./src/test/e2e/utils/teardown"),
reporter: isCI ? [["github"], ["list"]] : [["list"]],
projects: [
{
name: "setup test environment",
testMatch: /global\.setup\.ts/,
},
{
name: "e2e tests",
testMatch: /.*\.test\.ts/,
dependencies: ["setup test environment"],
},
{
name: "cleanup test environment",
testMatch: /global\.teardown\.ts/,
},
],
})
@@ -1,7 +1,7 @@
syntax = "proto3";
package cline;
import "common.proto";
import "cline/common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
@@ -127,4 +127,4 @@ message OrganizationUsageTransaction {
int32 prompt_tokens = 10;
int32 total_tokens = 11;
string user_id = 12;
}
}
@@ -1,7 +1,7 @@
syntax = "proto3";
package cline;
import "common.proto";
import "cline/common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
@@ -1,7 +1,7 @@
syntax = "proto3";
package cline;
import "common.proto";
import "cline/common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
+1 -1
View File
@@ -1,7 +1,7 @@
syntax = "proto3";
package cline;
import "common.proto";
import "cline/common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
+1 -1
View File
@@ -1,7 +1,7 @@
syntax = "proto3";
package cline;
import "common.proto";
import "cline/common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
@@ -1,7 +1,7 @@
syntax = "proto3";
package cline;
import "common.proto";
import "cline/common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
@@ -27,6 +27,8 @@ service ModelsService {
rpc updateApiConfigurationProto(UpdateApiConfigurationRequest) returns (Empty);
// Refreshes and returns Groq models
rpc refreshGroqModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
// Refreshes and returns Baseten models
rpc refreshBasetenModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
}
// List of VS Code LM models
@@ -129,6 +131,8 @@ enum ApiProvider {
CLAUDE_CODE = 26;
MOONSHOT = 27;
HUGGINGFACE = 28;
HUAWEI_CLOUD_MAAS = 29;
BASETEN = 30;
}
// Model info for OpenAI-compatible models
@@ -229,6 +233,8 @@ message ModelsApiConfiguration {
optional string cline_account_id = 58;
optional string groq_api_key = 59;
optional string hugging_face_api_key = 60;
optional string huawei_cloud_maas_api_key = 61;
optional string baseten_api_key = 62;
// Plan mode configurations
optional ApiProvider plan_mode_api_provider = 100;
@@ -255,6 +261,10 @@ message ModelsApiConfiguration {
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;
optional string plan_mode_huawei_cloud_maas_model_id = 124;
optional OpenRouterModelInfo plan_mode_huawei_cloud_maas_model_info = 125;
optional string plan_mode_baseten_model_id = 126;
optional OpenRouterModelInfo plan_mode_baseten_model_info = 127;
// Act mode configurations
optional ApiProvider act_mode_api_provider = 200;
@@ -281,6 +291,10 @@ message ModelsApiConfiguration {
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;
optional string act_mode_huawei_cloud_maas_model_id = 224;
optional OpenRouterModelInfo act_mode_huawei_cloud_maas_model_info = 225;
optional string act_mode_baseten_model_id = 226;
optional OpenRouterModelInfo act_mode_baseten_model_info = 227;
repeated string favorited_model_ids = 300;
}
@@ -1,7 +1,7 @@
syntax = "proto3";
package cline;
import "common.proto";
import "cline/common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
+28 -18
View File
@@ -1,19 +1,19 @@
syntax = "proto3";
package cline;
import "common.proto";
import "cline/common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
service StateService {
rpc getLatestState(EmptyRequest) returns (State);
rpc updateTerminalConnectionTimeout(Int64Request) returns (Int64);
rpc updateTerminalConnectionTimeout(UpdateTerminalConnectionTimeoutRequest) returns (UpdateTerminalConnectionTimeoutResponse);
rpc updateTerminalReuseEnabled(BooleanRequest) returns (Empty);
rpc updateDefaultTerminalProfile(StringRequest) returns (TerminalProfileUpdateResponse);
rpc getAvailableTerminalProfiles(EmptyRequest) returns (TerminalProfiles);
rpc subscribeToState(EmptyRequest) returns (stream State);
rpc toggleFavoriteModel(StringRequest) returns (Empty);
rpc resetState(ResetStateRequest) returns (Empty);
rpc togglePlanActMode(TogglePlanActModeRequest) returns (Boolean);
rpc togglePlanActModeProto(TogglePlanActModeRequest) returns (Boolean);
rpc updateAutoApprovalSettings(AutoApprovalSettingsRequest) returns (Empty);
rpc updateSettings(UpdateSettingsRequest) returns (Empty);
rpc updateTelemetrySetting(TelemetrySettingRequest) returns (Empty);
@@ -43,7 +43,7 @@ message TerminalProfileUpdateResponse {
message TogglePlanActModeRequest {
Metadata metadata = 1;
ChatSettings chat_settings = 2;
PlanActMode mode = 2;
optional ChatContent chat_content = 3;
}
@@ -52,12 +52,6 @@ enum PlanActMode {
ACT = 1;
}
message ChatSettings {
PlanActMode mode = 1;
optional string preferred_language = 2;
optional string open_ai_reasoning_effort = 3;
}
message ChatContent {
optional string message = 1;
repeated string images = 2;
@@ -108,12 +102,15 @@ message UpdateSettingsRequest {
optional bool plan_act_separate_models_setting = 4;
optional bool enable_checkpoints_setting = 5;
optional bool mcp_marketplace_enabled = 6;
optional ChatSettings chat_settings = 7;
optional int64 shell_integration_timeout = 8;
optional int32 shell_integration_timeout = 8;
optional bool terminal_reuse_enabled = 9;
optional bool mcp_responses_collapsed = 10;
optional string mcp_display_mode = 11;
optional int64 terminal_output_line_limit = 12;
optional int32 terminal_output_line_limit = 12;
optional PlanActMode mode = 13;
optional string preferred_language = 14;
optional string openai_reasoning_effort = 15;
optional bool strict_plan_mode_enabled = 16;
}
// Complete API Configuration message
@@ -153,8 +150,8 @@ message ApiConfiguration {
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 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;
@@ -166,7 +163,7 @@ message ApiConfiguration {
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 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;
@@ -174,11 +171,12 @@ message ApiConfiguration {
optional string sap_ai_core_base_url = 53;
optional string moonshot_api_key = 54;
optional string moonshot_api_line = 55;
optional string huawei_cloud_maas_api_key = 56;
// 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 int32 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;
@@ -196,11 +194,13 @@ message ApiConfiguration {
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_huawei_cloud_maas_model_id = 120;
optional string plan_mode_huawei_cloud_maas_model_info = 121;
// 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 int32 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;
@@ -218,6 +218,8 @@ message ApiConfiguration {
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_huawei_cloud_maas_model_id = 220;
optional string act_mode_huawei_cloud_maas_model_info = 221;
// Favorited model IDs
repeated string favorited_model_ids = 300;
@@ -228,3 +230,11 @@ message ApiConfiguration {
optional string cline_account_id = 303;
}
message UpdateTerminalConnectionTimeoutRequest {
optional int32 timeout_ms = 1;
}
message UpdateTerminalConnectionTimeoutResponse {
optional int32 timeout_ms = 1;
}
+1 -1
View File
@@ -1,7 +1,7 @@
syntax = "proto3";
package cline;
import "common.proto";
import "cline/common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
+1 -1
View File
@@ -1,7 +1,7 @@
syntax = "proto3";
package cline;
import "common.proto";
import "cline/common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
+1 -1
View File
@@ -1,7 +1,7 @@
syntax = "proto3";
package cline;
import "common.proto";
import "cline/common.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
+1 -1
View File
@@ -4,7 +4,7 @@ package host;
option java_package = "bot.cline.host.proto";
option java_multiple_files = true;
import "common.proto";
import "cline/common.proto";
// Provides methods for diff views.
service DiffService {
+1 -4
View File
@@ -4,7 +4,7 @@ package host;
option java_package = "bot.cline.host.proto";
option java_multiple_files = true;
import "common.proto";
import "cline/common.proto";
// Provides methods for working with the user's environment.
service EnvService {
@@ -13,7 +13,4 @@ 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);
}
+1 -1
View File
@@ -4,7 +4,7 @@ package host;
option java_package = "bot.cline.host.proto";
option java_multiple_files = true;
import "common.proto";
import "cline/common.proto";
/**
* The watch service is only here as example of a streaming rpc in the host bridge.
+15 -2
View File
@@ -4,7 +4,7 @@ package host;
option java_package = "bot.cline.host.proto";
option java_multiple_files = true;
import "common.proto";
import "cline/common.proto";
// Provides methods for working with IDE windows and editors.
service WindowService {
@@ -14,6 +14,7 @@ service WindowService {
rpc showMessage(ShowMessageRequest) returns (SelectedResponse);
rpc showInputBox(ShowInputBoxRequest) returns (ShowInputBoxResponse);
rpc showSaveDialog(ShowSaveDialogRequest) returns (ShowSaveDialogResponse);
rpc openFile(OpenFileRequest) returns (OpenFileResponse);
rpc getOpenTabs(GetOpenTabsRequest) returns (GetOpenTabsResponse);
rpc getVisibleTabs(GetVisibleTabsRequest) returns (GetVisibleTabsResponse);
}
@@ -83,6 +84,8 @@ message ShowSaveDialogRequest {
message ShowSaveDialogOptions {
optional string default_path = 1;
// A map of file types to extensions, e.g
// "Text Files": { "extensions": ["txt", "md"] }
map<string, FileExtensionList> filters = 2;
}
@@ -91,6 +94,7 @@ message FileExtensionList {
}
message ShowSaveDialogResponse {
// If the user cancelled the dialog, this will be empty.
optional string selected_path = 1;
}
@@ -105,6 +109,15 @@ message ShowInputBoxResponse {
optional string response = 1;
}
message OpenFileRequest {
cline.Metadata metadata = 1;
string file_path = 2;
}
message OpenFileResponse {
// empty
}
message GetOpenTabsRequest {
// empty
}
@@ -119,4 +132,4 @@ message GetVisibleTabsRequest {
message GetVisibleTabsResponse {
repeated string paths = 1;
}
}
+9 -6
View File
@@ -4,14 +4,14 @@ package host;
option java_package = "bot.cline.host.proto";
option java_multiple_files = true;
import "common.proto";
// Provides methods for working with workspaces/projects.
service WorkspaceService {
// Returns a list of the top level directories of the workspace.
rpc getWorkspacePaths(GetWorkspacePathsRequest) returns (GetWorkspacePathsResponse);
// Saves an open document if it's dirty
rpc saveOpenDocumentIfDirty(SaveOpenDocumentIfDirtyRequest) returns (cline.Empty);
// Saves an open document if it's open in the editor and has unsaved changes.
// Returns true if the document was saved, returns false if the document was not found, or did not
// need to be saved.
rpc saveOpenDocumentIfDirty(SaveOpenDocumentIfDirtyRequest) returns (SaveOpenDocumentIfDirtyResponse);
}
message GetWorkspacePathsRequest {
@@ -28,6 +28,9 @@ message GetWorkspacePathsResponse {
}
message SaveOpenDocumentIfDirtyRequest {
cline.Metadata metadata = 1;
string file_path = 2;
optional string file_path = 2;
}
message SaveOpenDocumentIfDirtyResponse {
// Returns true if the document was saved.
optional bool was_saved = 1;
}
+55 -3
View File
@@ -7,8 +7,10 @@ import { globby } from "globby"
import { createRequire } from "module"
import os from "os"
import * as path from "path"
import { fileURLToPath } from "url"
import { rmrf } from "./file-utils.mjs"
import { main as generateProtoBusSetup } from "./generate-protobus-setup.mjs"
import { main as generateHostBridgeClient } from "./generate-host-bridge-client.mjs"
import { loadProtoDescriptorSet } from "./proto-utils.mjs"
const require = createRequire(import.meta.url)
const PROTOC = path.join(require.resolve("grpc-tools"), "../bin/protoc")
@@ -34,9 +36,14 @@ const TS_PROTO_OPTIONS = [
]
async function main() {
console.log(chalk.bold.blue("Compiling Protocol Buffers..."))
await cleanup()
await compileProtos()
await checkProtos()
await generateProtoBusSetup()
await generateHostBridgeClient()
}
async function compileProtos() {
console.log(chalk.bold.blue("Compiling Protocol Buffers..."))
// Check for Apple Silicon compatibility before proceeding
checkAppleSiliconCompatibility()
@@ -180,6 +187,51 @@ function checkAppleSiliconCompatibility() {
}
}
const int64TypeNames = ["TYPE_INT64", "TYPE_UINT64", "TYPE_SINT64", "TYPE_FIXED64", "TYPE_SFIXED64"]
async function checkProtos() {
const proto = await loadProtoDescriptorSet()
const int64Fields = []
for (const [packageName, packageDef] of Object.entries(proto)) {
for (const [messageName, def] of Object.entries(packageDef)) {
// Skip service definitions
if (def && typeof def === "object" && "service" in def) {
continue
}
// Check message fields
if (def && def.type && def.type.field) {
for (const field of def.type.field) {
if (int64TypeNames.includes(field.type)) {
const name = `${packageName}.${messageName}.${field.name}`
int64Fields.push({
name: name,
type: field.type,
})
}
}
}
}
}
if (int64Fields.length > 0) {
console.log(chalk.yellow(`\nWarning: Found ${int64Fields.length} fields using 64-bit integer types`))
for (const field of int64Fields) {
const typeNames = {
TYPE_INT64: "int64",
TYPE_UINT64: "uint64",
TYPE_SINT64: "sint64",
TYPE_FIXED64: "fixed64",
TYPE_SFIXED64: "sfixed64",
}
log_verbose(chalk.yellow(` - ${field.name} (${typeNames[field.type]})`))
}
log_verbose(chalk.yellow("\nWARNING: 64-bit integer fields detected in proto definitions"))
log_verbose(chalk.yellow("JavaScript cannot safely represent integers larger than 2^53-1 (Number.MAX_SAFE_INTEGER)."))
log_verbose(chalk.yellow("Consider using string representation for large numbers or implementing BigInt support.\n"))
}
}
function log_verbose(s) {
if (process.argv.includes("-v") || process.argv.includes("--verbose")) {
console.log(s)
Regular → Executable
+1
View File
@@ -1,3 +1,4 @@
#!/usr/bin/env node
const { execSync } = require("child_process")
const esbuild = require("esbuild")
+8 -6
View File
@@ -15,7 +15,7 @@ const VSCODE_CLIENT_FILE = path.resolve("src/generated/hosts/vscode/hostbridge-g
/**
* Main function to generate the host bridge client
*/
async function main() {
export async function main() {
const { hostServices } = await loadServicesFromProtoDescriptor()
await generateTypesFile(hostServices)
@@ -234,8 +234,10 @@ const ${name}ServiceRegistry = createServiceRegistry("${name}")
${methods}`
}
// Run the main function
main().catch((error) => {
console.error(chalk.red("Error:"), error)
process.exit(1)
})
// Only run main if this script is executed directly
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((error) => {
console.error(chalk.red("Error:"), error)
process.exit(1)
})
}
+10 -4
View File
@@ -12,7 +12,7 @@ const STANDALONE_SERVER_SETUP_FILE = path.resolve("src/generated/hosts/standalon
const SCRIPT_NAME = path.relative(process.cwd(), fileURLToPath(import.meta.url))
async function main() {
export async function main() {
const { protobusServices } = await loadServicesFromProtoDescriptor()
await generateWebviewProtobusClients(protobusServices)
await generateVscodeServiceTypes(protobusServices)
@@ -40,11 +40,11 @@ async function generateWebviewProtobusClients(protobusServices) {
}
if (!rpc.responseStream) {
rpcs.push(` static async ${rpcName}(request: ${requestType}): Promise<${responseType}> {
return this.makeRequest("${rpcName}", request)
return this.makeUnaryRequest("${rpcName}", request, ${requestType}.toJSON, ${responseType}.fromJSON)
}`)
} else {
rpcs.push(` static ${rpcName}(request: ${requestType}, callbacks: Callbacks<${responseType}>): ()=>void {
return this.makeStreamingRequest("${rpcName}", request, callbacks)
return this.makeStreamingRequest("${rpcName}", request, ${requestType}.toJSON, ${responseType}.fromJSON, callbacks)
}`)
}
}
@@ -205,4 +205,10 @@ function getDirName(serviceName) {
return domain.charAt(0).toLowerCase() + domain.slice(1)
}
main()
// Only run main if this script is executed directly
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((error) => {
console.error(chalk.red("Error:"), error)
process.exit(1)
})
}
+8 -4
View File
@@ -10,7 +10,7 @@ const DESCRIPTOR_SET = path.resolve("dist-standalone/proto/descriptor_set.pb")
const typeNameToFQN = new Map()
function addTypeNameToFqn(name, fqn) {
if (typeNameToFQN.has(name)) {
if (typeNameToFQN.has(name) && typeNameToFQN.get(name) !== fqn) {
throw new Error(`Proto type ${name} redefined (${fqn}).`)
}
typeNameToFQN.set(name, fqn)
@@ -23,11 +23,15 @@ export function getFqn(name) {
return typeNameToFQN.get(name)
}
export async function loadServicesFromProtoDescriptor() {
// Load service definitions from descriptor set
export async function loadProtoDescriptorSet() {
const descriptorBuffer = await fs.readFile(DESCRIPTOR_SET)
const packageDefinition = protoLoader.loadFileDescriptorSetFromBuffer(descriptorBuffer)
const proto = grpc.loadPackageDefinition(packageDefinition)
return grpc.loadPackageDefinition(packageDefinition)
}
export async function loadServicesFromProtoDescriptor() {
// Load service definitions from descriptor set
const proto = await loadProtoDescriptorSet()
// Extract host services and proto messages from the proto definition
const hostServices = {}
Regular → Executable
View File
+18 -1
View File
@@ -29,8 +29,10 @@ 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 { Mode } from "@shared/storage/types"
import { HuggingFaceHandler } from "./providers/huggingface"
import { HuaweiCloudMaaSHandler } from "./providers/huawei-cloud-maas"
import { BasetenHandler } from "./providers/baseten"
export interface ApiHandler {
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream
@@ -256,6 +258,13 @@ function createHandlerForProvider(
groqModelInfo: mode === "plan" ? options.planModeGroqModelInfo : options.actModeGroqModelInfo,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "baseten":
return new BasetenHandler({
basetenApiKey: options.basetenApiKey,
basetenModelId: mode === "plan" ? options.planModeBasetenModelId : options.actModeBasetenModelId,
basetenModelInfo: mode === "plan" ? options.planModeBasetenModelInfo : options.actModeBasetenModelInfo,
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
})
case "sapaicore":
return new SapAiCoreHandler({
sapAiCoreClientId: options.sapAiCoreClientId,
@@ -272,6 +281,14 @@ function createHandlerForProvider(
thinkingBudgetTokens:
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
})
case "huawei-cloud-maas":
return new HuaweiCloudMaaSHandler({
huaweiCloudMaasApiKey: options.huaweiCloudMaasApiKey,
huaweiCloudMaasModelId:
mode === "plan" ? options.planModeHuaweiCloudMaasModelId : options.actModeHuaweiCloudMaasModelId,
huaweiCloudMaasModelInfo:
mode === "plan" ? options.planModeHuaweiCloudMaasModelInfo : options.actModeHuaweiCloudMaasModelInfo,
})
default:
return new AnthropicHandler({
apiKey: options.apiKey,
+85 -84
View File
@@ -612,101 +612,102 @@ describe("AwsBedrockHandler", () => {
})
})
describe("getModelId", () => {
it("should return raw model ID for custom models", async () => {
const customOptions: ApiHandlerOptions = {
...mockOptions,
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)
// TODO: Re-enable or remove these tests.
// describe("getModelId", () => {
// it("should return raw model ID for custom models", async () => {
// const customOptions: ApiHandlerOptions = {
// ...mockOptions,
// 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)
const modelId = await customHandler.getModelId()
modelId.should.equal(
"arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
)
})
// const modelId = await customHandler.getModelId()
// modelId.should.equal(
// "arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
// )
// })
it("should not encode custom model IDs with slashes", async () => {
const customOptions: ApiHandlerOptions = {
...mockOptions,
actModeAwsBedrockCustomSelected: true,
actModeApiModelId: "my-namespace/my-custom-model",
}
const customHandler = new AwsBedrockHandler(customOptions)
// it("should not encode custom model IDs with slashes", async () => {
// const customOptions: ApiHandlerOptions = {
// ...mockOptions,
// actModeAwsBedrockCustomSelected: true,
// actModeApiModelId: "my-namespace/my-custom-model",
// }
// const customHandler = new AwsBedrockHandler(customOptions)
const modelId = await customHandler.getModelId()
modelId.should.equal("my-namespace/my-custom-model")
modelId.should.not.match(/%2F/)
})
// const modelId = await customHandler.getModelId()
// modelId.should.equal("my-namespace/my-custom-model")
// modelId.should.not.match(/%2F/)
// })
it("should apply cross-region prefix for non-custom models when enabled", async () => {
const crossRegionOptions: ApiHandlerOptions = {
...mockOptions,
awsUseCrossRegionInference: true,
awsRegion: "us-west-2",
}
const crossRegionHandler = new AwsBedrockHandler(crossRegionOptions)
// it("should apply cross-region prefix for non-custom models when enabled", async () => {
// const crossRegionOptions: ApiHandlerOptions = {
// ...mockOptions,
// awsUseCrossRegionInference: true,
// awsRegion: "us-west-2",
// }
// const crossRegionHandler = new AwsBedrockHandler(crossRegionOptions)
const modelId = await crossRegionHandler.getModelId()
modelId.should.equal("us.anthropic.claude-3-7-sonnet-20250219-v1:0")
})
// const modelId = await crossRegionHandler.getModelId()
// modelId.should.equal("us.anthropic.claude-3-7-sonnet-20250219-v1:0")
// })
it("should apply EU cross-region prefix", async () => {
const euOptions: ApiHandlerOptions = {
...mockOptions,
awsUseCrossRegionInference: true,
awsRegion: "eu-central-1",
}
const euHandler = new AwsBedrockHandler(euOptions)
// it("should apply EU cross-region prefix", async () => {
// const euOptions: ApiHandlerOptions = {
// ...mockOptions,
// awsUseCrossRegionInference: true,
// awsRegion: "eu-central-1",
// }
// const euHandler = new AwsBedrockHandler(euOptions)
const modelId = await euHandler.getModelId()
modelId.should.equal("eu.anthropic.claude-3-7-sonnet-20250219-v1:0")
})
// const modelId = await euHandler.getModelId()
// modelId.should.equal("eu.anthropic.claude-3-7-sonnet-20250219-v1:0")
// })
it("should apply APAC cross-region prefix", async () => {
const apacOptions: ApiHandlerOptions = {
...mockOptions,
awsUseCrossRegionInference: true,
awsRegion: "ap-northeast-1",
}
const apacHandler = new AwsBedrockHandler(apacOptions)
// it("should apply APAC cross-region prefix", async () => {
// const apacOptions: ApiHandlerOptions = {
// ...mockOptions,
// awsUseCrossRegionInference: true,
// awsRegion: "ap-northeast-1",
// }
// const apacHandler = new AwsBedrockHandler(apacOptions)
const modelId = await apacHandler.getModelId()
modelId.should.equal("apac.anthropic.claude-3-7-sonnet-20250219-v1:0")
})
// const modelId = await apacHandler.getModelId()
// modelId.should.equal("apac.anthropic.claude-3-7-sonnet-20250219-v1:0")
// })
it("should not apply cross-region prefix for custom models even when enabled", async () => {
const customCrossRegionOptions: ApiHandlerOptions = {
...mockOptions,
actModeAwsBedrockCustomSelected: true,
actModeApiModelId: "arn:aws:bedrock:us-west-2:123456789012:custom-model/my-model",
awsUseCrossRegionInference: true,
}
const customCrossRegionHandler = new AwsBedrockHandler(customCrossRegionOptions)
// it("should not apply cross-region prefix for custom models even when enabled", async () => {
// const customCrossRegionOptions: ApiHandlerOptions = {
// ...mockOptions,
// actModeAwsBedrockCustomSelected: true,
// actModeApiModelId: "arn:aws:bedrock:us-west-2:123456789012:custom-model/my-model",
// awsUseCrossRegionInference: true,
// }
// const customCrossRegionHandler = new AwsBedrockHandler(customCrossRegionOptions)
const modelId = await customCrossRegionHandler.getModelId()
modelId.should.equal("arn:aws:bedrock:us-west-2:123456789012:custom-model/my-model")
})
// const modelId = await customCrossRegionHandler.getModelId()
// modelId.should.equal("arn:aws:bedrock:us-west-2:123456789012:custom-model/my-model")
// })
it("should handle UltraThink model ARN correctly", async () => {
const ultraThinkOptions: ApiHandlerOptions = {
...mockOptions,
actModeAwsBedrockCustomSelected: true,
actModeApiModelId:
"arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
actModeAwsBedrockCustomModelBaseId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
}
const ultraThinkHandler = new AwsBedrockHandler(ultraThinkOptions)
// it("should handle UltraThink model ARN correctly", async () => {
// const ultraThinkOptions: ApiHandlerOptions = {
// ...mockOptions,
// actModeAwsBedrockCustomSelected: true,
// actModeApiModelId:
// "arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
// actModeAwsBedrockCustomModelBaseId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
// }
// const ultraThinkHandler = new AwsBedrockHandler(ultraThinkOptions)
const modelId = await ultraThinkHandler.getModelId()
// Should return the raw ARN without any encoding
modelId.should.equal(
"arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
)
modelId.should.not.match(/%2F/)
modelId.should.not.match(/%3A/)
})
})
// const modelId = await ultraThinkHandler.getModelId()
// // Should return the raw ARN without any encoding
// modelId.should.equal(
// "arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
// )
// modelId.should.not.match(/%2F/)
// modelId.should.not.match(/%3A/)
// })
// })
})
+2
View File
@@ -55,6 +55,7 @@ export class AnthropicHandler implements ApiHandler {
case "claude-3-5-sonnet-20241022":
case "claude-3-5-haiku-20241022":
case "claude-opus-4-20250514":
case "claude-opus-4-1-20250805":
case "claude-3-opus-20240229":
case "claude-3-haiku-20240307": {
/*
@@ -122,6 +123,7 @@ export class AnthropicHandler implements ApiHandler {
switch (modelId) {
case "claude-sonnet-4-20250514":
case "claude-opus-4-20250514":
case "claude-opus-4-1-20250805":
case "claude-3-7-sonnet-20250219":
case "claude-3-5-sonnet-20241022":
case "claude-3-5-haiku-20241022":
+165
View File
@@ -0,0 +1,165 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { withRetry } from "../retry"
import { ApiHandler } from "../"
import { BasetenModelId, ModelInfo, basetenDefaultModelId, basetenModels } from "@shared/api"
import { calculateApiCostOpenAI } from "../../utils/cost"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
interface BasetenHandlerOptions {
basetenApiKey?: string
basetenModelId?: string
basetenModelInfo?: ModelInfo
apiModelId?: string // For backward compatibility
}
export class BasetenHandler implements ApiHandler {
private options: BasetenHandlerOptions
private client: OpenAI | undefined
constructor(options: BasetenHandlerOptions) {
this.options = options
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.basetenApiKey) {
throw new Error("Baseten API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://inference.baseten.co/v1",
apiKey: this.options.basetenApiKey,
})
} catch (error) {
throw new Error(`Error creating Baseten client: ${error.message}`)
}
}
return this.client
}
/**
* Gets the optimal max_tokens based on model capabilities
*/
private getOptimalMaxTokens(model: { id: BasetenModelId; info: ModelInfo }): number {
// Use model-specific max tokens if available
if (model.info.maxTokens && model.info.maxTokens > 0) {
return model.info.maxTokens
}
// Default fallback
return 8192
}
getModel(): { id: BasetenModelId; info: ModelInfo } {
// First priority: basetenModelId and basetenModelInfo
const basetenModelId = this.options.basetenModelId
const basetenModelInfo = this.options.basetenModelInfo
if (basetenModelId && basetenModelInfo) {
return { id: basetenModelId as BasetenModelId, info: basetenModelInfo }
}
// Second priority: basetenModelId with static model info
if (basetenModelId && basetenModelId in basetenModels) {
const id = basetenModelId as BasetenModelId
return { id, info: basetenModels[id] }
}
// Third priority: apiModelId (for backward compatibility)
const apiModelId = this.options.apiModelId
if (apiModelId && apiModelId in basetenModels) {
const id = apiModelId as BasetenModelId
return { id, info: basetenModels[id] }
}
// Default fallback
return {
id: basetenDefaultModelId,
info: basetenModels[basetenDefaultModelId],
}
}
private async *yieldUsage(modelInfo: ModelInfo, usage: any): ApiStream {
if (usage.prompt_tokens || usage.completion_tokens) {
const cost = calculateApiCostOpenAI(modelInfo, usage.prompt_tokens || 0, usage.completion_tokens || 0)
yield {
type: "usage",
inputTokens: usage.prompt_tokens || 0,
outputTokens: usage.completion_tokens || 0,
cacheWriteTokens: 0,
cacheReadTokens: 0,
totalCost: cost,
}
}
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
const maxTokens = this.getOptimalMaxTokens(model)
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
const stream = await client.chat.completions.create({
model: model.id,
max_tokens: maxTokens,
messages: openAiMessages,
stream: true,
stream_options: { include_usage: true },
temperature: 0,
})
let didOutputUsage = false
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
if (delta?.content) {
yield {
type: "text",
text: delta.content,
}
}
// Handle usage information - only output once
if (!didOutputUsage && chunk.usage) {
yield* this.yieldUsage(model.info, chunk.usage)
didOutputUsage = true
}
}
}
/**
* 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()
// Baseten models support tools via OpenAI-compatible API
return true
}
}
+10 -3
View File
@@ -102,6 +102,7 @@ export class CerebrasHandler implements ApiHandler {
messages: cerebrasMessages,
temperature: 0,
stream: true,
max_tokens: this.getModel().info.maxTokens,
})
// Handle streaming response
@@ -175,9 +176,15 @@ export class CerebrasHandler implements ApiHandler {
}
getModel(): { id: string; info: ModelInfo } {
const modelId = this.options.apiModelId
if (modelId && modelId in cerebrasModels) {
const id = modelId as CerebrasModelId
const originalModelId = this.options.apiModelId
let apiModelId = originalModelId
if (originalModelId === "qwen-3-coder-480b-free") {
apiModelId = "qwen-3-coder-480b"
return { id: apiModelId, info: cerebrasModels[originalModelId as CerebrasModelId] }
}
if (originalModelId && originalModelId in cerebrasModels) {
const id = originalModelId as CerebrasModelId
return { id, info: cerebrasModels[id] }
}
return {
+53
View File
@@ -0,0 +1,53 @@
// Mock for @google/genai module to avoid ESM compatibility issues in tests
export class GoogleGenAI {
constructor(options: any) {
// Mock constructor
}
models = {
generateContentStream: async (params: any) => {
// Mock implementation that returns an async iterator
return {
async *[Symbol.asyncIterator]() {
yield {
text: "Mock response",
candidates: [],
usageMetadata: {
promptTokenCount: 100,
candidatesTokenCount: 50,
thoughtsTokenCount: 0,
cachedContentTokenCount: 0,
},
}
},
}
},
countTokens: async (params: any) => {
// Mock token counting
return {
totalTokens: 100,
}
},
}
}
// Export mock types
export interface GenerateContentConfig {
httpOptions?: any
systemInstruction?: string
temperature?: number
thinkingConfig?: any
}
export interface GenerateContentResponseUsageMetadata {
promptTokenCount?: number
candidatesTokenCount?: number
thoughtsTokenCount?: number
cachedContentTokenCount?: number
}
export interface Part {
thought?: boolean
text?: string
}
+1 -1
View File
@@ -7,7 +7,7 @@ import { ApiHandler } from "../"
import { ApiHandlerOptions, geminiDefaultModelId, GeminiModelId, geminiModels, ModelInfo } from "@shared/api"
import { convertAnthropicMessageToGemini } from "../transform/gemini-format"
import { ApiStream } from "../transform/stream"
import { telemetryService } from "@services/posthog/telemetry/TelemetryService"
import { telemetryService } from "@services/posthog/PostHogClientProvider"
// Define a default TTL for the cache (e.g., 15 minutes in seconds)
const DEFAULT_CACHE_TTL_SECONDS = 900
+132
View File
@@ -0,0 +1,132 @@
import { ApiHandler } from ".."
import { huaweiCloudMaasDefaultModelId, HuaweiCloudMaasModelId, huaweiCloudMaasModels, 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 HuaweiCloudMaaSHandlerOptions {
huaweiCloudMaasApiKey?: string
huaweiCloudMaasModelId?: string
huaweiCloudMaasModelInfo?: ModelInfo
}
export class HuaweiCloudMaaSHandler implements ApiHandler {
private options: HuaweiCloudMaaSHandlerOptions
private client: OpenAI | undefined
constructor(options: HuaweiCloudMaaSHandlerOptions) {
this.options = options
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.huaweiCloudMaasApiKey) {
throw new Error("Huawei Cloud MaaS API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://api.modelarts-maas.com/v1/",
apiKey: this.options.huaweiCloudMaasApiKey,
})
} catch (error) {
throw new Error(`Error creating Huawei Cloud MaaS client: ${error.message}`)
}
}
return this.client
}
getModel(): { id: HuaweiCloudMaasModelId; info: ModelInfo } {
// First priority: huaweiCloudMaasModelId and huaweiCloudMaasModelInfo (like Groq does)
const huaweiCloudMaasModelId = this.options.huaweiCloudMaasModelId
const huaweiCloudMaasModelInfo = this.options.huaweiCloudMaasModelInfo
if (huaweiCloudMaasModelId && huaweiCloudMaasModelInfo) {
return { id: huaweiCloudMaasModelId as HuaweiCloudMaasModelId, info: huaweiCloudMaasModelInfo }
}
// Second priority: huaweiCloudMaasModelId with static model info
if (huaweiCloudMaasModelId && huaweiCloudMaasModelId in huaweiCloudMaasModels) {
const id = huaweiCloudMaasModelId as HuaweiCloudMaasModelId
return { id, info: huaweiCloudMaasModels[id] }
}
// Default fallback
return {
id: huaweiCloudMaasDefaultModelId,
info: huaweiCloudMaasModels[huaweiCloudMaasDefaultModelId],
}
}
@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 client.chat.completions.create({
model: model.id,
max_completion_tokens: model.info.maxTokens,
messages: openAiMessages,
stream: true,
stream_options: { include_usage: true },
temperature: 0,
})
let reasoning: string | null = null
let didOutputUsage: boolean = false
let finalUsage: any = null
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
// Handle reasoning content detection
if (delta?.content) {
if (reasoning || delta.content.includes("<think>")) {
reasoning = (reasoning || "") + delta.content
} else if (!reasoning) {
yield {
type: "text",
text: delta.content,
}
}
}
// Handle reasoning output
if (reasoning || (delta && "reasoning_content" in delta && delta.reasoning_content)) {
const reasoningContent = delta?.content || ((delta as any)?.reasoning_content as string | undefined) || ""
if (reasoningContent.trim()) {
yield {
type: "reasoning",
reasoning: reasoningContent,
}
}
// Check if reasoning is complete
if (reasoning?.includes("</think>")) {
reasoning = null
}
}
// Store usage information for later output
if (chunk.usage) {
finalUsage = chunk.usage
}
// Output usage when stream is finished
if (!didOutputUsage && chunk.choices?.[0]?.finish_reason) {
if (finalUsage) {
yield {
type: "usage",
inputTokens: finalUsage.prompt_tokens || 0,
outputTokens: finalUsage.completion_tokens || 0,
cacheWriteTokens: 0,
cacheReadTokens: 0,
}
}
didOutputUsage = true
}
}
}
}
+34 -76
View File
@@ -1,13 +1,13 @@
import { Anthropic } from "@anthropic-ai/sdk"
import * as vscode from "vscode"
import { ApiHandler, SingleCompletionHandler } from "../"
import { calculateApiCostAnthropic } from "@utils/cost"
import { ApiStream } from "@api/transform/stream"
import { convertToVsCodeLmMessages } from "@api/transform/vscode-lm-format"
import { SELECTOR_SEPARATOR, stringifyVsCodeLmModelSelector } from "@shared/vsCodeSelectorUtils"
import { ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
import type { LanguageModelChatSelector as LanguageModelChatSelectorFromTypes } from "./types"
import { SELECTOR_SEPARATOR, stringifyVsCodeLmModelSelector } from "@shared/vsCodeSelectorUtils"
import { calculateApiCostAnthropic } from "@utils/cost"
import * as vscode from "vscode"
import { ApiHandler, SingleCompletionHandler } from "../"
import { withRetry } from "../retry"
import type { LanguageModelChatSelector as LanguageModelChatSelectorFromTypes } from "./types"
interface VsCodeLmHandlerOptions {
vsCodeLmModelSelector?: any
@@ -237,82 +237,40 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
}
}
private async countTokens(text: string | vscode.LanguageModelChatMessage): Promise<number> {
// Check for required dependencies
if (!this.client) {
console.warn("Cline <Language Model API>: No client available for token counting")
return 0
}
if (!this.currentRequestCancellation) {
console.warn("Cline <Language Model API>: No cancellation token available for token counting")
return 0
}
// Validate input
if (!text) {
console.debug("Cline <Language Model API>: Empty text provided for token counting")
return 0
}
try {
// Handle different input types
let tokenCount: number
if (typeof text === "string") {
tokenCount = await this.client.countTokens(text, this.currentRequestCancellation.token)
} else if (text instanceof vscode.LanguageModelChatMessage) {
// For chat messages, ensure we have content
if (!text.content || (Array.isArray(text.content) && text.content.length === 0)) {
console.debug("Cline <Language Model API>: Empty chat message content")
return 0
}
tokenCount = await this.client.countTokens(text, this.currentRequestCancellation.token)
} else {
console.warn("Cline <Language Model API>: Invalid input type for token counting")
return 0
}
// Validate the result
if (typeof tokenCount !== "number") {
console.warn("Cline <Language Model API>: Non-numeric token count received:", tokenCount)
return 0
}
if (tokenCount < 0) {
console.warn("Cline <Language Model API>: Negative token count received:", tokenCount)
return 0
}
return tokenCount
} catch (error) {
// Handle specific error types
if (error instanceof vscode.CancellationError) {
console.debug("Cline <Language Model API>: Token counting cancelled by user")
return 0
}
const errorMessage = error instanceof Error ? error.message : "Unknown error"
console.warn("Cline <Language Model API>: Token counting failed:", errorMessage)
// Log additional error details if available
if (error instanceof Error && error.stack) {
console.debug("Token counting error stack:", error.stack)
}
return 0 // Fallback to prevent stream interruption
private extractTextFromMessage(message: vscode.LanguageModelChatMessage): string {
if (Array.isArray(message.content)) {
return message.content
.filter((part) => part instanceof vscode.LanguageModelTextPart)
.map((part) => (part as vscode.LanguageModelTextPart).value)
.join("")
}
return ""
}
private async calculateTotalInputTokens(
systemPrompt: string,
vsCodeLmMessages: vscode.LanguageModelChatMessage[],
): Promise<number> {
const systemTokens: number = await this.countTokens(systemPrompt)
private isClaudeModel(): boolean {
return this.client?.family?.startsWith("claude") || false
}
private async countTokens(text: string | vscode.LanguageModelChatMessage): Promise<number> {
/**
* NOTE (intentional trade-off):
* We use a coarse chars/4 heuristic here instead of a real tokenizer (e.g., js-tiktoken with o200k_base).
* Rationale:
* - Avoid pulling multiMB rank files and increasing the extension install/download size.
* - Eliminate encoder lifecycle/memory concerns in long-running sessions.
* Consequences:
* - This is not model-accurate and can under/over-estimate tokens, especially with tool/function calls.
* - It is “good enough” for budgeting/context checks, and we accept the inaccuracy by design.
* If precise accounting becomes a requirement, reintroduce a tokenizer behind a feature flag or backend-only path.
*/
const textContent = typeof text === "string" ? text : this.extractTextFromMessage(text)
return Math.ceil((textContent || "").length / 4)
}
private async calculateTotalInputTokens(vsCodeLmMessages: vscode.LanguageModelChatMessage[]): Promise<number> {
const messageTokens: number[] = await Promise.all(vsCodeLmMessages.map((msg) => this.countTokens(msg)))
return systemTokens + messageTokens.reduce((sum: number, tokens: number): number => sum + tokens, 0)
return messageTokens.reduce((sum: number, tokens: number): number => sum + tokens, 0)
}
private ensureCleanState(): void {
@@ -434,7 +392,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
this.currentRequestCancellation = new vscode.CancellationTokenSource()
// Calculate input tokens before starting the stream
const totalInputTokens: number = await this.calculateTotalInputTokens(systemPrompt, vsCodeLmMessages)
const totalInputTokens: number = await this.calculateTotalInputTokens(vsCodeLmMessages)
// Accumulate the text and count at the end of the stream to reduce token counting overhead.
let accumulatedText: string = ""
@@ -1,14 +1,13 @@
import { describe, it, beforeEach, afterEach } from "mocha"
import { HostProvider } from "@/hosts/host-provider"
import { setVscodeHostProviderMock } from "@/test/host-provider-test-utils"
import * as diskModule from "@core/storage/disk"
import { expect } from "chai"
import { afterEach, beforeEach, describe, it } from "mocha"
import * as path from "path"
import * as sinon from "sinon"
import * as vscode from "vscode"
import * as path from "path"
import type { FileMetadataEntry, TaskMetadata } from "./ContextTrackerTypes"
import { FileContextTracker } from "./FileContextTracker"
import * as diskModule from "@core/storage/disk"
import type { TaskMetadata, FileMetadataEntry } from "./ContextTrackerTypes"
import type { DiffViewProviderCreator, WebviewProviderCreator } from "@/hosts/host-provider"
import { HostProvider } from "@/hosts/host-provider"
import { vscodeHostBridgeClient } from "@/hosts/vscode/hostbridge/client/host-grpc-client"
describe("FileContextTracker", () => {
let sandbox: sinon.SinonSandbox
@@ -54,13 +53,7 @@ describe("FileContextTracker", () => {
getTaskMetadataStub = sandbox.stub(diskModule, "getTaskMetadata").resolves(mockTaskMetadata)
saveTaskMetadataStub = sandbox.stub(diskModule, "saveTaskMetadata").resolves()
// Reset HostProvider before initializing to avoid "already initialized" errors
HostProvider.reset()
HostProvider.initialize(
((_) => {}) as WebviewProviderCreator,
(() => {}) as DiffViewProviderCreator,
vscodeHostBridgeClient,
)
setVscodeHostProviderMock()
// Create tracker instance
taskId = "test-task-id"
@@ -1,6 +1,6 @@
import { Controller } from "../index"
import { AuthService } from "@/services/auth/AuthService"
import { EmptyRequest, String } from "../../../shared/proto/common"
import { EmptyRequest, String } from "@shared/proto/cline/common"
const authService = AuthService.getInstance()
@@ -1,6 +1,6 @@
import { AuthService } from "@/services/auth/AuthService"
import { Empty } from "../../../shared/proto/common"
import type { EmptyRequest } from "../../../shared/proto/common"
import { Empty } from "@shared/proto/cline/common"
import type { EmptyRequest } from "@shared/proto/cline/common"
import type { Controller } from "../index"
const authService = AuthService.getInstance()
@@ -1,4 +1,4 @@
import { AuthStateChangedRequest, AuthState } from "@shared/proto/account"
import { AuthStateChangedRequest, AuthState } from "@shared/proto/cline/account"
import type { Controller } from "../index"
import { updateGlobalState } from "../../storage/state"
@@ -1,5 +1,5 @@
import type { Controller } from "../index"
import { GetOrganizationCreditsRequest, OrganizationCreditsData, OrganizationUsageTransaction } from "@shared/proto/account"
import { GetOrganizationCreditsRequest, OrganizationCreditsData, OrganizationUsageTransaction } from "@shared/proto/cline/account"
/**
* Handles fetching all organization credits data (balance, usage, payments)
@@ -1,6 +1,6 @@
import type { Controller } from "../index"
import type { EmptyRequest } from "@shared/proto/common"
import { UserCreditsData } from "@shared/proto/account"
import type { EmptyRequest } from "@shared/proto/cline/common"
import { UserCreditsData } from "@shared/proto/cline/account"
/**
* Handles fetching all user credits data (balance, usage, payments)
@@ -1,7 +1,6 @@
import type { Controller } from "../index"
import type { EmptyRequest } from "@shared/proto/common"
import { UserOrganization, UserOrganizationsResponse } from "@shared/proto/account"
import { getGlobalState } from "../../storage/state"
import type { EmptyRequest } from "@shared/proto/cline/common"
import { UserOrganization, UserOrganizationsResponse } from "@shared/proto/cline/account"
/**
* Handles fetching all user credits data (balance, usage, payments)
@@ -18,14 +17,11 @@ export async function getUserOrganizations(controller: Controller, request: Empt
// Fetch user organizations from the account service
const organizations = await controller.accountService.fetchUserOrganizationsRPC()
// Get the current active organization ID from global state
const currentActiveOrganizationId = await getGlobalState(controller.context, "currentActiveOrganizationId")
return UserOrganizationsResponse.create({
organizations:
organizations?.map((org) =>
UserOrganization.create({
active: org.organizationId === currentActiveOrganizationId,
active: org.active,
memberId: org.memberId,
name: org.name,
organizationId: org.organizationId,
@@ -1,10 +1,6 @@
import type { Controller } from "../index"
import { Empty } from "@shared/proto/common"
import { UserOrganizationUpdateRequest } from "@shared/proto/account"
import { updateGlobalState } from "../../storage/state"
// Backend request deduplication tracking
const ongoingOrganizationSwitches = new Map<string, Promise<Empty>>()
import { Empty } from "@shared/proto/cline/common"
import { UserOrganizationUpdateRequest } from "@shared/proto/cline/account"
/**
* Handles setting the user's active organization
@@ -13,39 +9,16 @@ const ongoingOrganizationSwitches = new Map<string, Promise<Empty>>()
* @returns Empty response
*/
export async function setUserOrganization(controller: Controller, request: UserOrganizationUpdateRequest): Promise<Empty> {
// Create a unique key for this request (user + organization)
const userId = controller.context.globalState.get("clineUserId") || "unknown"
const requestKey = `${userId}:${request.organizationId || "personal"}`
// Check if there's already an ongoing request for this user+organization combination
const existingRequest = ongoingOrganizationSwitches.get(requestKey)
if (existingRequest) {
return existingRequest
}
// Create the promise for this request
const requestPromise = (async (): Promise<Empty> => {
try {
if (!controller.accountService) {
throw new Error("Account service not available")
}
await controller.accountService.switchAccount(request.organizationId)
// Store the current active organization ID in global state
await updateGlobalState(controller.context, "currentActiveOrganizationId", request.organizationId)
return Empty.create({})
} catch (error) {
throw error
} finally {
// Clean up the ongoing request tracking
ongoingOrganizationSwitches.delete(requestKey)
try {
if (!controller.accountService) {
throw new Error("Account service not available")
}
})()
// Store the promise to prevent duplicate requests
ongoingOrganizationSwitches.set(requestKey, requestPromise)
// Switch to the specified organization using the account service
await controller.accountService.switchAccount(request.organizationId)
return requestPromise
return Empty.create({})
} catch (error) {
throw error
}
}
@@ -1,5 +1,5 @@
import { BrowserConnection } from "@shared/proto/browser"
import { EmptyRequest } from "@shared/proto/common"
import { BrowserConnection } from "@shared/proto/cline/browser"
import { EmptyRequest } from "@shared/proto/cline/common"
import { Controller } from "../index"
import { getAllExtensionState } from "@core/storage/state"
import { BrowserSession } from "@services/browser/BrowserSession"
@@ -1,5 +1,5 @@
import { BrowserConnectionInfo } from "@shared/proto/browser"
import { EmptyRequest } from "@shared/proto/common"
import { BrowserConnectionInfo } from "@shared/proto/cline/browser"
import { EmptyRequest } from "@shared/proto/cline/common"
import { Controller } from "../index"
import { getAllExtensionState } from "@core/storage/state"
@@ -1,5 +1,5 @@
import { ChromePath } from "../../../shared/proto/browser"
import { EmptyRequest } from "../../../shared/proto/common"
import { ChromePath } from "@shared/proto/cline/browser"
import { EmptyRequest } from "@shared/proto/cline/common"
import { Controller } from "../index"
import { getAllExtensionState } from "../../storage/state"
import { BrowserSession } from "../../../services/browser/BrowserSession"
@@ -1,4 +1,4 @@
import { EmptyRequest, String as StringMessage } from "../../../shared/proto/common"
import { EmptyRequest, String as StringMessage } from "@shared/proto/cline/common"
import { Controller } from "../index"
import { BrowserSession } from "../../../services/browser/BrowserSession"
@@ -1,5 +1,5 @@
import { BrowserConnection } from "@shared/proto/browser"
import { StringRequest } from "@shared/proto/common"
import { BrowserConnection } from "@shared/proto/cline/browser"
import { StringRequest } from "@shared/proto/cline/common"
import { Controller } from "../index"
import { getAllExtensionState } from "@core/storage/state"
import { BrowserSession } from "@services/browser/BrowserSession"
@@ -1,5 +1,5 @@
import { UpdateBrowserSettingsRequest } from "../../../shared/proto/browser"
import { Boolean } from "../../../shared/proto/common"
import { UpdateBrowserSettingsRequest } from "@shared/proto/cline/browser"
import { Boolean } from "@shared/proto/cline/common"
import { Controller } from "../index"
import { updateGlobalState, getGlobalState } from "../../storage/state"
import { BrowserSettings as SharedBrowserSettings, DEFAULT_BROWSER_SETTINGS } from "../../../shared/BrowserSettings"
@@ -1,5 +1,5 @@
import { Controller } from ".."
import { Empty, Int64Request } from "@shared/proto/common"
import { Empty, Int64Request } from "@shared/proto/cline/common"
export async function checkpointDiff(controller: Controller, request: Int64Request): Promise<Empty> {
if (request.value) {
@@ -1,8 +1,8 @@
import { HostProvider } from "@/hosts/host-provider"
import { Controller } from ".."
import { ClineCheckpointRestore } from "../../../shared/WebviewMessage"
import { CheckpointRestoreRequest } from "../../../shared/proto/checkpoints"
import { Empty } from "../../../shared/proto/common"
import { CheckpointRestoreRequest } from "@shared/proto/cline/checkpoints"
import { Empty } from "@shared/proto/cline/common"
import pWaitFor from "p-wait-for"
import { ShowMessageType } from "@/shared/proto/index.host"
+1 -1
View File
@@ -1,5 +1,5 @@
import { Controller } from ".."
import { Empty, StringRequest } from "@shared/proto/common"
import { Empty, StringRequest } from "@shared/proto/cline/common"
import { writeTextToClipboard } from "@/utils/env"
/**
+1 -1
View File
@@ -1,5 +1,5 @@
import { Controller } from ".."
import { RuleFileRequest, RuleFile } from "@shared/proto/file"
import { RuleFileRequest, RuleFile } from "@shared/proto/cline/file"
import { refreshClineRulesToggles } from "@core/context/instructions/user-instructions/cline-rules"
import { createRuleFile as createRuleFileImpl } from "@core/context/instructions/user-instructions/rule-helpers"
import * as path from "path"
+1 -1
View File
@@ -1,5 +1,5 @@
import { deleteRuleFile as deleteRuleFileImpl } from "@core/context/instructions/user-instructions/rule-helpers"
import { RuleFile, RuleFileRequest } from "@shared/proto/file"
import { RuleFile, RuleFileRequest } from "@shared/proto/cline/file"
import * as path from "path"
import { Controller } from ".."
import { HostProvider } from "@/hosts/host-provider"
+1 -1
View File
@@ -1,5 +1,5 @@
import { asRelativePath } from "@/utils/path"
import { RelativePaths, RelativePathsRequest } from "@shared/proto/file"
import { RelativePaths, RelativePathsRequest } from "@shared/proto/cline/file"
import * as path from "path"
import { URI } from "vscode-uri"
import { Controller } from ".."
+1 -1
View File
@@ -1,5 +1,5 @@
import { Controller } from ".."
import { Empty, StringRequest } from "@shared/proto/common"
import { Empty, StringRequest } from "@shared/proto/cline/common"
import { openFile as openFileIntegration } from "@integrations/misc/open-file"
/**
+1 -1
View File
@@ -1,5 +1,5 @@
import { Controller } from ".."
import { Empty, StringRequest } from "@shared/proto/common"
import { Empty, StringRequest } from "@shared/proto/cline/common"
import { openImage as openImageIntegration } from "@integrations/misc/open-file"
/**
+1 -1
View File
@@ -1,5 +1,5 @@
import { Controller } from ".."
import { Empty, StringRequest } from "../../../shared/proto/common"
import { Empty, StringRequest } from "@shared/proto/cline/common"
import { openMention as coreOpenMention } from "../../mentions"
/**
+1 -1
View File
@@ -1,5 +1,5 @@
import { Controller } from ".."
import { Empty, StringRequest } from "@shared/proto/common"
import { Empty, StringRequest } from "@shared/proto/cline/common"
import { openFile as openFileIntegration } from "@integrations/misc/open-file"
import path from "path"
/**
+2 -2
View File
@@ -1,5 +1,5 @@
import { EmptyRequest } from "@shared/proto/common"
import { RefreshedRules } from "@shared/proto/file"
import { EmptyRequest } from "@shared/proto/cline/common"
import { RefreshedRules } from "@shared/proto/cline/file"
import type { Controller } from "../index"
import { refreshClineRulesToggles } from "@core/context/instructions/user-instructions/cline-rules"
import { refreshExternalRulesToggles } from "@core/context/instructions/user-instructions/external-rules"
+2 -2
View File
@@ -1,6 +1,6 @@
import { Controller } from ".."
import { GitCommits } from "@shared/proto/file"
import { StringRequest } from "@shared/proto/common"
import { GitCommits } from "@shared/proto/cline/file"
import { StringRequest } from "@shared/proto/cline/common"
import { searchCommits as searchCommitsUtil } from "@utils/git"
import { getWorkspacePath } from "@utils/path"
import { convertGitCommitsToProtoGitCommits } from "@shared/proto-conversions/file/git-commit-conversion"
+1 -1
View File
@@ -1,5 +1,5 @@
import { Controller } from ".."
import { FileSearchRequest, FileSearchResults } from "@shared/proto/file"
import { FileSearchRequest, FileSearchResults } from "@shared/proto/cline/file"
import { searchWorkspaceFiles } from "@services/search/file-search"
import { getWorkspacePath } from "@utils/path"
import { convertSearchResultsToProtoFileInfos } from "@shared/proto-conversions/file/search-result-conversion"
+1 -1
View File
@@ -1,5 +1,5 @@
import { Controller } from ".."
import { BooleanRequest, StringArrays } from "@shared/proto/common"
import { BooleanRequest, StringArrays } from "@shared/proto/cline/common"
import { selectFiles as selectFilesIntegration } from "@integrations/misc/process-files"
/**
@@ -1,5 +1,5 @@
import { Controller } from "../index"
import { EmptyRequest, StringArray } from "@shared/proto/common"
import { EmptyRequest, StringArray } from "@shared/proto/cline/common"
import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler"
// Keep track of active subscriptions
+2 -2
View File
@@ -1,5 +1,5 @@
import { ToggleClineRules } from "../../../shared/proto/file"
import type { ToggleClineRuleRequest } from "../../../shared/proto/file"
import { ToggleClineRules } from "@shared/proto/cline/file"
import type { ToggleClineRuleRequest } from "@shared/proto/cline/file"
import type { Controller } from "../index"
import { getGlobalState, getWorkspaceState, updateGlobalState, updateWorkspaceState } from "../../../core/storage/state"
import { ClineRulesToggles as AppClineRulesToggles } from "@shared/cline-rules"
+2 -2
View File
@@ -1,5 +1,5 @@
import type { ToggleCursorRuleRequest } from "../../../shared/proto/file"
import { ClineRulesToggles } from "../../../shared/proto/file"
import type { ToggleCursorRuleRequest } from "@shared/proto/cline/file"
import { ClineRulesToggles } from "@shared/proto/cline/file"
import type { Controller } from "../index"
import { getWorkspaceState, updateWorkspaceState } from "../../../core/storage/state"
import { ClineRulesToggles as AppClineRulesToggles } from "@shared/cline-rules"
@@ -1,5 +1,5 @@
import type { ToggleWindsurfRuleRequest } from "../../../shared/proto/file"
import { ClineRulesToggles } from "../../../shared/proto/file"
import type { ToggleWindsurfRuleRequest } from "@shared/proto/cline/file"
import { ClineRulesToggles } from "@shared/proto/cline/file"
import type { Controller } from "../index"
import { getWorkspaceState, updateWorkspaceState } from "../../../core/storage/state"
import { ClineRulesToggles as AppClineRulesToggles } from "@shared/cline-rules"
+2 -2
View File
@@ -1,6 +1,6 @@
import { Controller } from ".."
import { Metadata } from "../../../shared/proto/common"
import { ToggleWorkflowRequest, ClineRulesToggles } from "../../../shared/proto/file"
import { Metadata } from "@shared/proto/cline/common"
import { ToggleWorkflowRequest, ClineRulesToggles } from "@shared/proto/cline/file"
import { getWorkspaceState, updateWorkspaceState, getGlobalState, updateGlobalState } from "../../../core/storage/state"
import { ClineRulesToggles as AppClineRulesToggles } from "../../../shared/cline-rules"
+412
View File
@@ -0,0 +1,412 @@
import { describe, it, beforeEach, afterEach } from "mocha"
import { expect } from "chai"
import * as sinon from "sinon"
import { handleGrpcRequest, handleGrpcRequestCancel, getRequestRegistry } from "./grpc-handler"
import { Controller } from "@core/controller"
import { GrpcRequest, GrpcCancel } from "@shared/WebviewMessage"
import { serviceHandlers } from "@generated/hosts/vscode/protobus-services"
describe("grpc-handler", () => {
let sandbox: sinon.SinonSandbox
let mockController: sinon.SinonStubbedInstance<Controller>
let mockUnaryHandler: sinon.SinonStub
let mockUnaryFailingHandler: sinon.SinonStub
let mockStreamingHandler: sinon.SinonStub
let mockStreamingFailingHandler: sinon.SinonStub
const serviceName = "cline.TestService"
const mockResponse = { result: "result-1234" }
beforeEach(() => {
sandbox = sinon.createSandbox()
// Create a mock controller
mockController = {
postMessageToWebview: sandbox.stub().resolves(),
} as any
// Create mock service handlers
mockUnaryHandler = sandbox.stub().resolves(mockResponse)
mockStreamingHandler = sandbox.stub().resolves()
mockUnaryFailingHandler = sandbox.stub().rejects(new Error("Test error unary"))
mockStreamingFailingHandler = sandbox.stub().rejects(new Error("Stream error"))
serviceHandlers[serviceName] = {
testUnary: mockUnaryHandler,
testUnaryFailing: mockUnaryFailingHandler,
testStreaming: mockStreamingHandler,
testStreamingFailing: mockStreamingFailingHandler,
}
})
afterEach(() => {
sandbox.restore()
})
describe("handleGrpcRequest", () => {
describe("Unary requests", () => {
it("should handle successful unary requests", async () => {
const request: GrpcRequest = {
service: serviceName,
method: "testUnary",
message: { input: "test" },
request_id: "test-123",
is_streaming: false,
}
await handleGrpcRequest(mockController as any, request)
// Verify the handler was called
expect(mockUnaryHandler.calledOnce).to.be.true
expect(mockUnaryHandler.firstCall.args[0]).to.equal(mockController)
expect(mockUnaryHandler.firstCall.args[1]).to.deep.equal({ input: "test" })
// Verify the response was sent
expect(mockController.postMessageToWebview.calledOnce).to.be.true
const sentMessage = mockController.postMessageToWebview.firstCall.args[0]
expect(sentMessage).to.deep.equal({
type: "grpc_response",
grpc_response: {
message: mockResponse,
request_id: "test-123",
},
})
})
it("should handle errors in unary requests", async () => {
const request: GrpcRequest = {
service: serviceName,
method: "testUnaryFailing",
message: { input: "test" },
request_id: "test-456",
is_streaming: false,
}
await handleGrpcRequest(mockController as any, request)
// Verify the error response was sent
expect(mockController.postMessageToWebview.calledOnce).to.be.true
const sentMessage = mockController.postMessageToWebview.firstCall.args[0]
expect(sentMessage).to.deep.equal({
type: "grpc_response",
grpc_response: {
error: "Test error unary",
request_id: "test-456",
is_streaming: false,
},
})
})
it("should handle unknown service errors", async () => {
const request: GrpcRequest = {
service: "UnknownService",
method: "someMethod",
message: {},
request_id: "test-789",
is_streaming: false,
}
await handleGrpcRequest(mockController as any, request)
// Verify the error response was sent
expect(mockController.postMessageToWebview.calledOnce).to.be.true
const sentMessage = mockController.postMessageToWebview.firstCall.args[0]
expect(sentMessage.type).to.equal("grpc_response")
expect(sentMessage.grpc_response?.error).to.include("Unknown service: UnknownService")
expect(sentMessage.grpc_response?.request_id).to.equal("test-789")
})
it("should handle unknown method errors", async () => {
const request: GrpcRequest = {
service: serviceName,
method: "unknownMethod",
message: {},
request_id: "test-999",
is_streaming: false,
}
await handleGrpcRequest(mockController as any, request)
// Verify the error response was sent
expect(mockController.postMessageToWebview.calledOnce).to.be.true
const sentMessage = mockController.postMessageToWebview.firstCall.args[0]
expect(sentMessage.type).to.equal("grpc_response")
expect(sentMessage.grpc_response?.error).to.include("Unknown rpc: cline.TestService.unknownMethod")
expect(sentMessage.grpc_response?.request_id).to.equal("test-999")
})
})
describe("Streaming requests", () => {
it("should handle successful streaming requests", async () => {
// Set up a streaming handler that sends multiple responses
const request: GrpcRequest = {
service: serviceName,
method: "testStreaming",
message: { input: "stream" },
request_id: "stream-123",
is_streaming: true,
}
// Reset the mock and set up the handler using callsFake
mockStreamingHandler.reset()
mockStreamingHandler.callsFake(async (controller: any, message: any, responseStream: any, requestId: string) => {
// Simulate streaming multiple messages
await responseStream({ value: 1 }, false, 0)
await responseStream({ value: 2 }, false, 1)
await responseStream({ value: 3 }, true, 2) // Last message
})
await handleGrpcRequest(mockController as any, request)
// Verify the handler was called
expect(mockStreamingHandler.calledOnce).to.be.true
expect(mockStreamingHandler.firstCall.args[0]).to.equal(mockController)
expect(mockStreamingHandler.firstCall.args[1]).to.deep.equal({ input: "stream" })
expect(mockStreamingHandler.firstCall.args[3]).to.equal("stream-123")
// Verify all streaming responses were sent
expect(mockController.postMessageToWebview.callCount).to.equal(3)
// Check all responses
expect(mockController.postMessageToWebview.firstCall.args[0]).to.deep.equal({
type: "grpc_response",
grpc_response: {
message: { value: 1 },
request_id: "stream-123",
is_streaming: true,
sequence_number: 0,
},
})
expect(mockController.postMessageToWebview.secondCall.args[0]).to.deep.equal({
type: "grpc_response",
grpc_response: {
message: { value: 2 },
request_id: "stream-123",
is_streaming: true,
sequence_number: 1,
},
})
expect(mockController.postMessageToWebview.thirdCall.args[0]).to.deep.equal({
type: "grpc_response",
grpc_response: {
message: { value: 3 },
request_id: "stream-123",
is_streaming: false, // Last message has is_streaming: false
sequence_number: 2,
},
})
})
it("should handle errors in streaming requests", async () => {
const request: GrpcRequest = {
service: serviceName,
method: "testStreamingFailing",
message: { input: "stream" },
request_id: "stream-456",
is_streaming: true,
}
await handleGrpcRequest(mockController as any, request)
// Verify the error response was sent
expect(mockController.postMessageToWebview.calledOnce).to.be.true
const sentMessage = mockController.postMessageToWebview.firstCall.args[0]
expect(sentMessage).to.deep.equal({
type: "grpc_response",
grpc_response: {
error: "Stream error",
request_id: "stream-456",
is_streaming: false,
},
})
})
it("should handle streaming with message, error, then another message", async () => {
// This test simulates a scenario where:
// 1. First message is sent successfully
// 2. An error occurs
// 3. Another message is attempted (which should not be sent after error)
const request: GrpcRequest = {
service: serviceName,
method: "testStreaming",
message: { input: "stream-with-error" },
request_id: "stream-error-mid",
is_streaming: true,
}
// Reset the mock and set up the handler to throw an error after being called
mockStreamingHandler.reset()
mockStreamingHandler.callsFake(async (controller: any, message: any, responseStream: any, requestId: string) => {
// Send first message successfully
await responseStream({ value: "first" }, false, 0)
// Throw an error
throw new Error("Mid-stream error")
})
await handleGrpcRequest(mockController as any, request)
// Verify the handler was called
expect(mockStreamingHandler.calledOnce).to.be.true
// Verify that we got the first message and then the error
expect(mockController.postMessageToWebview.callCount).to.equal(2)
// Check first message was sent successfully
expect(mockController.postMessageToWebview.firstCall.args[0]).to.deep.equal({
type: "grpc_response",
grpc_response: {
message: { value: "first" },
request_id: "stream-error-mid",
is_streaming: true,
sequence_number: 0,
},
})
// Check error response was sent
expect(mockController.postMessageToWebview.secondCall.args[0]).to.deep.equal({
type: "grpc_response",
grpc_response: {
error: "Mid-stream error",
request_id: "stream-error-mid",
is_streaming: false,
},
})
// Try to send another message after the error (simulating what might happen
// if the handler tried to continue after an error)
const responseStream = mockStreamingHandler.firstCall.args[2]
// This should still work as the responseStream function is still valid
await responseStream({ value: "after-error" }, false, 1)
// Verify we now have 3 total calls (first message, error, after-error message)
expect(mockController.postMessageToWebview.callCount).to.equal(3)
// Verify the message after error was still sent
// (In a real scenario, the handler would have stopped due to the error,
// but this tests that the responseStream function itself still works)
expect(mockController.postMessageToWebview.thirdCall.args[0]).to.deep.equal({
type: "grpc_response",
grpc_response: {
message: { value: "after-error" },
request_id: "stream-error-mid",
is_streaming: true,
sequence_number: 1,
},
})
})
})
describe("handleGrpcRequestCancel", () => {
it("should cancel an active request", async () => {
// Register a request in the registry
const registry = getRequestRegistry()
const cleanupStub = sandbox.stub()
registry.registerRequest("cancel-123", cleanupStub)
const cancelRequest: GrpcCancel = {
request_id: "cancel-123",
}
await handleGrpcRequestCancel(mockController as any, cancelRequest)
// Verify the cleanup was called
expect(cleanupStub.calledOnce).to.be.true
// Verify the cancellation confirmation was sent
expect(mockController.postMessageToWebview.calledOnce).to.be.true
const sentMessage = mockController.postMessageToWebview.firstCall.args[0]
expect(sentMessage).to.deep.equal({
type: "grpc_response",
grpc_response: {
message: { cancelled: true },
request_id: "cancel-123",
is_streaming: false,
},
})
// Verify the request was removed from the registry
expect(registry.hasRequest("cancel-123")).to.be.false
})
it("should handle cancellation of non-existent request", async () => {
const cancelRequest: GrpcCancel = {
request_id: "non-existent",
}
await handleGrpcRequestCancel(mockController as any, cancelRequest)
// Verify no message was sent (request not found)
expect(mockController.postMessageToWebview.called).to.be.false
})
it("should handle cleanup errors gracefully", async () => {
// Register a request with a failing cleanup
const registry = getRequestRegistry()
const cleanupStub = sandbox.stub().throws(new Error("Cleanup failed"))
registry.registerRequest("cancel-error", cleanupStub)
const cancelRequest: GrpcCancel = {
request_id: "cancel-error",
}
// Should not throw
await handleGrpcRequestCancel(mockController as any, cancelRequest)
// Verify the cleanup was attempted
expect(cleanupStub.calledOnce).to.be.true
// Verify the cancellation confirmation was still sent
expect(mockController.postMessageToWebview.calledOnce).to.be.true
// Verify the request was removed despite the error
expect(registry.hasRequest("cancel-error")).to.be.false
})
})
describe("Concurrent requests", () => {
it("should handle concurrent requests", async () => {
// Set up handlers
mockUnaryHandler.resolves({ result: "unary" })
mockStreamingHandler.callsFake(async (controller: any, message: any, responseStream: any) => {
await responseStream({ value: "stream1" }, false, 0)
await responseStream({ value: "stream2" }, true, 1)
})
// Send multiple requests concurrently
const requests = [
handleGrpcRequest(mockController as any, {
service: serviceName,
method: "testUnary",
message: { id: 1 },
request_id: "concurrent-1",
is_streaming: false,
}),
handleGrpcRequest(mockController as any, {
service: serviceName,
method: "testStreaming",
message: { id: 2 },
request_id: "concurrent-2",
is_streaming: true,
}),
handleGrpcRequest(mockController as any, {
service: serviceName,
method: "testUnary",
message: { id: 3 },
request_id: "concurrent-3",
is_streaming: false,
}),
]
await Promise.all(requests)
// Verify all handlers were called
expect(mockUnaryHandler.callCount).to.equal(2)
expect(mockStreamingHandler.callCount).to.equal(1)
// Verify all responses were sent (2 unary + 2 streaming)
expect(mockController.postMessageToWebview.callCount).to.equal(4)
})
})
})
})
+81 -152
View File
@@ -1,6 +1,7 @@
import { Controller } from "./index"
import { serviceHandlers } from "@generated/hosts/vscode/protobus-services"
import { GrpcRequestRegistry } from "./grpc-request-registry"
import { GrpcCancel, GrpcRequest } from "@/shared/WebviewMessage"
/**
* Type definition for a streaming response handler
@@ -12,152 +13,34 @@ export type StreamingResponseHandler<TResponse> = (
) => Promise<void>
/**
* Handles gRPC requests from the webview
* Handles a gRPC request from the webview.
*/
export class GrpcHandler {
constructor(private controller: Controller) {}
/**
* Handle a gRPC request from the webview
* @param service The service name
* @param method The method name
* @param message The request message
* @param requestId The request ID for response correlation
* @param isStreaming Whether this is a streaming request
* @returns The response message or error for unary requests, void for streaming requests
*/
async handleRequest(
service: string,
method: string,
message: any,
requestId: string,
isStreaming: boolean = false,
): Promise<{
message?: any
error?: string
request_id: string
} | void> {
try {
// If this is a streaming request, use the streaming handler
if (isStreaming) {
await this.handleStreamingRequest(service, method, message, requestId)
return
}
// Get the service handler from the config
const handler = getHandler(service, method)
// Handle unary request
return {
message: await handler(this.controller, message),
request_id: requestId,
}
} catch (error) {
return {
error: error instanceof Error ? error.message : String(error),
request_id: requestId,
}
}
}
/**
* Handle a streaming gRPC request
* @param service The service name
* @param method The method name
* @param message The request message
* @param requestId The request ID for response correlation
*/
private async handleStreamingRequest(service: string, method: string, message: any, requestId: string): Promise<void> {
// Create a response stream function
const responseStream: StreamingResponseHandler<any> = async (
response: any,
isLast: boolean = false,
sequenceNumber?: number,
) => {
await this.controller.postMessageToWebview({
type: "grpc_response",
grpc_response: {
message: response,
request_id: requestId,
is_streaming: !isLast,
sequence_number: sequenceNumber,
},
})
}
try {
// Get the service handler from the config
const handler = getHandler(service, method)
// Handle streaming request and pass the requestId to all streaming handlers
await handler(this.controller, message, responseStream, requestId)
// Don't send a final message here - the stream should stay open for future updates
// The stream will be closed when the client disconnects or when the service explicitly ends it
} catch (error) {
// Send error response
console.log("Protobus error:", error)
await this.controller.postMessageToWebview({
type: "grpc_response",
grpc_response: {
error: error instanceof Error ? error.message : String(error),
request_id: requestId,
is_streaming: false,
},
})
}
export async function handleGrpcRequest(controller: Controller, request: GrpcRequest): Promise<void> {
if (request.is_streaming) {
await handleStreamingRequest(controller, request)
} else {
await handleUnaryRequest(controller, request)
}
}
// Registry to track active gRPC requests and their cleanup functions
const requestRegistry = new GrpcRequestRegistry()
/**
* Handle a gRPC request from the webview
* @param controller The controller instance
* @param request The gRPC request
* Handles a gRPC unary request from the webview.
*
* Calls the handler using the service and method name, and then posts the result back to the webview.
*/
export async function handleGrpcRequest(
controller: Controller,
request: {
service: string
method: string
message: any
request_id: string
is_streaming?: boolean
},
) {
async function handleUnaryRequest(controller: Controller, request: GrpcRequest): Promise<void> {
try {
const grpcHandler = new GrpcHandler(controller)
// For streaming requests, handleRequest handles sending responses directly
if (request.is_streaming) {
try {
await grpcHandler.handleRequest(request.service, request.method, request.message, request.request_id, true)
} finally {
// Note: We don't automatically clean up here anymore
// The request will be cleaned up when it completes or is cancelled
}
return
}
// For unary requests, we get a response and send it back
const response = (await grpcHandler.handleRequest(
request.service,
request.method,
request.message,
request.request_id,
false,
)) as {
message?: any
error?: string
request_id: string
}
// Send the response back to the webview
// Get the service handler from the config
const handler = getHandler(request.service, request.method)
// Handle unary request
const response = await handler(controller, request.message)
// Send response to the webview
await controller.postMessageToWebview({
type: "grpc_response",
grpc_response: response,
grpc_response: {
message: response,
request_id: request.request_id,
},
})
} catch (error) {
// Send error response
@@ -167,22 +50,65 @@ export async function handleGrpcRequest(
grpc_response: {
error: error instanceof Error ? error.message : String(error),
request_id: request.request_id,
is_streaming: false,
},
})
}
}
/**
* Handle a gRPC request cancellation from the webview
* Handle a streaming gRPC request from the webview.
*
* Calls the handler using the service and method name, and creates a streaming response handler
* which posts results back to the webview.
*/
async function handleStreamingRequest(controller: Controller, request: GrpcRequest): Promise<void> {
// Create a response stream function
const responseStream: StreamingResponseHandler<any> = async (
response: any,
isLast: boolean = false,
sequenceNumber?: number,
) => {
await controller.postMessageToWebview({
type: "grpc_response",
grpc_response: {
message: response,
request_id: request.request_id,
is_streaming: !isLast,
sequence_number: sequenceNumber,
},
})
}
try {
// Get the service handler from the config
const handler = getHandler(request.service, request.method)
// Handle streaming request and pass the requestId to all streaming handlers
await handler(controller, request.message, responseStream, request.request_id)
// Don't send a final message here - the stream should stay open for future updates
// The stream will be closed when the client disconnects or when the service explicitly ends it
} catch (error) {
// Send error response
console.log("Protobus error:", error)
await controller.postMessageToWebview({
type: "grpc_response",
grpc_response: {
error: error instanceof Error ? error.message : String(error),
request_id: request.request_id,
is_streaming: false,
},
})
}
}
/**
* Handles a gRPC request cancellation from the webview.
* @param controller The controller instance
* @param request The cancellation request
*/
export async function handleGrpcRequestCancel(
controller: Controller,
request: {
request_id: string
},
) {
export async function handleGrpcRequestCancel(controller: Controller, request: GrpcCancel) {
const cancelled = requestRegistry.cancelRequest(request.request_id)
if (cancelled) {
@@ -200,6 +126,17 @@ export async function handleGrpcRequestCancel(
}
}
// Registry to track active gRPC requests and their cleanup functions
const requestRegistry = new GrpcRequestRegistry()
/**
* Get the request registry instance
* This allows other parts of the code to access the registry
*/
export function getRequestRegistry(): GrpcRequestRegistry {
return requestRegistry
}
function getHandler(serviceName: string, methodName: string): any {
// Get the service handler from the config
const serviceConfig = serviceHandlers[serviceName]
@@ -212,11 +149,3 @@ function getHandler(serviceName: string, methodName: string): any {
}
return handler
}
/**
* Get the request registry instance
* This allows other parts of the code to access the registry
*/
export function getRequestRegistry(): GrpcRequestRegistry {
return requestRegistry
}
+135 -145
View File
@@ -1,7 +1,7 @@
import { clineEnvConfig } from "@/config"
import { HostProvider } from "@/hosts/host-provider"
import { AuthService } from "@/services/auth/AuthService"
import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
import { PostHogClientProvider, telemetryService } from "@/services/posthog/PostHogClientProvider"
import { ShowMessageType } from "@/shared/proto/host/window"
import { getCwd, getDesktopDir } from "@/utils/path"
import { Anthropic } from "@anthropic-ai/sdk"
@@ -13,7 +13,7 @@ import { ClineAccountService } from "@services/account/ClineAccountService"
import { McpHub } from "@services/mcp/McpHub"
import { ApiProvider, ModelInfo } from "@shared/api"
import { ChatContent } from "@shared/ChatContent"
import { ChatSettings, Mode, StoredChatSettings } from "@shared/ChatSettings"
import { Mode } from "@shared/storage/types"
import { ClineRulesToggles } from "@shared/cline-rules"
import { ExtensionMessage, ExtensionState, Platform } from "@shared/ExtensionMessage"
import { HistoryItem } from "@shared/HistoryItem"
@@ -30,12 +30,13 @@ import * as path from "path"
import * as vscode from "vscode"
import { ensureMcpServersDirectoryExists, ensureSettingsDirectoryExists, GlobalFileNames } from "../storage/disk"
import { getAllExtensionState, getGlobalState, getWorkspaceState, storeSecret, updateGlobalState } from "../storage/state"
import { CacheService, PersistenceErrorEvent } from "../storage/CacheService"
import { Task } from "../task"
import { handleGrpcRequest, handleGrpcRequestCancel } from "./grpc-handler"
import { sendMcpMarketplaceCatalogEvent } from "./mcp/subscribeToMcpMarketplaceCatalog"
import { sendStateUpdate } from "./state/subscribeToState"
import { sendAddToInputEvent } from "./ui/subscribeToAddToInput"
import { getLatestAnnouncementId } from "@/utils/announcements"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -52,34 +53,59 @@ export class Controller {
workspaceTracker: WorkspaceTracker
mcpHub: McpHub
accountService: ClineAccountService
authService: AuthService
get latestAnnouncementId(): string {
return this.context.extension?.packageJSON?.version?.split(".").slice(0, 2).join(".") ?? ""
}
readonly cacheService: CacheService
constructor(
readonly context: vscode.ExtensionContext,
private readonly outputChannel: vscode.OutputChannel,
postMessage: (message: ExtensionMessage) => Thenable<boolean> | undefined,
id: string,
) {
this.id = id
this.outputChannel.appendLine("ClineProvider instantiated")
HostProvider.get().logToChannel("ClineProvider instantiated")
this.postMessage = postMessage
this.accountService = ClineAccountService.getInstance()
this.cacheService = new CacheService(context)
const authService = AuthService.getInstance(this)
// Initialize cache service asynchronously - critical for extension functionality
this.cacheService
.initialize()
.then(() => {
authService.restoreRefreshTokenAndRetrieveAuthInfo()
})
.catch((error) => {
console.error("CRITICAL: Failed to initialize CacheService - extension may not function properly:", error)
})
// Set up persistence error recovery
this.cacheService.onPersistenceError = async ({ error }: PersistenceErrorEvent) => {
console.error("Cache persistence failed, recovering:", error)
try {
await this.cacheService.reInitialize()
await this.postStateToWebview()
HostProvider.window.showMessage({
type: ShowMessageType.WARNING,
message: "Saving settings to storage failed.",
})
} catch (recoveryError) {
console.error("Cache recovery failed:", recoveryError)
HostProvider.window.showMessage({
type: ShowMessageType.ERROR,
message: "Failed to save settings. Please restart the extension.",
})
}
}
this.workspaceTracker = new WorkspaceTracker()
this.mcpHub = new McpHub(
() => ensureMcpServersDirectoryExists(),
() => ensureSettingsDirectoryExists(this.context),
(msg) => this.postMessageToWebview(msg),
this.context.extension?.packageJSON?.version ?? "1.0.0",
)
this.accountService = ClineAccountService.getInstance()
this.authService = AuthService.getInstance(context)
this.authService.restoreRefreshTokenAndRetrieveAuthInfo()
// Clean up legacy checkpoints
cleanupLegacyCheckpoints(this.context.globalStorageUri.fsPath, this.outputChannel).catch((error) => {
cleanupLegacyCheckpoints(this.context.globalStorageUri.fsPath).catch((error) => {
console.error("Failed to cleanup legacy checkpoints:", error)
})
}
@@ -111,12 +137,18 @@ export class Controller {
async handleSignOut() {
try {
// TODO: update to clineAccountId and then move clineApiKey to a clear function.
await storeSecret(this.context, "clineAccountId", undefined)
this.cacheService.setSecret("clineAccountId", undefined)
await updateGlobalState(this.context, "userInfo", undefined)
await Promise.all([
updateGlobalState(this.context, "planModeApiProvider", "openrouter"),
updateGlobalState(this.context, "actModeApiProvider", "openrouter"),
])
// Update API providers through cache service
const apiConfiguration = this.cacheService.getApiConfiguration()
const updatedConfig = {
...apiConfiguration,
planModeApiProvider: "openrouter" as ApiProvider,
actModeApiProvider: "openrouter" as ApiProvider,
}
this.cacheService.setApiConfiguration(updatedConfig)
await this.postStateToWebview()
HostProvider.window.showMessage({
type: ShowMessageType.INFORMATION,
@@ -136,11 +168,16 @@ export class Controller {
async initTask(task?: string, images?: string[], files?: string[], historyItem?: HistoryItem) {
await this.clearTask() // ensures that an existing task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one
// Get API configuration from cache for immediate access
const apiConfiguration = this.cacheService.getApiConfiguration()
const {
apiConfiguration,
autoApprovalSettings,
browserSettings,
chatSettings: storedChatSettings,
preferredLanguage,
openaiReasoningEffort,
mode,
shellIntegrationTimeout,
terminalReuseEnabled,
terminalOutputLineLimit,
@@ -148,17 +185,9 @@ export class Controller {
enableCheckpointsSetting,
isNewUser,
taskHistory,
strictPlanModeEnabled,
} = await getAllExtensionState(this.context)
// Get current mode using helper function
const currentMode = await this.getCurrentMode()
// Reconstruct ChatSettings with mode from global state and stored preferences
const chatSettings: ChatSettings = {
...storedChatSettings, // Spread stored preferences (preferredLanguage, openAIReasoningEffort)
mode: currentMode, // Use mode from global state
}
const NEW_USER_TASK_COUNT_THRESHOLD = 10
// Check if the user has completed enough tasks to no longer be considered a "new user"
@@ -185,13 +214,17 @@ export class Controller {
apiConfiguration,
autoApprovalSettings,
browserSettings,
chatSettings,
preferredLanguage,
openaiReasoningEffort,
mode,
strictPlanModeEnabled ?? false,
shellIntegrationTimeout,
terminalReuseEnabled ?? true,
terminalOutputLineLimit ?? 500,
defaultTerminalProfile ?? "default",
enableCheckpointsSetting ?? true,
await getCwd(getDesktopDir()),
this.cacheService,
task,
images,
files,
@@ -219,10 +252,6 @@ export class Controller {
*/
async handleWebviewMessage(message: WebviewMessage) {
switch (message.type) {
case "fetchMcpMarketplace": {
await this.fetchMcpMarketplace(message.bool)
break
}
case "grpc_request": {
if (message.grpc_request) {
await handleGrpcRequest(this, message.grpc_request)
@@ -235,9 +264,9 @@ export class Controller {
}
break
}
// Add more switch case statements here as more webview message commands
// are created within the webview context (i.e. inside media/main.js)
default: {
console.error("Received unhandled WebviewMessage type:", JSON.stringify(message))
}
}
}
@@ -248,28 +277,25 @@ export class Controller {
await this.postStateToWebview()
}
async togglePlanActModeWithChatSettings(chatSettings: ChatSettings, chatContent?: ChatContent): Promise<boolean> {
const didSwitchToActMode = chatSettings.mode === "act"
async togglePlanActMode(modeToSwitchTo: Mode, chatContent?: ChatContent): Promise<boolean> {
const didSwitchToActMode = modeToSwitchTo === "act"
// Store mode to global state
await updateGlobalState(this.context, "mode", chatSettings.mode)
await updateGlobalState(this.context, "mode", modeToSwitchTo)
// Capture mode switch telemetry | Capture regardless of if we know the taskId
telemetryService.captureModeSwitch(this.task?.taskId ?? "0", chatSettings.mode)
telemetryService.captureModeSwitch(this.task?.taskId ?? "0", modeToSwitchTo)
// Update API handler with new mode (buildApiHandler now selects provider based on mode)
if (this.task) {
const { apiConfiguration } = await getAllExtensionState(this.context)
this.task.api = buildApiHandler({ ...apiConfiguration, taskId: this.task.taskId }, chatSettings.mode)
const apiConfiguration = this.cacheService.getApiConfiguration()
this.task.api = buildApiHandler({ ...apiConfiguration, taskId: this.task.taskId }, modeToSwitchTo)
}
// Save only non-mode properties to global storage
const { mode, ...persistentChatSettings }: { mode: string } & StoredChatSettings = chatSettings
await updateGlobalState(this.context, "chatSettings", persistentChatSettings)
await this.postStateToWebview()
if (this.task) {
this.task.chatSettings = chatSettings
this.task.updateMode(modeToSwitchTo)
if (this.task.taskState.isAwaitingPlanResponse && didSwitchToActMode) {
this.task.taskState.didRespondToPlanAskBySwitchingMode = true
// Use chatContent if provided, otherwise use default message
@@ -321,7 +347,7 @@ export class Controller {
async handleAuthCallback(customToken: string, provider: string | null = null) {
try {
await this.authService.handleAuthCallback(customToken, provider ? provider : "google")
await AuthService.getInstance(this).handleAuthCallback(customToken, provider ? provider : "google")
const clineProvider: ApiProvider = "cline"
@@ -329,27 +355,26 @@ export class Controller {
const { planActSeparateModelsSetting } = await getAllExtensionState(this.context)
const currentMode = await this.getCurrentMode()
// Get current API configuration from cache
const currentApiConfiguration = this.cacheService.getApiConfiguration()
const updatedConfig = { ...currentApiConfiguration }
if (planActSeparateModelsSetting) {
// Only update the current mode's provider
if (currentMode === "plan") {
await updateGlobalState(this.context, "planModeApiProvider", clineProvider)
updatedConfig.planModeApiProvider = clineProvider
} else {
await updateGlobalState(this.context, "actModeApiProvider", clineProvider)
updatedConfig.actModeApiProvider = clineProvider
}
} else {
// Update both modes to keep them in sync
await Promise.all([
updateGlobalState(this.context, "planModeApiProvider", clineProvider),
updateGlobalState(this.context, "actModeApiProvider", clineProvider),
])
updatedConfig.planModeApiProvider = clineProvider
updatedConfig.actModeApiProvider = clineProvider
}
// Get the updated API configuration (now includes the updated providers)
const { apiConfiguration } = await getAllExtensionState(this.context)
const updatedConfig = {
...apiConfiguration,
apiProvider: clineProvider,
}
// Update the API configuration through cache service
this.cacheService.setApiConfiguration(updatedConfig)
// Mark welcome view as completed since user has successfully logged in
await updateGlobalState(this.context, "welcomeViewCompleted", true)
@@ -468,31 +493,6 @@ export class Controller {
}
}
private async fetchMcpMarketplace(forceRefresh: boolean = false) {
try {
// Check if we have cached data
const cachedCatalog = (await getGlobalState(this.context, "mcpMarketplaceCatalog")) as
| McpMarketplaceCatalog
| undefined
if (!forceRefresh && cachedCatalog?.items) {
await sendMcpMarketplaceCatalogEvent(cachedCatalog)
return
}
const catalog = await this.fetchMcpMarketplaceFromApi(false)
if (catalog) {
await sendMcpMarketplaceCatalogEvent(catalog)
}
} catch (error) {
console.error("Failed to handle cached MCP marketplace:", error)
const errorMessage = error instanceof Error ? error.message : "Failed to handle cached MCP marketplace"
HostProvider.window.showMessage({
type: ShowMessageType.ERROR,
message: errorMessage,
})
}
}
// OpenRouter
async handleOpenRouterCallback(code: string) {
@@ -511,21 +511,20 @@ export class Controller {
const openrouter: ApiProvider = "openrouter"
const currentMode = await this.getCurrentMode()
await Promise.all([
updateGlobalState(this.context, "planModeApiProvider", openrouter),
updateGlobalState(this.context, "actModeApiProvider", openrouter),
])
await storeSecret(this.context, "openRouterApiKey", apiKey)
// Update API configuration through cache service
const currentApiConfiguration = this.cacheService.getApiConfiguration()
const updatedConfig = {
...currentApiConfiguration,
planModeApiProvider: openrouter,
actModeApiProvider: openrouter,
openRouterApiKey: apiKey,
}
this.cacheService.setApiConfiguration(updatedConfig)
await this.postStateToWebview()
if (this.task) {
// Get the updated API configuration (now includes the updated providers)
const { apiConfiguration } = await getAllExtensionState(this.context)
const updatedConfig = {
...apiConfiguration,
openRouterApiKey: apiKey,
taskId: this.task.taskId,
}
this.task.api = buildApiHandler(updatedConfig, currentMode)
this.task.api = buildApiHandler({ ...updatedConfig, taskId: this.task.taskId }, currentMode)
}
// await this.postMessageToWebview({ type: "action", action: "settingsButtonClicked" }) // bad ux if user is on welcome
}
@@ -699,13 +698,18 @@ export class Controller {
}
async getStateToPostToWebview(): Promise<ExtensionState> {
// Get API configuration from cache for immediate access
const apiConfiguration = this.cacheService.getApiConfiguration()
const {
apiConfiguration,
lastShownAnnouncementId,
taskHistory,
autoApprovalSettings,
browserSettings,
chatSettings: storedChatSettings,
preferredLanguage,
openaiReasoningEffort,
mode,
strictPlanModeEnabled,
userInfo,
mcpMarketplaceEnabled,
mcpDisplayMode,
@@ -721,51 +725,51 @@ export class Controller {
welcomeViewCompleted,
mcpResponsesCollapsed,
terminalOutputLineLimit,
localClineRulesToggles,
localWindsurfRulesToggles,
localCursorRulesToggles,
localWorkflowToggles,
} = await getAllExtensionState(this.context)
// Get current mode using helper function
const currentMode = await this.getCurrentMode()
const currentTaskItem = this.task?.taskId ? (taskHistory || []).find((item) => item.id === this.task?.taskId) : undefined
const checkpointTrackerErrorMessage = this.task?.taskState.checkpointTrackerErrorMessage
const clineMessages = this.task?.messageStateHandler.getClineMessages() || []
// Reconstruct ChatSettings with mode from global state and stored preferences
const chatSettings: ChatSettings = {
...storedChatSettings, // Spread stored preferences (preferredLanguage, openAIReasoningEffort)
mode: currentMode, // Use mode from global state
}
const processedTaskHistory = (taskHistory || [])
.filter((item) => item.ts && item.task)
.sort((a, b) => b.ts - a.ts)
.slice(0, 100) // for now we're only getting the latest 100 tasks, but a better solution here is to only pass in 3 for recent task history, and then get the full task history on demand when going to the task history view (maybe with pagination?)
const localClineRulesToggles =
((await getWorkspaceState(this.context, "localClineRulesToggles")) as ClineRulesToggles) || {}
const localWindsurfRulesToggles =
((await getWorkspaceState(this.context, "localWindsurfRulesToggles")) as ClineRulesToggles) || {}
const localCursorRulesToggles =
((await getWorkspaceState(this.context, "localCursorRulesToggles")) as ClineRulesToggles) || {}
const localWorkflowToggles = ((await getWorkspaceState(this.context, "workflowToggles")) as ClineRulesToggles) || {}
const latestAnnouncementId = getLatestAnnouncementId(this.context)
const shouldShowAnnouncement = lastShownAnnouncementId !== latestAnnouncementId
const platform = process.platform as Platform
const distinctId = PostHogClientProvider.getInstance().distinctId
const version = this.context.extension?.packageJSON?.version ?? ""
const uriScheme = vscode.env.uriScheme
return {
version: this.context.extension?.packageJSON?.version ?? "",
version,
apiConfiguration,
uriScheme: vscode.env.uriScheme,
currentTaskItem: this.task?.taskId ? (taskHistory || []).find((item) => item.id === this.task?.taskId) : undefined,
checkpointTrackerErrorMessage: this.task?.taskState.checkpointTrackerErrorMessage,
clineMessages: this.task?.messageStateHandler.getClineMessages() || [],
taskHistory: (taskHistory || [])
.filter((item) => item.ts && item.task)
.sort((a, b) => b.ts - a.ts)
.slice(0, 100), // for now we're only getting the latest 100 tasks, but a better solution here is to only pass in 3 for recent task history, and then get the full task history on demand when going to the task history view (maybe with pagination?)
shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId,
platform: process.platform as Platform,
uriScheme,
currentTaskItem,
checkpointTrackerErrorMessage,
clineMessages,
taskHistory: processedTaskHistory,
shouldShowAnnouncement,
platform,
autoApprovalSettings,
browserSettings,
chatSettings,
preferredLanguage,
openaiReasoningEffort,
mode,
strictPlanModeEnabled,
userInfo,
mcpMarketplaceEnabled,
mcpDisplayMode,
telemetrySetting,
planActSeparateModelsSetting,
enableCheckpointsSetting: enableCheckpointsSetting ?? true,
distinctId: telemetryService.distinctId,
distinctId,
globalClineRulesToggles: globalClineRulesToggles || {},
localClineRulesToggles: localClineRulesToggles || {},
localWindsurfRulesToggles: localWindsurfRulesToggles || {},
@@ -840,18 +844,4 @@ export class Controller {
await updateGlobalState(this.context, "taskHistory", history)
return history
}
// private async clearState() {
// this.context.workspaceState.keys().forEach((key) => {
// this.context.workspaceState.update(key, undefined)
// })
// this.context.globalState.keys().forEach((key) => {
// this.context.globalState.update(key, undefined)
// })
// this.context.secrets.delete("apiKey")
// }
// secrets
// dev
}
@@ -1,6 +1,6 @@
import { convertMcpServersToProtoMcpServers } from "@/shared/proto-conversions/mcp/mcp-server-conversion"
import type { AddRemoteMcpServerRequest } from "../../../shared/proto/mcp"
import { McpServers } from "../../../shared/proto/mcp"
import type { AddRemoteMcpServerRequest } from "@shared/proto/cline/mcp"
import { McpServers } from "@shared/proto/cline/mcp"
import type { Controller } from "../index"
/**

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