Compare commits

...
Author SHA1 Message Date
kevinneung 21da404f5b feat: add focus border to action buttons 2025-07-09 18:15:55 -07:00
Ara 5f90018ab5 Revert "remove blur (#4664)" (#4677) 2025-07-06 12:05:30 -06:00
celestial-vault 9e761cd1f0 lazily initialize provider sdks with error catching (#4661) 2025-07-06 11:41:16 -05:00
吴天一 e84f2ff962 remove blur (#4664) 2025-07-06 00:13:23 -05:00
tjandy98 1842254c57 Add support for Gemini 2.5 Pro and Flash models to SAP AI Core Provider (#4655)
* Add support for Gemini 2.5 Pro and Flash models

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

* Create calm-ads-glow.md

---------

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

* addressing comments

---------

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

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

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

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

* Updating CHANGELOG.md format

* Update CHANGELOG.md for version 3.18.3

---------

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

* changeset

---------

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

* Improve env variables and remove the magic number

* Add changeset

* refactor

* Update run.ts

---------

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

Filter out thinking tokens in message history

* changeset

* changeset

---------

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

* cleanup

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

* add general settings section

* duplicate import

* move terminal, browser, and feature settings

* move files to sections folder

* refactor out debug section

* pull out about section

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

* add doc strings to new hook functions

* add debounced text field for smooth typing

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

* add welcomeViewCompleted state boolean to control welcome view showing

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

* cleanup

* remove memo

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

* Get last terminal output if no output is retrieved

* Fix getting terminal output for when shell integration unavailable

* Apply suggestions from code review

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

* Revert removing previous changes

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

* Revert when first chunk fails message

* Revert error title

* Fixing tests with fake timers

---------

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

* Remove unused property and param postMessage

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

* Adding Comments and removing redundancies

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

---------

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

* format fix

* sjf review cleanups

* prefer paths over URI based on chats with sjf

* also document_uri -> document_path

* remove unused metadata import

---------

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

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

* chore: add changeset

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

---------

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

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

* fname

* integrate

* optionally save locally

* other cli

* name

* default

* return replacements with match type

* baseline -1

* baseline for evals

* work

* more info

---------

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

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

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

Replace GitHelper.getWorkspacePath with utils.getCwd()

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

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

* Remove unused file CheckpointTracker-old.ts

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

* Adding Comments and removing redundancies

---------

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

* add general settings section

* duplicate import

* move terminal, browser, and feature settings

* move files to sections folder

* refactor out debug section

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

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

* add changelog

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

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

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

* add claude 4 opus as well

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

---------

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

This reverts commit 890148407a.

* revert

* revert

* revert

---------

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

* browser 2
2025-06-28 10:12:51 -07:00
Toshii fe9e5bff74 base (#4539) 2025-06-28 00:27:13 -07:00
celestial-vault e65605535a [SettingsView.tsx] Extract Debug Section (#4536)
* refactor out apiconfig section

* add general settings section

* duplicate import

* move terminal, browser, and feature settings

* move files to sections folder

* refactor out debug section
2025-06-27 16:04:28 -07:00
celestial-vault 3d100d835a [SettingsView.tsx] Extract Browser, Terminal, and Feature Sections (#4533)
* refactor out apiconfig section

* add general settings section

* duplicate import

* move terminal, browser, and feature settings

* move files to sections folder
2025-06-27 15:44:31 -07:00
celestial-vault b9d3814355 [SettingsView.tsx] Extract GeneralSettings Section (#4529)
* refactor out apiconfig section

* add general settings section

* duplicate import

* add missing type
2025-06-27 15:14:12 -07:00
xiongxiong 12fbc48629 feat. support anthropic--claude-4-sonnet in SAP AI Core provider (#4448) 2025-06-27 14:44:54 -07:00
Sarah Fortune d2f1e0cde0 Replace vscode.workspace.workspaceFolders with host bridge getWorkspacePaths (#4443)
* Use the host bridge in utils/path.ts

Update utils/path.ts to use the host bridge to get the workspace folders, instead of the vscode SDK.
Update callers to use await as the functions are now async.

* Replace vscode workspaceFolders in WorkspaceTracker

Make the cwd an instance property because await cannot be used at the top level.

* Use the host bridge getWorkspacePaths in FileContextTracker

Replace the vscode SDK getWorkspaceFolders with the util function getCwd (this is already switched to the host bridge).

* Fix test failure

Update the rootDir for the tests to be "." instead of "src". The changes to path.ts pull in new dependencies from the extension, which indirectly include files from the webview-ui.

```
Run npm run pretest

> claude-dev@3.18.0 pretest
> npm run compile-tests && npm run compile && npm run compile-standalone && npm run lint

> claude-dev@3.18.0 compile-tests
> node ./scripts/build-tests.js

node:child_process:957
    throw err;
    ^

Error: Command failed: tsc -p ./tsconfig.test.json --outDir out
    at genericNodeError (node:internal/errors:983:15)
    at wrappedFn (node:internal/errors:537:14)
    at checkExecSyncError (node:child_process:882:11)
    at execSync (node:child_process:954:15)
    at Object.<anonymous> (/home/runner/work/cline/cline/scripts/build-tests.js:55:1)
    at Module._compile (node:internal/modules/cjs/loader:1730:14)
    at Object..js (node:internal/modules/cjs/loader:1895:10)
    at Module.load (node:internal/modules/cjs/loader:1465:32)
    at Function._load (node:internal/modules/cjs/loader:1282:12)
    at TracingChannel.traceSync (node:diagnostics_channel:322:14) {
  status: 2,
  signal: null,
  output: [
    null,
    "src/services/test/TestServer.ts(8,35): error TS6059: File '/home/runner/work/cline/cline/webview-ui/src/services/grpc-client.ts' is not under 'rootDir' '/home/runner/work/cline/cline/src'. 'rootDir' is expected to contain all source files.\n" +
      "webview-ui/src/services/grpc-client-base.ts(1,24): error TS6059: File '/home/runner/work/cline/cline/webview-ui/src/utils/vscode.ts' is not under 'rootDir' '/home/runner/work/cline/cline/src'. 'rootDir' is expected to contain all source files.\n" +
      "webview-ui/src/services/grpc-client.ts(4,34): error TS6059: File '/home/runner/work/cline/cline/webview-ui/src/services/grpc-client-base.ts' is not under 'rootDir' '/home/runner/work/cline/cline/src'. 'rootDir' is expected to contain all source files.\n",
    ''
  ],
  pid: 2496,
  stdout: "src/services/test/TestServer.ts(8,35): error TS6059: File '/home/runner/work/cline/cline/webview-ui/src/services/grpc-client.ts' is not under 'rootDir' '/home/runner/work/cline/cline/src'. 'rootDir' is expected to contain all source files.\n" +
    "webview-ui/src/services/grpc-client-base.ts(1,24): error TS6059: File '/home/runner/work/cline/cline/webview-ui/src/utils/vscode.ts' is not under 'rootDir' '/home/runner/work/cline/cline/src'. 'rootDir' is expected to contain all source files.\n" +
    "webview-ui/src/services/grpc-client.ts(4,34): error TS6059: File '/home/runner/work/cline/cline/webview-ui/src/services/grpc-client-base.ts' is not under 'rootDir' '/home/runner/work/cline/cline/src'. 'rootDir' is expected to contain all source files.\n",
  stderr: ''
}
```

* Build the protos before compiling the tests.

The tests depend on generated files now, so compile the extension before the tests so that the protos are built.

* Set up the host providers in the integration test FileContextTracker.test.ts

* Reduce the amount of logging in grpc-service.ts

Just log the service registration, instead of every rpc.

* In the `clean` build target, also remove the compiled test code.

* Correct the alias mapping for the compiled test files.
2025-06-27 11:51:02 -07:00
celestial-vault efe2388e4a refactor out apiconfig section (#4528) 2025-06-27 10:57:54 -07:00
celestial-vault 247552fcb5 remove unused function (#4518) 2025-06-27 09:35:02 -07:00
ToshiiandStvLz 656e3276c6 lite llm provider ui (#4517)
* base 2

Co-authored-by: StvLz <lizarazo.steven@gmail.com>

* base 3

* base

---------

Co-authored-by: StvLz <lizarazo.steven@gmail.com>
2025-06-26 22:51:12 -07:00
ToshiiandStvLz 0e29f05e28 refactor nebius and remove unused logic (#4515)
* base 2

Co-authored-by: StvLz <lizarazo.steven@gmail.com>

* base 3

---------

Co-authored-by: StvLz <lizarazo.steven@gmail.com>
2025-06-26 22:39:56 -07:00
celestial-vault 985cb51c39 refactor lmstudio (#4514)
* refactor lmstudio

* merge conflict deleted stuff
2025-06-26 22:34:14 -07:00
celestial-vault abccde0e2a refactor out vscode lm (#4513) 2025-06-26 21:55:37 -07:00
celestial-vault ca984609ca refactor out cline (#4512) 2025-06-26 21:41:35 -07:00
celestial-vault 6690d392cd refactor out bedrock (#4510) 2025-06-26 21:29:27 -07:00
Toshii 40244f09fe base (#4511) 2025-06-26 21:23:43 -07:00
Toshii 9563a71c8a base (#4509) 2025-06-26 21:14:00 -07:00
celestial-vault c13e749eed refactor out sap ai core settings (#4508) 2025-06-26 21:05:29 -07:00
Toshii 8c3fd8ba55 refactor cerebras provider menu ui (#4507)
* xai

* base
2025-06-26 20:53:58 -07:00
celestial-vault 8bfa7daa28 refactor ollama settings to different component (#4506) 2025-06-26 20:45:30 -07:00
Toshii 7cd4be7a68 xai (#4504) 2025-06-26 20:34:34 -07:00
Toshii ab9f1a0785 refactor request and fireworks provider ui (#4499)
* requesty

* fireworks
2025-06-26 20:11:15 -07:00
canvrno 68b84f3df4 Fix for delete all bug, refactored deleteAll logic (#4497) 2025-06-26 17:53:44 -07:00
kevinneung 16da0f1e06 Fix telemetry source code link path (#4476)
Update the source code link in telemetry documentation to point to the correct file location: src/services/posthog/telemetry/TelemetryService.ts
2025-06-26 16:54:28 -07:00
Daniel Steigman e898bd8825 Updated best model to be gemini (#4500) 2025-06-26 16:47:34 -07:00
canvrno b7b0e96cc7 Add env host bridge service, migrate clipboard read/write calls (#4496)
* host bridge migration - clipboard

* changeset

* removed dev logging

* switched to empty return on clipboard write

* Moved new hostServiceNameMap entry to new proto configs
2025-06-26 16:18:29 -07:00
Andrei EternalandAndrei Eternal f00c5f4ecc experimental vscode impls & build-proto cleanup (#4493)
* experimental vscode impls

* weird errors

* add workspace host config

* format

---------

Co-authored-by: Andrei Eternal <eternal@cline.bot>
2025-06-26 16:07:21 -07:00
Tomás Barreiro 2709ccefcd fix: ENAMETOOLONG on Claude Code - write to stdin instead of including the messages in the prompt (#4494)
* Write to stdin instead of including the messages in the prompt

* Add changeset
2025-06-27 04:13:58 +05:30
Toshii c8f0324536 remove gemini free text for 2.5 models (#4492)
* gemini text

* remove
2025-06-26 15:43:24 -07:00
ToshiiandStvLz 937cebc7de refactor vertex and doubao provider ui (#4474)
* doubao

* vertex

Co-authored-by: StvLz <lizarazo.steven@gmail.com>

* ui

---------

Co-authored-by: StvLz <lizarazo.steven@gmail.com>
2025-06-26 15:01:31 -07:00
canvrno d4d5a49e67 Refactor autoApprove out of task class (#4233)
* Refactor auto approve out of task class

* Rebase fixes

* cleanup

* reorg
2025-06-26 10:40:37 -07:00
SmileFisherandToshii 01d3afe0c5 Consider adding a pronunciation guide, since many Chinese developers tend to pronounce it as "C line". (#3635)
* Update README.md

add pronunciation

* Update README.md

---------

Co-authored-by: Toshii <94262432+0xToshii@users.noreply.github.com>
2025-06-25 18:57:45 -07:00
github-actions[bot] 7bdab5613b v3.18.0 Release Notes
-   Optimized Cline to work with the Claude 4 family of models, resulting in improved performance, reliability, and new capabilities
-   Added a new Gemini CLI provider that allows you to use your local Gemini CLI authentication to access Gemini models for free (Thanks @google-gemini!)
-   Optimized Cline to work with the Gemini 2.5 family of models
-   Updated the default and recommended model to Claude 4 Sonnet for the best performance
-   Fix race condition in Plan/Act mode switching
-   Improve robustness of search and replace parsing
2025-06-25 17:39:46 -07:00
AraandCline Evaluation d79722bec0 Fix: Solving race condition in the Plan/Act mode switching functionality (#4421)
* type fix

* markdown fix

* markdown fix

* Fix settings on switch

* Fix settings on switch

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-06-25 16:58:48 -07:00
Saoud Rizwan 06af720d85 Show placeholder while first response loads (#4475) 2025-06-25 16:55:55 -07:00
AraandCline Evaluation b80ef25a33 fix: Solving Context window counting issues in gemini model family in openrouter and cline provider (#4314)
Co-authored-by: Cline Evaluation <cline@example.com>
2025-06-25 16:54:30 -07:00
AraandCline Evaluation 1996543eb0 Raise errors properly when OpenRouter stream cuts midway (#4398)
* Raise error on openrouter stream termination midway

* Raise error on openrouter stream termination midway

* type fix

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-06-25 16:42:12 -07:00
AraandCline Evaluation d9dfd57da5 Settings fix (#4405)
Co-authored-by: Cline Evaluation <cline@example.com>
2025-06-25 16:41:25 -07:00
AraandCline Evaluation 890148407a Adding Gemini CLI Provider with Oauth support (#4472)
* Adding Gemini CLI direct oauth

* Adding Gemini CLI direct oauth

* Adding Gemini CLI direct oauth

* Adding Gemini CLI direct oauth

* Adding Gemini CLI direct oauth

* Adding Gemini CLI direct oauth

* Adding Gemini CLI direct oauth

* Adding Gemini CLI direct oauth

* Adding Gemini CLI direct oauth

* Adding Gemini CLI direct oauth

* Adding Gemini CLI direct oauth

* language

* themes instead of hardcorded colors

---------

Co-authored-by: Cline Evaluation <cline@example.com>
2025-06-25 16:40:31 -07:00
226 changed files with 11478 additions and 8569 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
added focusBorder coloring to Approve / Reject buttons
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Add support for Gemini 2.5 Pro and Flash to SAP AI Core Provider
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Optimizing cline to work for gemini 2.5 family of models
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Modified diff regex to account for trailing > in search & replace blocks
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
telemtrySetting protobus migration
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Optimized Cline for Claude 4
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
clearAllTaskHistory protobus migration
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Updating default and recommended model to claude 4 sonnet
+2 -1
View File
@@ -7,6 +7,7 @@ tmp
*.vsix
.DS_Store
.idea
pnpm-lock.yaml
@@ -37,4 +38,4 @@ src/hosts/vscode/*/methods.ts
src/hosts/vscode/*/index.ts
src/hosts/vscode/client/host-grpc-client.ts
src/hosts/vscode/host-grpc-service-config.ts
src/standalone/server-setup.ts
src/standalone/server-setup.ts
-4
View File
@@ -37,10 +37,6 @@ docs/**
!node_modules/@vscode/codicons/dist/codicon.css
!node_modules/@vscode/codicons/dist/codicon.ttf
# Include KaTeX CSS and fonts for LaTeX rendering
!webview-ui/node_modules/katex/dist/katex.min.css
!webview-ui/node_modules/katex/dist/fonts/**
# Include default themes JSON files used in getTheme
!src/integrations/theme/default-themes/**
+30
View File
@@ -1,5 +1,35 @@
# Changelog
## [3.18.3]
- Improve Cerebras Qwen model performance by removing thinking tokens from model input (Thanks @kevint-cerebras!)
- Improve Claude Code provider with better error handling and performance optimizations (Thanks @BarreiroT!)
## [3.18.2]
- Fix issue where terminal output would not be captured if shell integration fails by falling back to capturing the terminal content.
- Add confirmation popup when deleting tasks
- Add support for Claude Sonnet 4 and Opus 4 model in SAP AI Core provider (Thanks @lizzzcai!)
- Add support for `litellm_session_id` to group requests in a single session (Thanks @jorgegarciarey!)
- Add "Thinking Budget" customization for Claude Code (Thanks @BarreiroT!)
- Fix issue where the extension would use the user's environment variables for authentication when using Claude Code (Thanks @BarreiroT!)
## [3.18.1]
- Add support for Claude 4 Sonnet in SAP AI Core provider (Thanks @GTxx!)
- Fix ENAMETOOLONG error when using Claude Code provider with long conversation histories (Thanks @BarreiroT!)
- Remove Gemini CLI provider because Google asked us to
- Fix bug with "Delete All Tasks" functionality
## [3.18.0]
- Optimized Cline to work with the Claude 4 family of models, resulting in improved performance, reliability, and new capabilities
- Added a new Gemini CLI provider that allows you to use your local Gemini CLI authentication to access Gemini models for free (Thanks @google-gemini!)
- Optimized Cline to work with the Gemini 2.5 family of models
- Updated the default and recommended model to Claude 4 Sonnet for the best performance
- Fix race condition in Plan/Act mode switching
- Improve robustness of search and replace parsing
## [3.17.16]
- Fix Claude Code provider error handling for incomplete messages during long-running tasks (Thanks @BarreiroT!)
+1 -1
View File
@@ -30,7 +30,7 @@ English | <a href="https://github.com/cline/cline/blob/main/locales/es/README.md
</table>
</div>
Meet Cline, an AI assistant that can use your **CLI** a**N**d **E**ditor.
Meet Cline (pronounced /klaɪn/, like "Klein"), an AI assistant that can use your **CLI** a**N**d **E**ditor.
Thanks to [Claude 3.7 Sonnet's agentic coding capabilities](https://www.anthropic.com/claude/sonnet), Cline can handle complex software development tasks step-by-step. With tools that let him create & edit files, explore large projects, use the browser, and execute terminal commands (after you grant permission), he can assist you in ways that go beyond code completion or tech support. Cline can even use the Model Context Protocol (MCP) to create new tools and extend his own capabilities. While autonomous AI scripts traditionally run in sandboxed environments, this extension provides a human-in-the-loop GUI to approve every file change and terminal command, providing a safe and accessible way to explore the potential of agentic AI.
+4
View File
@@ -170,6 +170,10 @@
"running-models-locally/ollama"
]
},
{
"group": "Troubleshooting",
"pages": ["troubleshooting/terminal-quick-fixes", "troubleshooting/terminal-integration-guide"]
},
{
"group": "More Info",
"pages": ["more-info/telemetry"]
@@ -58,3 +58,16 @@ When you use the terminal mention in your message, here's what happens behind th
6. The AI can now "see" the complete terminal output with all formatting preserved
This process happens automatically whenever you use the terminal mention, giving the AI access to your command results, error messages, and other terminal output without you having to copy it manually.
## Troubleshooting Terminal Issues
If you're experiencing issues with terminal mentions or terminal integration in general (such as "Shell Integration Unavailable" or commands not showing output), please refer to our comprehensive [Terminal Integration Troubleshooting Guide](/troubleshooting/terminal-integration-guide).
Common issues include:
- Terminal mentions not capturing output
- "Shell Integration Unavailable" messages in Cline chat
- Commands executing but output not visible to Cline
- Terminal integration working inconsistently
The troubleshooting guide provides platform-specific solutions and detailed configuration steps to resolve these issues.
@@ -74,8 +74,25 @@ This approach ensures that all terminal output, including colors and formatting,
- **Select specific output when needed**: By default, the integration captures all terminal content, but you can also select specific lines before right-clicking to focus on just the relevant output.
- **Combine with file mentions**: After sending terminal output to Cline, you can enhance your question by mentioning relevant files using the @ mentions feature.
- **Combine terminal outputs with file mentions**: After sending terminal output to Cline, you can enhance your question by mentioning relevant files using the @ mentions feature.
- **Use for build and test output**: Terminal integration is particularly useful for understanding complex build errors or test failures that span multiple lines.
- **Contextualize build & test outputs with the terminal**: Terminal integration is particularly useful for understanding complex build errors or test failures that span multiple lines.
Next time you're staring at a cryptic error message in your terminal, try using Cline's terminal integration instead of copying and pasting. You'll get more accurate help because Cline can see the complete terminal context with proper formatting.
## Troubleshooting Terminal Issues
If you're experiencing issues with terminal integration, such as "Shell Integration Unavailable" or commands not showing output, please refer to our comprehensive [Terminal Integration Troubleshooting Guide](/troubleshooting/terminal-integration-guide).
The troubleshooting guide covers:
- Common terminal integration issues and quick fixes
- Platform-specific solutions for Windows, macOS, and Linux
- Shell-specific configurations for zsh, bash, PowerShell, and more
- Advanced debugging techniques
- Terminal settings optimization
<Tip>
**Quick Fix**: Most terminal issues can be resolved by switching to bash in the Cline settings and increasing the shell
integration timeout to 10 seconds.
</Tip>
+2 -2
View File
@@ -6,7 +6,7 @@ title: "Telemetry"
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.
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/telemetry/TelemetryService.ts) to see exactly what we track.
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
@@ -22,7 +22,7 @@ We collect basic anonymous usage data including:
**System Context:** OS type and VS Code environment details\
**UI Activity:** Navigation patterns and feature usage
For complete transparency, you can inspect our [telemetry implementation](https://github.com/cline/cline/blob/main/src/services/telemetry/TelemetryService.ts) to see the exact events we track.
For complete transparency, you can inspect our [telemetry implementation](https://github.com/cline/cline/blob/main/src/services/posthog/telemetry/TelemetryService.ts) to see the exact events we track.
### How to Opt Out
@@ -0,0 +1,399 @@
---
title: "Terminal Integration Troubleshooting Guide"
sidebarTitle: "Terminal Troubleshooting"
description: "Complete guide to resolving terminal integration issues in Cline"
---
This guide helps you resolve terminal integration issues in Cline. Terminal integration is crucial for Cline to execute commands and read their output, enabling it to understand errors, test results, and command responses.
<Tip>
If you're experiencing terminal issues, try switching to a simpler shell like `bash` in the Cline settings, under "Terminal Settings"
This resolves most terminal integration problems.
</Tip>
## Quick Diagnosis Flowchart
Follow this flowchart to quickly identify your issue:
```mermaid
graph TD
A[Terminal Issue] --> B{Can Cline execute commands?}
B -->|No| C[Shell Integration Unavailable]
B -->|Yes| D{Can Cline see the output?}
D -->|No| E[Output Capture Failed]
D -->|Yes| F{Is the output corrupted?}
F -->|Yes| G[Character Filtering Issue]
F -->|No| H{Does the command hang?}
H -->|Yes| I[Long-Running Command Issue]
H -->|No| J[Check Terminal Settings]
C --> K[Try Solution 1]
E --> L[Try Solution 2]
G --> M[Try Solution 3]
I --> N[Try Solution 4]
style A fill:#f9f,stroke:#333,stroke-width:2px
style K fill:#9f9,stroke:#333,stroke-width:2px
style L fill:#9f9,stroke:#333,stroke-width:2px
style M fill:#9f9,stroke:#333,stroke-width:2px
style N fill:#9f9,stroke:#333,stroke-width:2px
```
## Common Issues & Quick Solutions
### 1. Shell Integration Unavailable
**Symptoms:**
- Message: "Shell Integration Unavailable"
- Commands execute but Cline can't read output
- Terminal works fine manually but not with Cline
**Quick Solutions:**
#### macOS
- **Switch to bash**
1. Go to Cline Settings
2. Left-Click the **"Terminal Settings"** tab
3. Navigate to **"Default Terminal Profile"** and select **"bash"** from the drop-down menu
- **Disable Oh-My-Zsh temporarily**:
1. If using zsh, enter `mv ~/.zshrc ~/.zshrc.backup` into the terminal
2. Restart VSCode
- **Set environment**:
1.a For Zsh users, use one of the following Zsh commands to edit your shell profile:
- `nano ~/.zshrc`
- `vim ~/.zshrc`
- `code ~/.zshrc`
1.b For Bash users
- nano ~/.bash_profile
2. Add the following to your shell config: `export TERM=xterm-256color`
3. Save your configuration
#### Windows
- **Use PowerShell 7**
1. Install from Microsoft Store
2. Go to Cline Settings
3. Left-Click the **"Terminal Settings"** tab
4. Navigate to **"Default Terminal Profile"** and select **"PowerShell 7"** from the drop-down menu
- **Disable Windows ConPTY**
1. Navigate to your VSCode Settings
2. Enter "Integrated: Windows Enable Conpty" into the Settings searchbar
3. Uncheck the option
- **Try Command Prompt**
1. Go to Cline Settings
2. Left-Click the **"Terminal Settings"** tab
3. Navigate to **"Default Terminal Profile"** and select **"Command Prompt"** from the drop-down menu
#### Linux
- **Use bash**
1. Go to Cline Settings
2. Left-Click the **"Terminal Settings"** tab
3. Navigate to **"Default Terminal Profile"** and select **"bash"** from the drop-down menu
- **Check permissions**
1. Ensure VSCode has terminal access permissions
- **Disable custom prompts**
1. Comment out prompt customizations in `.bashrc`
### 2. Command Output Not Visible
**Symptoms:**
- Cline states in chat: "[Command is running but producing no output]"
- Commands complete but Cline doesn't see results
- Commands work sometimes but not consistently
**Solutions:**
- **Increase Shell Integration Timeout**
1. Within Cline, left-click the **Settings** button in the top right-hand corner of the chat window
2. Once in the **Settings** window, left-click the **"Terminal Settings"** tab from the left-hand column
3. Navigate to "Shell integration timeout (seconds)" and enter **"10"** into the text field
- **Disable Terminal Reuse**
1. Within Cline, left-click the **Settings** button in the top right-hand corner of the chat window
2. Once in the **Settings** window, left-click the **"Terminal Settings"** tab from the left-hand column
3. Look for **"Enable aggressive terminal reuse"**, and **uncheck** this option
- **Check for interfering extensions**
1. Disable other terminal-related VSCode extensions
### 3. Character Filtering Issues
**Symptoms:**
- Commas missing from output (JSON appears corrupted)
- Special characters stripped from terminal output
- Syntax errors that don't appear when running manually
**Solution:**
This is a known bug in output processing. Workarounds:
- Recommend AI to use file output instead
1. Tell Cline in chat or Cline rules, to use `command > output.txt` before reading the file/s
<Tip>
This family of issues is only partially solved in the latest Cline versions, so if you still face this, create a GitHub issue
if it is a persistent problem.
</Tip>
### 4. Long-Running Commands & Progress Bars
**Symptoms:**
- Docker builds never complete in Cline
- Progress bars consume thousands of tokens
- The Cline button "Proceed while running" doesn't work properly in chat
<Tip>
This family of issues has been solved in latest Cline versions but if you still face any issues, then create a GitHub issue
for this.
</Tip>
## Terminal Settings Explained
Access these in Cline by clicking the settings icon, and navigating to the "Terminal Settings" section:
### Default Terminal Profile
- **What it does**: Selects which shell Cline uses for commands
- **When to change**: If experiencing shell integration issues with your default shell
- **Recommended**: - macOS: bash (if zsh has issues) - Windows: PowerShell 7 - Linux: bash
### Shell Integration Timeout
- **What it does**: How long Cline waits for the terminal to be ready
- **Default**: 4 seconds
- **When to increase**:
- Slow shell startup (heavy .zshrc/.bashrc)
- WSL environments
- SSH connections
- **Recommended**: - Start with 10 seconds if having issues
### Enable Aggressive Terminal Reuse
- **What it does**: Reuses existing terminals even if not in the correct directory
- **When to disable**:
- Commands execute in wrong directory
- Virtual environment issues
- Terminal state corruption
- **Trade-off**: - Disabling creates more terminals but ensures clean state
### Terminal Output Line Limit
- **What it does**: Limits how many lines Cline reads from terminal output
- **Default**: 500 lines
- **When to adjust**:
- Increase for verbose build outputs
- Decrease if hitting token limits
- Set to 100 for commands with progress bars
## Platform-Specific Solutions
### macOS Issues
#### Oh-My-Zsh Conflicts
Oh-My-Zsh often interferes with shell integration. Solutions:
1. Create a minimal `.zshrc` for VSCode:
```bash
# ~/.zshrc-vscode
export TERM=xterm-256color
export PAGER=cat
# Minimal PATH and environment setup
```
2. Configure VSCode to use it:
```json
{
"terminal.integrated.env.osx": {
"ZDOTDIR": "~/.zshrc-vscode"
}
}
```
#### macOS 15+ Issues
Recent macOS versions have stricter terminal permissions:
1. System Preferences → Privacy & Security → Developer Tools
2. Add Visual Studio Code
3. Restart VSCode completely
### Windows Issues
#### PowerShell Execution Policy
If commands fail silently:
```powershell
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
```
#### WSL Integration
For WSL issues:
1. Use WSL extension for VSCode
2. Open folder in WSL: `code .` from WSL terminal
3. Select "WSL Bash" as terminal profile in Cline
#### Path Issues
Windows path problems:
1. Use forward slashes in Cline: `C:/Users/...`
2. Quote paths with spaces: `"C:/Program Files/..."`
3. Avoid `~` - use full paths
### Linux/SSH/Container Issues
#### SSH Connections
For remote development:
1. Install Cline on the remote machine, not locally
2. Use SSH extension's integrated terminal
3. Increase timeout to 15+ seconds
#### Docker Containers
When developing in containers:
1. Install Cline in the container
2. Use Dev Containers extension
3. Ensure shell integration scripts are available
## Shell-Specific Fixes
### Zsh
```bash
# Add to ~/.zshrc
export TERM=xterm-256color
export PAGER=cat
# Disable fancy prompts for VSCode
if [[ "$TERM_PROGRAM" == "vscode" ]]; then
PS1="%n@%m %1~ %# "
fi
```
### Bash
```bash
# Add to ~/.bashrc
export TERM=xterm-256color
export PAGER=cat
# Simple prompt for VSCode
if [[ "$TERM_PROGRAM" == "vscode" ]]; then
PS1='\u@\h:\w\$ '
fi
```
### Fish
```fish
# Add to ~/.config/fish/config.fish
set -x TERM xterm-256color
set -x PAGER cat
# Disable fancy features in VSCode
if test "$TERM_PROGRAM" = "vscode"
function fish_prompt
echo (whoami)'@'(hostname)':'(pwd)'> '
end
end
```
### PowerShell
```powershell
# Add to $PROFILE
$env:PAGER = "cat"
# Disable progress bars
$ProgressPreference = 'SilentlyContinue'
```
## Advanced Troubleshooting
### Debug Mode
Enable terminal debugging to see what's happening:
1. Open VSCode Command Palette (Cmd/Ctrl+Shift+P)
2. Run: "Developer: Set Log Level..."
3. Choose "Trace"
4. Check Output panel → "Cline" for terminal logs
### Manual Shell Integration Test
Test if shell integration works at all:
```bash
# In VSCode terminal
echo $TERM_PROGRAM # Should show "vscode"
echo $VSCODE_SHELL_INTEGRATION # Should be "1"
```
## FAQ
### Why does Cline create so many terminals?
When shell integration fails, Cline can't reuse terminals safely (they might be running long processes). Enable shell integration or adjust the terminal reuse setting.
### Can I use my custom shell (nushell, xonsh, etc.)?
Cline officially supports bash, zsh, fish, and PowerShell. Custom shells may work but aren't guaranteed. Use bash as a fallback.
### Why do some commands work but others don't?
Commands that use interactive features (pagers, progress bars, curses) often fail. Set `PAGER=cat` and use non-interactive flags.
### How do I know if shell integration is working?
Working integration shows command output in Cline's chat. Failed integration shows "Shell Integration Unavailable" or "[Command is running but producing no output]".
## Still Having Issues?
If you've tried everything:
1. **Collect Debug Info**:
```bash
echo "Shell: $SHELL"
echo "Term: $TERM"
echo "VSCode: $TERM_PROGRAM"
which bash
bash --version
```
2. **Report the Issue**:
- Use `/reportbug` in Cline github issues
- Include your debug info
- Mention which solutions you tried
<Tip>
Remember: Most terminal issues are resolved by switching to bash and increasing the timeout. Start there before trying complex
solutions.
</Tip>
@@ -0,0 +1,51 @@
---
title: "Terminal Quick Fixes"
sidebarTitle: "Terminal Quick Fixes"
description: "Quick solutions for common terminal issues"
---
**Here is a list of common fixes, starting with the most applicable:**
- **Switch to bash** (solves most instances)
1. Within Cline, left-click the **Settings** button in the top right-hand corner of the chat window
2. Once in the **Settings** window, left-click the **"Terminal Settings"** tab from the left-hand column
3. Navigate to **"Default Terminal Profile"** and select **"bash"** from the drop-down
- **Increase timeout**
1. Within Cline, left-click the **Settings** button in the top right-hand corner of the chat window
2. Once in the **Settings** window, left-click the **"Terminal Settings"** tab from the left-hand column
3. Navigate to "Shell integration timeout (seconds)" and enter **"10"** into the text field
- **Disable terminal reuse**
1. Within Cline, left-click the **Settings** button in the top right-hand corner of the chat window
2. Once in the **Settings** window, left-click the **"Terminal Settings"** tab from the left-hand column
3. Look for **"Enable aggressive terminal reuse"**, and **uncheck** this option
## Platform-Specific Fixes
### macOS + Oh-My-Zsh
```bash
# Create minimal config for VSCode
echo 'export TERM=xterm-256color' > ~/.zshrc-vscode
echo 'export PAGER=cat' >> ~/.zshrc-vscode
```
### Windows PowerShell
```powershell
# Run as Administrator
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
```
### WSL
- Open folder from WSL: `code .`
- Select **"WSL Bash"** in Cline settings, under **"Terminal Settings"**
- Increase **"Shell integration timeout (seconds)"** to **15**
## Full Guide
For detailed troubleshooting, see the [Complete Terminal Troubleshooting Guide](/troubleshooting/terminal-integration-guide).
+10
View File
@@ -6,6 +6,7 @@ interface RunDiffEvalOptions {
modelIds: string
systemPromptName: string
validAttemptsPerCase: number
maxAttemptsPerCase?: number
parsingFunction: string
diffEditFunction: string
thinkingBudget: number
@@ -16,6 +17,7 @@ interface RunDiffEvalOptions {
replay: boolean
replayRunId?: string
diffApplyFile?: string
saveLocally: boolean
maxCases?: number
}
@@ -70,10 +72,18 @@ export async function runDiffEvalHandler(options: RunDiffEvalOptions) {
args.push("--verbose")
}
if (options.maxAttemptsPerCase) {
args.push("--max-attempts-per-case", String(options.maxAttemptsPerCase))
}
if (options.maxCases) {
args.push("--max-cases", String(options.maxCases))
}
if (options.saveLocally) {
args.push("--save-locally")
}
try {
console.log(chalk.gray(`Executing: npx tsx ${scriptPath} ${args.join(" ")}`))
+3
View File
@@ -87,6 +87,7 @@ program
.option("--model-ids <model_ids>", "Comma-separated list of model IDs to test")
.option("--system-prompt-name <name>", "The name of the system prompt to use", "basicSystemPrompt")
.option("-n, --valid-attempts-per-case <number>", "Number of valid attempts per test case per model (will retry until this many valid attempts are collected)", "1")
.option("--max-attempts-per-case <number>", "Maximum total attempts per test case (default: 10x valid attempts)")
.option("--max-cases <number>", "Maximum number of test cases to run (limits total cases loaded)")
.option("--parsing-function <name>", "The parsing function to use", "parseAssistantMessageV2")
.option("--diff-edit-function <name>", "The diff editing function to use", "constructNewFileContentV2")
@@ -95,12 +96,14 @@ program
.option("--replay", "Run evaluation from a pre-recorded LLM output, skipping the API call", false)
.option("--replay-run-id <run_id>", "The ID of the run to replay from the database")
.option("--diff-apply-file <filename>", "The name of the diff apply file to use for the replay")
.option("--save-locally", "Save results to local JSON files in addition to database", false)
.option("-v, --verbose", "Enable verbose logging", false)
.action(async (options) => {
try {
const fullOptions = {
...options,
validAttemptsPerCase: parseInt(options.validAttemptsPerCase, 10),
maxAttemptsPerCase: options.maxAttemptsPerCase ? parseInt(options.maxAttemptsPerCase, 10) : undefined,
thinkingBudget: parseInt(options.thinkingBudget, 10),
maxCases: options.maxCases ? parseInt(options.maxCases, 10) : undefined,
}
+24 -12
View File
@@ -11,9 +11,10 @@ import {
import { constructNewFileContent as constructNewFileContent_06_06_25 } from "./diff-apply/diff-06-06-25"
import { constructNewFileContent as constructNewFileContent_06_23_25 } from "./diff-apply/diff-06-23-25"
import { constructNewFileContent as constructNewFileContent_06_25_25 } from "./diff-apply/diff-06-25-25"
import { constructNewFileContent as constructNewFileContent_06_26_25 } from "./diff-apply/diff-06-26-25"
type ParseAssistantMessageFn = (message: string) => AssistantMessageContent[]
type ConstructNewFileContentFn = (diff: string, original: string, strict: boolean) => Promise<string>
type ConstructNewFileContentFn = (diff: string, original: string, strict: boolean) => Promise<string | any>
const parsingFunctions: Record<string, ParseAssistantMessageFn> = {
parseAssistantMessageV1: parseAssistantMessageV1,
@@ -25,9 +26,11 @@ const diffEditingFunctions: Record<string, ConstructNewFileContentFn> = {
"diff-06-06-25": constructNewFileContent_06_06_25,
"diff-06-23-25": constructNewFileContent_06_23_25,
"diff-06-25-25": constructNewFileContent_06_25_25,
"diff-06-26-25": constructNewFileContent_06_26_25,
}
import { TestInput, TestResult, ExtractedToolCall } from "./types"
import { log } from "./helpers"
export { TestInput, TestResult, ExtractedToolCall }
interface StreamResult {
@@ -282,21 +285,21 @@ export async function runSingleEvaluation(input: TestInput): Promise<TestResult>
}
// check that we are editing the correct file path
console.log(`Expected file path: "${originalFilePath}"`);
console.log(`Actual file path used: "${diffToolPath}"`);
log(input.isVerbose, `Expected file path: "${originalFilePath}"`)
log(input.isVerbose, `Actual file path used: "${diffToolPath}"`)
if (diffToolPath !== originalFilePath) {
console.log(`❌ File path mismatch detected!`);
log(input.isVerbose, `❌ File path mismatch detected!`)
// Enhanced logging:
if (streamResult?.assistantMessage) {
console.log(` Full model output (assistantMessage):`);
console.log(` -----------------------------------------`);
console.log(` ${streamResult.assistantMessage}`);
console.log(` -----------------------------------------`);
log(input.isVerbose, ` Full model output (assistantMessage):`)
log(input.isVerbose, ` -----------------------------------------`)
log(input.isVerbose, ` ${streamResult.assistantMessage}`)
log(input.isVerbose, ` -----------------------------------------`)
}
if (toolCall) {
console.log(` Parsed tool call that caused mismatch:`);
console.log(` ${JSON.stringify(toolCall, null, 2)}`);
console.log(` -----------------------------------------`);
log(input.isVerbose, ` Parsed tool call that caused mismatch:`)
log(input.isVerbose, ` ${JSON.stringify(toolCall, null, 2)}`)
log(input.isVerbose, ` -----------------------------------------`)
}
return {
success: false,
@@ -308,10 +311,18 @@ export async function runSingleEvaluation(input: TestInput): Promise<TestResult>
// checking if the diff edit succeeds, if it failed it will throw an error
let diffSuccess = true
let replacementData: any = undefined
try {
await constructNewFileContent(diffToolContent, originalFile, true)
const result = await constructNewFileContent(diffToolContent, originalFile, true)
// Check if result is an object with replacements (new format)
if (typeof result === 'object' && result !== null && 'replacements' in result) {
replacementData = result.replacements
}
// If it's just a string, diffSuccess stays true and replacementData stays undefined
} catch (error: any) {
diffSuccess = false
log(input.isVerbose, `ERROR: ${error}`)
}
return {
@@ -320,6 +331,7 @@ export async function runSingleEvaluation(input: TestInput): Promise<TestResult>
toolCalls: detectedToolCalls,
diffEdit: diffToolContent,
diffEditSuccess: diffSuccess,
replacementData: replacementData,
}
} catch (error: any) {
return {
+23 -11
View File
@@ -3,10 +3,11 @@ import { parseAssistantMessageV2, AssistantMessageContent } from "./parsing/pars
import { constructNewFileContent as constructNewFileContent_06_06_25 } from "./diff-apply/diff-06-06-25"
import { constructNewFileContent as constructNewFileContent_06_23_25 } from "./diff-apply/diff-06-23-25"
import { constructNewFileContent as constructNewFileContent_06_25_25 } from "./diff-apply/diff-06-25-25"
import { constructNewFileContent as constructNewFileContent_06_26_25 } from "./diff-apply/diff-06-26-25"
import { constructNewFileContent as constructNewFileContentV3 } from "../../src/core/assistant-message/diff"
import { basicSystemPrompt } from "./prompts/basicSystemPrompt-06-06-25"
import { claude4SystemPrompt } from "./prompts/claude4SystemPrompt-06-06-25"
import { formatResponse } from "./helpers"
import { formatResponse, log } from "./helpers"
import { Anthropic } from "@anthropic-ai/sdk"
import * as fs from "fs"
import * as path from "path"
@@ -39,12 +40,6 @@ const encoding = get_encoding("cl100k_base");
let openRouterModelDataGlobal: Record<string, EvalOpenRouterModelInfo> = {}; // Global to store fetched data
function log(isVerbose: boolean, message: string) {
if (isVerbose) {
console.log(message)
}
}
const systemPromptGeneratorLookup: Record<string, ConstructSystemPromptFn> = {
basicSystemPrompt: basicSystemPrompt,
claude4SystemPrompt: claude4SystemPrompt,
@@ -484,6 +479,7 @@ class NodeTestRunner {
"diff-06-06-25": constructNewFileContent_06_06_25,
"diff-06-23-25": constructNewFileContent_06_23_25,
"diff-06-25-25": constructNewFileContent_06_25_25,
"diff-06-26-25": constructNewFileContent_06_26_25,
constructNewFileContentV3: constructNewFileContentV3,
}
const constructNewFileContent = diffEditingFunctions[diffApplyFile]
@@ -639,6 +635,7 @@ class NodeTestRunner {
thinkingBudgetTokens: testConfig.thinking_tokens_budget,
originalDiffEditToolCallMessage: testConfig.replay ? testCase.original_diff_edit_tool_call_message : undefined,
diffApplyFile: testConfig.diff_apply_file,
isVerbose: isVerbose,
}
if (isVerbose) {
@@ -805,8 +802,8 @@ class NodeTestRunner {
log(isVerbose, `Warning: Failed to store result in database: ${error}`);
}
// Safety check to prevent infinite loops - limit to 10 attempts per valid attempt requested
if (totalAttempts >= testConfig.number_of_runs * 10) {
// Safety check to prevent infinite loops - use configurable max attempts limit
if (totalAttempts >= testConfig.max_attempts_per_case) {
log(isVerbose, ` ⚠️ Reached maximum attempts (${totalAttempts}) for test case ${testCase.test_id}. Only got ${validAttempts}/${testConfig.number_of_runs} valid attempts.`);
break;
}
@@ -925,14 +922,16 @@ async function main() {
.option("--model-ids <model_ids>", "Comma-separated list of model IDs to test")
.option("--system-prompt-name <name>", "The name of the system prompt to use", "basicSystemPrompt")
.option("-n, --valid-attempts-per-case <number>", "Number of valid attempts per test case per model (will retry until this many valid attempts are collected)", "1")
.option("--max-attempts-per-case <number>", "Maximum total attempts per test case (default: 10x valid attempts)")
.option("--max-cases <number>", "Maximum number of test cases to run (limits total cases loaded)")
.option("--parsing-function <name>", "The parsing function to use", "parseAssistantMessageV2")
.option("--diff-edit-function <name>", "The diff editing function to use", "constructNewFileContentV2")
.option("--diff-edit-function <name>", "The diff editing function to use", "diff-06-25-25")
.option("--thinking-budget <tokens>", "Set the thinking tokens budget", "0")
.option("--parallel", "Run tests in parallel", false)
.option("--replay", "Run evaluation from a pre-recorded LLM output, skipping the API call", false)
.option("--replay-run-id <run_id>", "The ID of the run to replay from the database")
.option("--diff-apply-file <filename>", "The name of the diff apply file to use for the replay")
.option("--save-locally", "Save results to local JSON files in addition to database", false)
.option("-v, --verbose", "Enable verbose logging", false)
.option("--max-concurrency <number>", "Maximum number of parallel requests", "80")
@@ -943,6 +942,7 @@ async function main() {
const isVerbose = options.verbose
const testPath = options.testPath
const outputPath = options.outputPath
const saveLocally = options.saveLocally
const maxConcurrency = parseInt(options.maxConcurrency, 10);
// Parse model IDs from comma-separated string
@@ -953,6 +953,11 @@ async function main() {
}
const validAttemptsPerCase = parseInt(options.validAttemptsPerCase, 10);
// Compute dynamic default for max attempts: 10x valid attempts if not specified
const maxAttemptsPerCase = options.maxAttemptsPerCase
? parseInt(options.maxAttemptsPerCase, 10)
: validAttemptsPerCase * 10;
const runner = new NodeTestRunner(options.replay || !!options.replayRunId)
@@ -1058,6 +1063,7 @@ async function main() {
model_id: modelId,
system_prompt_name: options.systemPromptName,
number_of_runs: validAttemptsPerCase,
max_attempts_per_case: maxAttemptsPerCase,
parsing_function: options.parsingFunction,
diff_edit_function: options.diffEditFunction,
thinking_tokens_budget: parseInt(options.thinkingBudget, 10),
@@ -1125,7 +1131,7 @@ async function main() {
remainingTasks = remainingTasks.filter(task => {
const taskId = `${task.modelId}-${task.testCase.test_id}`;
if (taskStates[taskId].total >= validAttemptsPerCase * 10) {
if (taskStates[taskId].total >= task.testConfig.max_attempts_per_case) {
log(isVerbose, ` ⚠️ Reached maximum attempts for ${task.testCase.test_id} with ${task.modelId}.`);
return false;
}
@@ -1150,6 +1156,12 @@ async function main() {
const durationSeconds = ((endTime - startTime) / 1000).toFixed(2)
log(isVerbose, `\n-Total execution time: ${durationSeconds} seconds`)
// Save results locally if requested
if (saveLocally) {
runner.saveTestResults(results, outputPath);
log(isVerbose, `✓ Results also saved to JSON files in ${outputPath}`);
}
log(isVerbose, `\n✓ All results stored in database. Use the dashboard to view results.`)
} catch (error) {
console.error("\nError running tests:", error)
@@ -0,0 +1,960 @@
const SEARCH_BLOCK_START = "------- SEARCH"
const SEARCH_BLOCK_END = "======="
const REPLACE_BLOCK_END = "+++++++ REPLACE"
const SEARCH_BLOCK_CHAR = "-"
const REPLACE_BLOCK_CHAR = "+"
const LEGACY_SEARCH_BLOCK_CHAR = "<"
const LEGACY_REPLACE_BLOCK_CHAR = ">"
// Replace the exact string constants with flexible regex patterns
const SEARCH_BLOCK_START_REGEX = /^[-]{3,} SEARCH>?$/
const LEGACY_SEARCH_BLOCK_START_REGEX = /^[<]{3,} SEARCH>?$/
const SEARCH_BLOCK_END_REGEX = /^[=]{3,}$/
const REPLACE_BLOCK_END_REGEX = /^[+]{3,} REPLACE>?$/
const LEGACY_REPLACE_BLOCK_END_REGEX = /^[>]{3,} REPLACE>?$/
// Similarity thresholds for block anchor fallback matching
const SINGLE_CANDIDATE_SIMILARITY_THRESHOLD = 0.0
const MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD = 0.0
/**
* Levenshtein distance algorithm implementation
*/
function levenshtein(a: string, b: string): number {
// Handle empty strings
if (a === "" || b === "") {
return Math.max(a.length, b.length)
}
const matrix = Array.from({ length: a.length + 1 }, (_, i) =>
Array.from({ length: b.length + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)),
)
for (let i = 1; i <= a.length; i++) {
for (let j = 1; j <= b.length; j++) {
const cost = a[i - 1] === b[j - 1] ? 0 : 1
matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost)
}
}
return matrix[a.length][b.length]
}
// Helper functions to check if a line matches the flexible patterns
function isSearchBlockStart(line: string): boolean {
return SEARCH_BLOCK_START_REGEX.test(line) || LEGACY_SEARCH_BLOCK_START_REGEX.test(line)
}
function isSearchBlockEnd(line: string): boolean {
return SEARCH_BLOCK_END_REGEX.test(line)
}
function isReplaceBlockEnd(line: string): boolean {
return REPLACE_BLOCK_END_REGEX.test(line) || LEGACY_REPLACE_BLOCK_END_REGEX.test(line)
}
/**
* Attempts a line-trimmed fallback match for the given search content in the original content.
* It tries to match `searchContent` lines against a block of lines in `originalContent` starting
* from `lastProcessedIndex`. Lines are matched by trimming leading/trailing whitespace and ensuring
* they are identical afterwards.
*
* Returns [matchIndexStart, matchIndexEnd] if found, or false if not found.
*/
function lineTrimmedFallbackMatch(originalContent: string, searchContent: string, startIndex: number): [number, number] | false {
// Split both contents into lines
const originalLines = originalContent.split("\n")
const searchLines = searchContent.split("\n")
// Trim trailing empty line if exists (from the trailing \n in searchContent)
if (searchLines[searchLines.length - 1] === "") {
searchLines.pop()
}
// Find the line number where startIndex falls
let startLineNum = 0
let currentIndex = 0
while (currentIndex < startIndex && startLineNum < originalLines.length) {
currentIndex += originalLines[startLineNum].length + 1 // +1 for \n
startLineNum++
}
// For each possible starting position in original content
for (let i = startLineNum; i <= originalLines.length - searchLines.length; i++) {
let matches = true
// Try to match all search lines from this position
for (let j = 0; j < searchLines.length; j++) {
const originalTrimmed = originalLines[i + j].trim()
const searchTrimmed = searchLines[j].trim()
if (originalTrimmed !== searchTrimmed) {
matches = false
break
}
}
// If we found a match, calculate the exact character positions
if (matches) {
// Find start character index
let matchStartIndex = 0
for (let k = 0; k < i; k++) {
matchStartIndex += originalLines[k].length + 1 // +1 for \n
}
// Find end character index
let matchEndIndex = matchStartIndex
for (let k = 0; k < searchLines.length; k++) {
matchEndIndex += originalLines[i + k].length + 1 // +1 for \n
}
return [matchStartIndex, matchEndIndex]
}
}
return false
}
/**
* Attempts to match blocks of code by using the first and last lines as anchors,
* with similarity checking to prevent false positives.
* This is a third-tier fallback strategy that helps match blocks where we can identify
* the correct location by matching the beginning and end, even if the exact content
* differs slightly.
*
* The matching strategy:
* 1. Only attempts to match blocks of 3 or more lines to avoid false positives
* 2. Extracts from the search content:
* - First line as the "start anchor"
* - Last line as the "end anchor"
* 3. Collects all candidate positions where both anchors match
* 4. Uses levenshtein distance to calculate similarity of middle lines
* 5. Returns match only if similarity meets threshold requirements
*
* This approach is particularly useful for matching blocks of code where:
* - The exact content might have minor differences
* - The beginning and end of the block are distinctive enough to serve as anchors
* - The overall structure (number of lines) remains the same
* - The middle content is reasonably similar (prevents false positives)
*
* @param originalContent - The full content of the original file
* @param searchContent - The content we're trying to find in the original file
* @param startIndex - The character index in originalContent where to start searching
* @returns A tuple of [startIndex, endIndex] if a match is found, false otherwise
*/
function blockAnchorFallbackMatch(originalContent: string, searchContent: string, startIndex: number): [number, number, number] | false {
const originalLines = originalContent.split("\n")
const searchLines = searchContent.split("\n")
// Only use this approach for blocks of 3+ lines
if (searchLines.length < 3) {
return false
}
// Trim trailing empty line if exists
if (searchLines[searchLines.length - 1] === "") {
searchLines.pop()
}
const firstLineSearch = searchLines[0].trim()
const lastLineSearch = searchLines[searchLines.length - 1].trim()
const searchBlockSize = searchLines.length
// Find the line number where startIndex falls
let startLineNum = 0
let currentIndex = 0
while (currentIndex < startIndex && startLineNum < originalLines.length) {
currentIndex += originalLines[startLineNum].length + 1
startLineNum++
}
// Collect all candidate positions
const candidates: number[] = []
for (let i = startLineNum; i <= originalLines.length - searchBlockSize; i++) {
if (originalLines[i].trim() === firstLineSearch && originalLines[i + searchBlockSize - 1].trim() === lastLineSearch) {
candidates.push(i)
}
}
// Return immediately if no candidates
if (candidates.length === 0) {
return false
}
// Handle single candidate scenario (using relaxed threshold)
if (candidates.length === 1) {
const i = candidates[0]
let similarity = 0
let linesToCheck = searchBlockSize - 2
for (let j = 1; j < searchBlockSize - 1; j++) {
const originalLine = originalLines[i + j].trim()
const searchLine = searchLines[j].trim()
const maxLen = Math.max(originalLine.length, searchLine.length)
if (maxLen === 0) {
continue
}
const distance = levenshtein(originalLine, searchLine)
similarity += (1 - distance / maxLen) / linesToCheck
// Exit early when threshold is reached
if (similarity >= SINGLE_CANDIDATE_SIMILARITY_THRESHOLD) {
break
}
}
if (similarity >= SINGLE_CANDIDATE_SIMILARITY_THRESHOLD) {
let matchStartIndex = 0
for (let k = 0; k < i; k++) {
matchStartIndex += originalLines[k].length + 1
}
let matchEndIndex = matchStartIndex
for (let k = 0; k < searchBlockSize; k++) {
matchEndIndex += originalLines[i + k].length + 1
}
return [matchStartIndex, matchEndIndex, similarity]
}
return false
}
// Calculate similarity for multiple candidates
let bestMatchIndex = -1
let maxSimilarity = -1
for (const i of candidates) {
let similarity = 0
for (let j = 1; j < searchBlockSize - 1; j++) {
const originalLine = originalLines[i + j].trim()
const searchLine = searchLines[j].trim()
const maxLen = Math.max(originalLine.length, searchLine.length)
if (maxLen === 0) {
continue
}
const distance = levenshtein(originalLine, searchLine)
similarity += 1 - distance / maxLen
}
similarity /= searchBlockSize - 2 // Average similarity
if (similarity > maxSimilarity) {
maxSimilarity = similarity
bestMatchIndex = i
}
}
// Threshold judgment
if (maxSimilarity >= MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD) {
const i = bestMatchIndex
let matchStartIndex = 0
for (let k = 0; k < i; k++) {
matchStartIndex += originalLines[k].length + 1
}
let matchEndIndex = matchStartIndex
for (let k = 0; k < searchBlockSize; k++) {
matchEndIndex += originalLines[i + k].length + 1
}
return [matchStartIndex, matchEndIndex, maxSimilarity]
}
return false
}
/**
* This function reconstructs the file content by applying a streamed diff (in a
* specialized SEARCH/REPLACE block format) to the original file content. It is designed
* to handle both incremental updates and the final resulting file after all chunks have
* been processed.
*
* The diff format is a custom structure that uses three markers to define changes:
*
* ------- SEARCH
* [Exact content to find in the original file]
* =======
* [Content to replace with]
* +++++++ REPLACE
*
* Behavior and Assumptions:
* 1. The file is processed chunk-by-chunk. Each chunk of `diffContent` may contain
* partial or complete SEARCH/REPLACE blocks. By calling this function with each
* incremental chunk (with `isFinal` indicating the last chunk), the final reconstructed
* file content is produced.
*
* 2. Matching Strategy (in order of attempt):
* a. Exact Match: First attempts to find the exact SEARCH block text in the original file
* b. Line-Trimmed Match: Falls back to line-by-line comparison ignoring leading/trailing whitespace
* c. Block Anchor Match: For blocks of 3+ lines, tries to match using first/last lines as anchors
* If all matching strategies fail, an error is thrown.
*
* 3. Empty SEARCH Section:
* - If SEARCH is empty and the original file is empty, this indicates creating a new file
* (pure insertion).
* - If SEARCH is empty and the original file is not empty, this indicates a complete
* file replacement (the entire original content is considered matched and replaced).
*
* 4. Applying Changes:
* - Before encountering the "=======" marker, lines are accumulated as search content.
* - After "=======" and before ">>>>>>> REPLACE", lines are accumulated as replacement content.
* - Once the block is complete (">>>>>>> REPLACE"), the matched section in the original
* file is replaced with the accumulated replacement lines, and the position in the original
* file is advanced.
*
* 5. Incremental Output:
* - As soon as the match location is found and we are in the REPLACE section, each new
* replacement line is appended to the result so that partial updates can be viewed
* incrementally.
*
* 6. Partial Markers:
* - If the final line of the chunk looks like it might be part of a marker but is not one
* of the known markers, it is removed. This prevents incomplete or partial markers
* from corrupting the output.
*
* 7. Finalization:
* - Once all chunks have been processed (when `isFinal` is true), any remaining original
* content after the last replaced section is appended to the result.
* - Trailing newlines are not forcibly added. The code tries to output exactly what is specified.
*
* Errors:
* - If the search block cannot be matched using any of the available matching strategies,
* an error is thrown.
*/
export async function constructNewFileContent(
diffContent: string,
originalContent: string,
isFinal: boolean,
version: "v1" | "v2" = "v1",
): Promise<any> {
const constructor = constructNewFileContentVersionMapping[version]
if (!constructor) {
throw new Error(`Invalid version '${version}' for file content constructor`)
}
return constructor(diffContent, originalContent, isFinal)
}
const constructNewFileContentVersionMapping: Record<
string,
(diffContent: string, originalContent: string, isFinal: boolean) => Promise<any>
> = {
v1: constructNewFileContentV1,
v2: constructNewFileContentV2,
} as const
async function constructNewFileContentV1(diffContent: string, originalContent: string, isFinal: boolean): Promise<{
content: string;
replacements: Array<{
start: number;
end: number;
content: string;
method: string;
similarity: number;
searchContent: string;
matchedText: string;
}>;
}> {
let result = ""
let lastProcessedIndex = 0
let currentSearchContent = ""
let currentReplaceContent = ""
let inSearch = false
let inReplace = false
let searchMatchIndex = -1
let searchEndIndex = -1
let matchMethod = ""
let similarityScore = -1.0
// Track all replacements to handle out-of-order edits
let replacements: Array<{
start: number;
end: number;
content: string;
method: string;
similarity: number;
searchContent: string;
matchedText: string;
}> = []
let pendingOutOfOrderReplacement = false
let lines = diffContent.split("\n")
// If the last line looks like a partial marker but isn't recognized,
// remove it because it might be incomplete.
const lastLine = lines[lines.length - 1]
if (
lines.length > 0 &&
(lastLine.startsWith(SEARCH_BLOCK_CHAR) ||
lastLine.startsWith(LEGACY_SEARCH_BLOCK_CHAR) ||
lastLine.startsWith("=") ||
lastLine.startsWith(REPLACE_BLOCK_CHAR) ||
lastLine.startsWith(LEGACY_REPLACE_BLOCK_CHAR)) &&
!isSearchBlockStart(lastLine) &&
!isSearchBlockEnd(lastLine) &&
!isReplaceBlockEnd(lastLine)
) {
lines.pop()
}
for (const line of lines) {
if (isSearchBlockStart(line)) {
inSearch = true
currentSearchContent = ""
currentReplaceContent = ""
continue
}
if (isSearchBlockEnd(line)) {
inSearch = false
inReplace = true
// Remove trailing linebreak for adding the === marker
// if (currentSearchContent.endsWith("\r\n")) {
// currentSearchContent = currentSearchContent.slice(0, -2)
// } else if (currentSearchContent.endsWith("\n")) {
// currentSearchContent = currentSearchContent.slice(0, -1)
// }
if (!currentSearchContent) {
// Empty search block
if (originalContent.length === 0) {
// New file scenario: nothing to match, just start inserting
searchMatchIndex = 0
searchEndIndex = 0
matchMethod = "empty_new_file"
} else {
// ERROR: Empty search block with non-empty file indicates malformed SEARCH marker
throw new Error(
"Empty SEARCH block detected with non-empty file. This usually indicates a malformed SEARCH marker.\n" +
"Please ensure your SEARCH marker follows the correct format:\n" +
"- Use '------- SEARCH' (7+ dashes + space + SEARCH)\n",
)
}
} else {
// Add check for inefficient full-file search
// if (currentSearchContent.trim() === originalContent.trim()) {
// throw new Error(
// "The SEARCH block contains the entire file content. Please either:\n" +
// "1. Use an empty SEARCH block to replace the entire file, or\n" +
// "2. Make focused changes to specific parts of the file that need modification.",
// )
// }
// Exact search match scenario
const exactIndex = originalContent.indexOf(currentSearchContent, lastProcessedIndex)
if (exactIndex !== -1) {
searchMatchIndex = exactIndex
searchEndIndex = exactIndex + currentSearchContent.length
matchMethod = "exact_match"
} else {
// Attempt fallback line-trimmed matching
const lineMatch = lineTrimmedFallbackMatch(originalContent, currentSearchContent, lastProcessedIndex)
if (lineMatch) {
;[searchMatchIndex, searchEndIndex] = lineMatch
matchMethod = "line_trimmed_fallback"
} else {
// Try block anchor fallback for larger blocks
const blockMatch = blockAnchorFallbackMatch(originalContent, currentSearchContent, lastProcessedIndex)
if (blockMatch) {
;[searchMatchIndex, searchEndIndex, similarityScore] = blockMatch
matchMethod = "block_anchor_fallback"
} else {
// Last resort: search the entire file from the beginning
const fullFileIndex = originalContent.indexOf(currentSearchContent, 0)
if (fullFileIndex !== -1) {
// Found in the file - could be out of order
searchMatchIndex = fullFileIndex
searchEndIndex = fullFileIndex + currentSearchContent.length
matchMethod = "full_file_search"
if (searchMatchIndex < lastProcessedIndex) {
pendingOutOfOrderReplacement = true
}
} else {
throw new Error(
`The SEARCH block:\n${currentSearchContent.trimEnd()}\n...does not match anything in the file.`,
)
}
}
}
}
}
// Check if this is an out-of-order replacement
if (searchMatchIndex < lastProcessedIndex) {
pendingOutOfOrderReplacement = true
}
// For in-order replacements, output everything up to the match location
if (!pendingOutOfOrderReplacement) {
result += originalContent.slice(lastProcessedIndex, searchMatchIndex)
}
continue
}
if (isReplaceBlockEnd(line)) {
// Finished one replace block
if (searchMatchIndex === -1) {
throw new Error(
`The SEARCH block:\n${currentSearchContent.trimEnd()}\n...is malformatted.`,
)
}
// Store this replacement
replacements.push({
start: searchMatchIndex,
end: searchEndIndex,
content: currentReplaceContent,
method: matchMethod,
similarity: similarityScore,
searchContent: currentSearchContent,
matchedText: originalContent.slice(searchMatchIndex, searchEndIndex),
})
// If this was an in-order replacement, advance lastProcessedIndex
if (!pendingOutOfOrderReplacement) {
lastProcessedIndex = searchEndIndex
}
// Reset for next block
inSearch = false
inReplace = false
currentSearchContent = ""
currentReplaceContent = ""
searchMatchIndex = -1
searchEndIndex = -1
similarityScore = -1.0
pendingOutOfOrderReplacement = false
continue
}
// Accumulate content for search or replace
// (currentReplaceContent is not being used for anything right now since we directly append to result.)
// (We artificially add a linebreak since we split on \n at the beginning. In order to not include a trailing linebreak in the final search/result blocks we need to remove it before using them. This allows for partial line matches to be correctly identified.)
// NOTE: search/replace blocks must be arranged in the order they appear in the file due to how we build the content using lastProcessedIndex. We also cannot strip the trailing newline since for non-partial lines it would remove the linebreak from the original content. (If we remove end linebreak from search, then we'd also have to remove it from replace but we can't know if it's a partial line or not since the model may be using the line break to indicate the end of the block rather than as part of the search content.) We require the model to output full lines in order for our fallbacks to work as well.
if (inSearch) {
currentSearchContent += line + "\n"
} else if (inReplace) {
currentReplaceContent += line + "\n"
// Only output replacement lines immediately for in-order replacements
if (searchMatchIndex !== -1 && !pendingOutOfOrderReplacement) {
result += line + "\n"
}
}
}
// If this is the final chunk, we need to apply all replacements and build the final result
if (isFinal) {
// Handle the case where we're still in replace mode when processing ends
// and this is the final chunk - treat it as if we encountered the REPLACE marker
if (inReplace && searchMatchIndex !== -1) {
// Store this replacement
replacements.push({
start: searchMatchIndex,
end: searchEndIndex,
content: currentReplaceContent,
method: matchMethod,
similarity: similarityScore,
searchContent: currentSearchContent,
matchedText: originalContent.slice(searchMatchIndex, searchEndIndex),
})
// If this was an in-order replacement, advance lastProcessedIndex
if (!pendingOutOfOrderReplacement) {
lastProcessedIndex = searchEndIndex
}
// Reset state
inSearch = false
inReplace = false
currentSearchContent = ""
currentReplaceContent = ""
searchMatchIndex = -1
searchEndIndex = -1
pendingOutOfOrderReplacement = false
}
// end of handling missing replace marker
// Sort replacements by start position
replacements.sort((a, b) => a.start - b.start)
// Rebuild the entire result by applying all replacements
result = ""
let currentPos = 0
for (const replacement of replacements) {
// Add original content up to this replacement
result += originalContent.slice(currentPos, replacement.start)
// Add the replacement content
result += replacement.content
// Move position to after the replaced section
currentPos = replacement.end
}
// Add any remaining original content
result += originalContent.slice(currentPos)
}
// For testing - return debug info
return {
content: result,
replacements: replacements
}
}
enum ProcessingState {
Idle = 0,
StateSearch = 1 << 0,
StateReplace = 1 << 1,
}
class NewFileContentConstructor {
private originalContent: string
private isFinal: boolean
private state: number
private pendingNonStandardLines: string[]
private result: string
private lastProcessedIndex: number
private currentSearchContent: string
private currentReplaceContent: string
private searchMatchIndex: number
private searchEndIndex: number
constructor(originalContent: string, isFinal: boolean) {
this.originalContent = originalContent
this.isFinal = isFinal
this.pendingNonStandardLines = []
this.result = ""
this.lastProcessedIndex = 0
this.state = ProcessingState.Idle
this.currentSearchContent = ""
this.currentReplaceContent = ""
this.searchMatchIndex = -1
this.searchEndIndex = -1
}
private resetForNextBlock() {
// Reset for next block
this.state = ProcessingState.Idle
this.currentSearchContent = ""
this.currentReplaceContent = ""
this.searchMatchIndex = -1
this.searchEndIndex = -1
}
private findLastMatchingLineIndex(regx: RegExp, lineLimit: number) {
for (let i = lineLimit; i > 0; ) {
i--
if (this.pendingNonStandardLines[i].match(regx)) {
return i
}
}
return -1
}
private updateProcessingState(newState: ProcessingState) {
const isValidTransition =
(this.state === ProcessingState.Idle && newState === ProcessingState.StateSearch) ||
(this.state === ProcessingState.StateSearch && newState === ProcessingState.StateReplace)
if (!isValidTransition) {
throw new Error(
`Invalid state transition.\n` +
"Valid transitions are:\n" +
"- Idle → StateSearch\n" +
"- StateSearch → StateReplace",
)
}
this.state |= newState
}
private isStateActive(state: ProcessingState): boolean {
return (this.state & state) === state
}
private activateReplaceState() {
this.updateProcessingState(ProcessingState.StateReplace)
}
private activateSearchState() {
this.updateProcessingState(ProcessingState.StateSearch)
this.currentSearchContent = ""
this.currentReplaceContent = ""
}
private isSearchingActive(): boolean {
return this.isStateActive(ProcessingState.StateSearch)
}
private isReplacingActive(): boolean {
return this.isStateActive(ProcessingState.StateReplace)
}
private hasPendingNonStandardLines(pendingNonStandardLineLimit: number): boolean {
return this.pendingNonStandardLines.length - pendingNonStandardLineLimit < this.pendingNonStandardLines.length
}
public processLine(line: string) {
this.internalProcessLine(line, true, this.pendingNonStandardLines.length)
}
public getResult() {
// If this is the final chunk, append any remaining original content
if (this.isFinal && this.lastProcessedIndex < this.originalContent.length) {
this.result += this.originalContent.slice(this.lastProcessedIndex)
}
if (this.isFinal && this.state !== ProcessingState.Idle) {
throw new Error("File processing incomplete - SEARCH/REPLACE operations still active during finalization")
}
return this.result
}
private internalProcessLine(
line: string,
canWritependingNonStandardLines: boolean,
pendingNonStandardLineLimit: number,
): number {
let removeLineCount = 0
if (isSearchBlockStart(line)) {
removeLineCount = this.trimPendingNonStandardTrailingEmptyLines(pendingNonStandardLineLimit)
if (removeLineCount > 0) {
pendingNonStandardLineLimit = pendingNonStandardLineLimit - removeLineCount
}
if (this.hasPendingNonStandardLines(pendingNonStandardLineLimit)) {
this.tryFixSearchReplaceBlock(pendingNonStandardLineLimit)
canWritependingNonStandardLines && (this.pendingNonStandardLines.length = 0)
}
this.activateSearchState()
} else if (isSearchBlockEnd(line)) {
// 校验非标内容
if (!this.isSearchingActive()) {
this.tryFixSearchBlock(pendingNonStandardLineLimit)
canWritependingNonStandardLines && (this.pendingNonStandardLines.length = 0)
}
this.activateReplaceState()
this.beforeReplace()
} else if (isReplaceBlockEnd(line)) {
if (!this.isReplacingActive()) {
this.tryFixReplaceBlock(pendingNonStandardLineLimit)
canWritependingNonStandardLines && (this.pendingNonStandardLines.length = 0)
}
this.lastProcessedIndex = this.searchEndIndex
this.resetForNextBlock()
} else {
// Accumulate content for search or replace
// (currentReplaceContent is not being used for anything right now since we directly append to result.)
// (We artificially add a linebreak since we split on \n at the beginning. In order to not include a trailing linebreak in the final search/result blocks we need to remove it before using them. This allows for partial line matches to be correctly identified.)
// NOTE: search/replace blocks must be arranged in the order they appear in the file due to how we build the content using lastProcessedIndex. We also cannot strip the trailing newline since for non-partial lines it would remove the linebreak from the original content. (If we remove end linebreak from search, then we'd also have to remove it from replace but we can't know if it's a partial line or not since the model may be using the line break to indicate the end of the block rather than as part of the search content.) We require the model to output full lines in order for our fallbacks to work as well.
if (this.isReplacingActive()) {
this.currentReplaceContent += line + "\n"
// Output replacement lines immediately if we know the insertion point
if (this.searchMatchIndex !== -1) {
this.result += line + "\n"
}
} else if (this.isSearchingActive()) {
this.currentSearchContent += line + "\n"
} else {
let appendToPendingNonStandardLines = canWritependingNonStandardLines
if (appendToPendingNonStandardLines) {
// 处理非标内容
this.pendingNonStandardLines.push(line)
}
}
}
return removeLineCount
}
private beforeReplace() {
// Remove trailing linebreak for adding the === marker
// if (currentSearchContent.endsWith("\r\n")) {
// currentSearchContent = currentSearchContent.slice(0, -2)
// } else if (currentSearchContent.endsWith("\n")) {
// currentSearchContent = currentSearchContent.slice(0, -1)
// }
if (!this.currentSearchContent) {
// Empty search block
if (this.originalContent.length === 0) {
// New file scenario: nothing to match, just start inserting
this.searchMatchIndex = 0
this.searchEndIndex = 0
} else {
// Complete file replacement scenario: treat the entire file as matched
this.searchMatchIndex = 0
this.searchEndIndex = this.originalContent.length
}
} else {
// Add check for inefficient full-file search
// if (currentSearchContent.trim() === originalContent.trim()) {
// throw new Error(
// "The SEARCH block contains the entire file content. Please either:\n" +
// "1. Use an empty SEARCH block to replace the entire file, or\n" +
// "2. Make focused changes to specific parts of the file that need modification.",
// )
// }
// Exact search match scenario
const exactIndex = this.originalContent.indexOf(this.currentSearchContent, this.lastProcessedIndex)
if (exactIndex !== -1) {
this.searchMatchIndex = exactIndex
this.searchEndIndex = exactIndex + this.currentSearchContent.length
} else {
// Attempt fallback line-trimmed matching
const lineMatch = lineTrimmedFallbackMatch(
this.originalContent,
this.currentSearchContent,
this.lastProcessedIndex,
)
if (lineMatch) {
;[this.searchMatchIndex, this.searchEndIndex] = lineMatch
} else {
// Try block anchor fallback for larger blocks
const blockMatch = blockAnchorFallbackMatch(
this.originalContent,
this.currentSearchContent,
this.lastProcessedIndex,
)
if (blockMatch) {
;[this.searchMatchIndex, this.searchEndIndex, /* ignore similarity */] = blockMatch
} else {
throw new Error(
`The SEARCH block:\n${this.currentSearchContent.trimEnd()}\n...does not match anything in the file.`,
)
}
}
}
}
if (this.searchMatchIndex < this.lastProcessedIndex) {
throw new Error(
`The SEARCH block:\n${this.currentSearchContent.trimEnd()}\n...matched an incorrect content in the file.`,
)
}
// Output everything up to the match location
this.result += this.originalContent.slice(this.lastProcessedIndex, this.searchMatchIndex)
}
private tryFixSearchBlock(lineLimit: number): number {
let removeLineCount = 0
if (lineLimit < 0) {
lineLimit = this.pendingNonStandardLines.length
}
if (!lineLimit) {
throw new Error("Invalid SEARCH/REPLACE block structure - no lines available to process")
}
let searchTagRegexp = /^([-]{3,}|[<]{3,}) SEARCH$/
const searchTagIndex = this.findLastMatchingLineIndex(searchTagRegexp, lineLimit)
if (searchTagIndex !== -1) {
let fixLines = this.pendingNonStandardLines.slice(searchTagIndex, lineLimit)
fixLines[0] = SEARCH_BLOCK_START
for (const line of fixLines) {
removeLineCount += this.internalProcessLine(line, false, searchTagIndex)
}
} else {
throw new Error(
`Invalid REPLACE marker detected - could not find matching SEARCH block starting from line ${searchTagIndex + 1}`,
)
}
return removeLineCount
}
private tryFixReplaceBlock(lineLimit: number): number {
let removeLineCount = 0
if (lineLimit < 0) {
lineLimit = this.pendingNonStandardLines.length
}
if (!lineLimit) {
throw new Error()
}
let replaceBeginTagRegexp = /^[=]{3,}$/
const replaceBeginTagIndex = this.findLastMatchingLineIndex(replaceBeginTagRegexp, lineLimit)
if (replaceBeginTagIndex !== -1) {
// // 校验非标内容
// if (!this.isSearchingActive()) {
// removeLineCount += this.tryFixSearchBlock(replaceBeginTagIndex)
// }
let fixLines = this.pendingNonStandardLines.slice(replaceBeginTagIndex - removeLineCount, lineLimit - removeLineCount)
fixLines[0] = SEARCH_BLOCK_END
for (const line of fixLines) {
removeLineCount += this.internalProcessLine(line, false, replaceBeginTagIndex - removeLineCount)
}
} else {
throw new Error(`Malformed REPLACE block - missing valid separator after line ${replaceBeginTagIndex + 1}`)
}
return removeLineCount
}
private tryFixSearchReplaceBlock(lineLimit: number): number {
let removeLineCount = 0
if (lineLimit < 0) {
lineLimit = this.pendingNonStandardLines.length
}
if (!lineLimit) {
throw new Error()
}
let replaceEndTagRegexp = /^([+]{3,}|[>]{3,}) REPLACE$/
const replaceEndTagIndex = this.findLastMatchingLineIndex(replaceEndTagRegexp, lineLimit)
const likeReplaceEndTag = replaceEndTagIndex === lineLimit - 1
if (likeReplaceEndTag) {
// // 校验非标内容
// if (!this.isReplacingActive()) {
// removeLineCount += this.tryFixReplaceBlock(replaceEndTagIndex)
// }
let fixLines = this.pendingNonStandardLines.slice(replaceEndTagIndex - removeLineCount, lineLimit - removeLineCount)
fixLines[fixLines.length - 1] = REPLACE_BLOCK_END
for (const line of fixLines) {
removeLineCount += this.internalProcessLine(line, false, replaceEndTagIndex - removeLineCount)
}
} else {
throw new Error("Malformed SEARCH/REPLACE block structure: Missing valid closing REPLACE marker")
}
return removeLineCount
}
/**
* Removes trailing empty lines from the pendingNonStandardLines array
* @param lineLimit - The index to start checking from (exclusive).
* Removes empty lines from lineLimit-1 backwards.
* @returns The number of empty lines removed
*/
private trimPendingNonStandardTrailingEmptyLines(lineLimit: number): number {
let removedCount = 0
let i = Math.min(lineLimit, this.pendingNonStandardLines.length) - 1
while (i >= 0 && this.pendingNonStandardLines[i].trim() === "") {
this.pendingNonStandardLines.pop()
removedCount++
i--
}
return removedCount
}
}
export async function constructNewFileContentV2(diffContent: string, originalContent: string, isFinal: boolean): Promise<string> {
let newFileContentConstructor = new NewFileContentConstructor(originalContent, isFinal)
let lines = diffContent.split("\n")
// If the last line looks like a partial marker but isn't recognized,
// remove it because it might be incomplete.
const lastLine = lines[lines.length - 1]
if (
lines.length > 0 &&
(lastLine.startsWith(SEARCH_BLOCK_CHAR) ||
lastLine.startsWith(LEGACY_SEARCH_BLOCK_CHAR) ||
lastLine.startsWith("=") ||
lastLine.startsWith(REPLACE_BLOCK_CHAR) ||
lastLine.startsWith(LEGACY_REPLACE_BLOCK_CHAR)) &&
lastLine !== SEARCH_BLOCK_START &&
lastLine !== SEARCH_BLOCK_END &&
lastLine !== REPLACE_BLOCK_END
) {
lines.pop()
}
for (const line of lines) {
newFileContentConstructor.processLine(line)
}
let result = newFileContentConstructor.getResult()
return result
}
+6
View File
@@ -23,3 +23,9 @@ export const formatResponse = {
return formatImagesIntoBlocks(images)
},
}
export function log(isVerbose: boolean, message: string) {
if (isVerbose) {
console.log(message)
}
}
+3
View File
@@ -29,6 +29,7 @@ export interface TestConfig {
model_id: string
system_prompt_name: string
number_of_runs: number
max_attempts_per_case: number
parsing_function: string
diff_edit_function: string
thinking_tokens_budget: number
@@ -81,6 +82,7 @@ export interface TestResult {
diffEdit?: string
toolCalls?: ExtractedToolCall[]
diffEditSuccess?: boolean
replacementData?: any
error?: string
errorString?: string
}
@@ -102,4 +104,5 @@ export interface TestInput {
thinkingBudgetTokens: number
originalDiffEditToolCallMessage?: string
diffApplyFile?: string
isVerbose: boolean
}
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "claude-dev",
"version": "3.17.16",
"version": "3.18.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.17.16",
"version": "3.18.3",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/sdk": "^0.37.0",
+3 -3
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.17.16",
"version": "3.18.3",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -332,14 +332,14 @@
"package": "npm run check-types && npm run build:webview && npm run lint && node esbuild.js --production",
"protos": "node proto/build-proto.js && node scripts/generate-server-setup.mjs && node scripts/generate-host-bridge-client.mjs",
"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",
"clean": "rimraf dist dist-standalone webview-ui/build src/generated out/",
"compile-tests": "node ./scripts/build-tests.js",
"watch-tests": "tsc -p . -w --outDir out",
"pretest": "npm run compile-tests && npm run compile && npm run compile-standalone && npm run lint",
"check-types": "npm run protos && tsc --noEmit",
"lint": "eslint src --ext ts && eslint webview-ui/src --ext ts && buf lint && cd webview-ui && npm run lint",
"format": "prettier . --check",
"format:fix": "prettier . --write",
"pretest": "npm run compile && npm run compile-tests && npm run compile-standalone && npm run lint",
"test": "npm-run-all test:unit test:integration",
"test:ci": "node scripts/test-ci.js",
"test:integration": "vscode-test",
+31
View File
@@ -0,0 +1,31 @@
// Configuration file for protocol buffer build scripts
// Contains service name mappings used by both build-proto.js and build-go-proto.js
// List of gRPC services
// To add a new service, simply add it to this map and run the build scripts
// The service handler will be automatically discovered and used by grpc-handler.ts
export const serviceNameMap = {
account: "cline.AccountService",
browser: "cline.BrowserService",
checkpoints: "cline.CheckpointsService",
file: "cline.FileService",
mcp: "cline.McpService",
state: "cline.StateService",
task: "cline.TaskService",
web: "cline.WebService",
models: "cline.ModelsService",
slash: "cline.SlashService",
ui: "cline.UiService",
// Add new services here - no other code changes needed!
}
// List of host gRPC services (IDE API bridge)
// These services are implemented in the IDE extension and called by the standalone Cline Core
export const hostServiceNameMap = {
uri: "host.UriService",
watch: "host.WatchService",
workspace: "host.WorkspaceService",
env: "host.EnvService",
window: "host.WindowService",
// Add new host services here
}
+18 -45
View File
@@ -9,16 +9,18 @@ import chalk from "chalk"
import os from "os"
import { createRequire } from "module"
import { serviceNameMap, hostServiceNameMap } from "./build-proto-config.js"
const require = createRequire(import.meta.url)
const PROTOC = path.join(require.resolve("grpc-tools"), "../bin/protoc")
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url))
const ROOT_DIR = path.resolve(SCRIPT_DIR, "..")
const TS_OUT_DIR = path.join(ROOT_DIR, "src/shared/proto")
const GRPC_JS_OUT_DIR = path.join(ROOT_DIR, "src/generated/grpc-js")
const NICE_JS_OUT_DIR = path.join(ROOT_DIR, "src/generated/nice-grpc")
const DESCRIPTOR_OUT_DIR = path.join(ROOT_DIR, "dist-standalone/proto")
const TS_OUT_DIR = path.join(ROOT_DIR, "src", "shared", "proto")
const GRPC_JS_OUT_DIR = path.join(ROOT_DIR, "src", "generated", "grpc-js")
const NICE_JS_OUT_DIR = path.join(ROOT_DIR, "src", "generated", "nice-grpc")
const DESCRIPTOR_OUT_DIR = path.join(ROOT_DIR, "dist-standalone", "proto")
const isWindows = process.platform === "win32"
const TS_PROTO_PLUGIN = isWindows
@@ -34,34 +36,13 @@ const TS_PROTO_OPTIONS = [
"useDate=false", // Timestamp fields will not be automatically converted to Date.
]
// List of gRPC services
// To add a new service, simply add it to this map and run this script
// The service handler will be automatically discovered and used by grpc-handler.ts
const serviceNameMap = {
account: "cline.AccountService",
browser: "cline.BrowserService",
checkpoints: "cline.CheckpointsService",
file: "cline.FileService",
mcp: "cline.McpService",
state: "cline.StateService",
task: "cline.TaskService",
web: "cline.WebService",
models: "cline.ModelsService",
slash: "cline.SlashService",
ui: "cline.UiService",
// Add new services here - no other code changes needed!
}
const serviceDirs = Object.keys(serviceNameMap).map((serviceKey) => path.join(ROOT_DIR, "src/core/controller", serviceKey))
// Service directories derived from imported serviceNameMap
const serviceDirs = Object.keys(serviceNameMap).map((serviceKey) => path.join(ROOT_DIR, "src", "core", "controller", serviceKey))
// List of host gRPC services (IDE API bridge)
// These services are implemented in the IDE extension and called by the standalone Cline Core
const hostServiceNameMap = {
uri: "host.UriService",
watch: "host.WatchService",
workspace: "host.WorkspaceService",
// Add new host services here
}
const hostServiceDirs = Object.keys(hostServiceNameMap).map((serviceKey) => path.join(ROOT_DIR, "src/hosts/vscode", serviceKey))
// Host service directories derived from imported hostServiceNameMap
const hostServiceDirs = Object.keys(hostServiceNameMap).map((serviceKey) =>
path.join(ROOT_DIR, "src", "hosts", "vscode", serviceKey),
)
async function main() {
console.log(chalk.bold.blue("Starting Protocol Buffer code generation..."))
@@ -177,7 +158,7 @@ export {
${serviceExports.join(",\n\t")}
}`
const filePath = path.join(ROOT_DIR, "webview-ui/src/services/grpc-client.ts")
const filePath = path.join(ROOT_DIR, "webview-ui", "src", "services", "grpc-client.ts")
await writeFileWithMkdirs(filePath, content)
log_verbose(chalk.green(`Generated gRPC client at ${filePath}`))
}
@@ -386,7 +367,7 @@ export interface ServiceHandlerConfig {
export const serviceHandlers: Record<string, ServiceHandlerConfig> = {${serviceConfigs.join(",")}
};`
const configPath = path.join(ROOT_DIR, "src/core/controller/grpc-service-config.ts")
const configPath = path.join(ROOT_DIR, "src", "core", "controller", "grpc-service-config.ts")
await writeFileWithMkdirs(configPath, content)
log_verbose(chalk.green(`Generated service configuration at ${configPath}`))
}
@@ -602,13 +583,12 @@ async function cleanup() {
for (const file of existingFiles) {
await fs.unlink(path.join(TS_OUT_DIR, file))
}
await rmdir(path.join(ROOT_DIR, "src/generated"))
await rmdir(path.join(ROOT_DIR, "src", "generated"))
// Clean up generated files that were moved.
await fs.rm(path.join(ROOT_DIR, "src/standalone/services/host-grpc-client.ts"), { force: true })
await rmdir(path.join(ROOT_DIR, "src/standalone/services"))
await fs.rm(path.join(ROOT_DIR, "hosts/vscode"), { force: true, recursive: true })
await fs.rm(path.join(ROOT_DIR, "src", "standalone", "services", "host-grpc-client.ts"), { force: true })
await rmdir(path.join(ROOT_DIR, "src", "standalone", "services"))
await fs.rm(path.join(ROOT_DIR, "hosts", "vscode"), { force: true, recursive: true })
await rmdir(path.join(ROOT_DIR, "hosts"))
await fs.rm(path.join(ROOT_DIR, "src/standalone/server-setup.ts"), { force: true })
@@ -636,13 +616,6 @@ async function rmdir(path) {
}
}
function serviceNameWithoutPackage(fullServiceName) {
return fullServiceName.replace(/.*\./, "")
}
function lowercaseFirstChar(str) {
return str.charAt(0).toLowerCase() + str.slice(1)
}
// Check for Apple Silicon compatibility
function checkAppleSiliconCompatibility() {
// Only run check on macOS
+16
View File
@@ -0,0 +1,16 @@
syntax = "proto3";
package host;
option java_package = "bot.cline.host.proto";
option java_multiple_files = true;
import "common.proto";
// Provides methods for working with the user's environment.
service EnvService {
// Writes text to the system clipboard.
rpc clipboardWriteText(cline.StringRequest) returns (cline.Empty);
// Reads text from the system clipboard.
rpc clipboardReadText(cline.EmptyRequest) returns (cline.String);
}
+48
View File
@@ -0,0 +1,48 @@
syntax = "proto3";
package host;
option java_package = "bot.cline.host.proto";
option java_multiple_files = true;
import "common.proto";
// Provides methods for working with IDE windows and editors.
service WindowService {
// Opens a text document in the editor and returns editor information.
rpc showTextDocument(ShowTextDocumentRequest) returns (TextEditorInfo);
rpc showOpenDialogue(ShowOpenDialogueRequest) returns (SelectedResources);
}
message ShowTextDocumentRequest {
cline.Metadata metadata = 1;
string path = 2;
optional ShowTextDocumentOptions options = 3;
}
// See https://code.visualstudio.com/api/references/vscode-api#TextDocumentShowOptions
message ShowTextDocumentOptions {
optional bool preview = 1;
optional bool preserve_focus = 2;
optional int32 view_column = 3;
}
message TextEditorInfo {
string document_path = 1;
optional int32 view_column = 2;
bool is_active = 3;
}
message ShowOpenDialogueRequest {
cline.Metadata metadata = 1;
optional bool can_select_many = 2;
optional string open_label = 3;
optional ShowOpenDialogueFilterOption filters = 4;
}
message ShowOpenDialogueFilterOption {
repeated string files = 1;
}
message SelectedResources {
repeated string paths = 1;
}
+1
View File
@@ -18,6 +18,7 @@ service StateService {
rpc updateAutoApprovalSettings(AutoApprovalSettingsRequest) returns (Empty);
rpc updateSettings(UpdateSettingsRequest) returns (Empty);
rpc updateTelemetrySetting(TelemetrySettingRequest) returns (Empty);
rpc setWelcomeViewCompleted(BooleanRequest) returns (Empty);
}
message State {
+1 -9
View File
@@ -23,8 +23,6 @@ service TaskService {
rpc exportTaskWithId(StringRequest) returns (Empty);
// Toggles the favorite status of a task
rpc toggleTaskFavorite(TaskFavoriteRequest) returns (Empty);
// Deletes all non-favorited tasks
rpc deleteNonFavoritedTasks(EmptyRequest) returns (DeleteNonFavoritedTasksResults);
// Gets filtered task history
rpc getTaskHistory(GetTaskHistoryRequest) returns (TaskHistoryArray);
// Sends a response to a previous ask operation
@@ -36,7 +34,7 @@ service TaskService {
// Executes a quick win task with command and title
rpc executeQuickWin(ExecuteQuickWinRequest) returns (Empty);
// Deletes all task history
rpc deleteAllTaskHistory(BooleanRequest) returns (DeleteAllTaskHistoryCount);
rpc deleteAllTaskHistory(EmptyRequest) returns (DeleteAllTaskHistoryCount);
}
// Request message for creating a new task
@@ -68,12 +66,6 @@ message TaskResponse {
int32 cache_reads = 10;
}
// Results returned when deleting non-favorited tasks
message DeleteNonFavoritedTasksResults {
int32 tasks_preserved = 1;
int32 tasks_deleted = 2;
}
// Request for getting task history with filtering
message GetTaskHistoryRequest {
Metadata metadata = 1;
+6 -3
View File
@@ -45,8 +45,10 @@ describe("OllamaHandler", () => {
this.skip()
}
this.timeout(5000)
// Ensure client is initialized
const client = (handler as any).ensureClient()
// Mock the Ollama client's chat method
const chatStub = sinon.stub(handler["client"], "chat").resolves({
const chatStub = sinon.stub(client, "chat").resolves({
[Symbol.asyncIterator]: async function* () {
yield {
message: { content: "Hello, world!" },
@@ -139,8 +141,9 @@ describe("OllamaHandler", () => {
// Restore real timers for this test
clock.restore()
// Mock the Ollama client's chat method to fail on first call and succeed on second
const chatStub = sinon.stub(handler["client"], "chat")
// Ensure client is initialized and mock the Ollama client's chat method to fail on first call and succeed on second
const client = (handler as any).ensureClient()
const chatStub = sinon.stub(client, "chat")
// First call throws an error
chatStub.onFirstCall().rejects(new Error("API Error"))
+22 -7
View File
@@ -7,18 +7,33 @@ import { ApiStream } from "../transform/stream"
export class AnthropicHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: Anthropic
private client: Anthropic | undefined
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new Anthropic({
apiKey: this.options.apiKey,
baseURL: this.options.anthropicBaseUrl || undefined,
})
}
private ensureClient(): Anthropic {
if (!this.client) {
if (!this.options.apiKey) {
throw new Error("Anthropic API key is required")
}
try {
this.client = new Anthropic({
apiKey: this.options.apiKey,
baseURL: this.options.anthropicBaseUrl || undefined,
})
} catch (error) {
throw new Error(`Error creating Anthropic client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
let stream: AnthropicStream<Anthropic.RawMessageStreamEvent>
const modelId = model.id
@@ -44,7 +59,7 @@ export class AnthropicHandler implements ApiHandler {
)
const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
stream = await this.client.messages.create(
stream = await client.messages.create(
{
model: modelId,
thinking: reasoningOn ? { type: "enabled", budget_tokens: budget_tokens } : undefined,
@@ -118,7 +133,7 @@ export class AnthropicHandler implements ApiHandler {
break
}
default: {
stream = await this.client.messages.create({
stream = await client.messages.create({
model: modelId,
max_tokens: model.info.maxTokens || 8192,
temperature: 0,
+39 -14
View File
@@ -7,32 +7,52 @@ import { ApiStream } from "@api/transform/stream"
export class CerebrasHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: Cerebras
private client: Cerebras | undefined
constructor(options: ApiHandlerOptions) {
this.options = options
}
// Clean and validate the API key
const cleanApiKey = this.options.cerebrasApiKey?.trim()
private ensureClient(): Cerebras {
if (!this.client) {
// Clean and validate the API key
const cleanApiKey = this.options.cerebrasApiKey?.trim()
if (!cleanApiKey) {
throw new Error("Cerebras API key is required")
if (!cleanApiKey) {
throw new Error("Cerebras API key is required")
}
try {
this.client = new Cerebras({
apiKey: cleanApiKey,
timeout: 30000, // 30 second timeout
})
} catch (error) {
throw new Error(`Error creating Cerebras client: ${error.message}`)
}
}
this.client = new Cerebras({
apiKey: cleanApiKey,
timeout: 30000, // 30 second timeout
})
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
// Convert Anthropic messages to Cerebras format
const cerebrasMessages: Array<{
role: "system" | "user" | "assistant"
content: string
}> = [{ role: "system", content: systemPrompt }]
// Helper function to strip thinking tags from content
const stripThinkingTags = (content: string): string => {
return content.replace(/<think>[\s\S]*?<\/think>/g, "").trim()
}
// Check if this is a reasoning model that uses thinking tags
const modelId = this.getModel().id
const isReasoningModel = modelId.includes("qwen") || modelId.includes("deepseek-r1-distill")
// Convert Anthropic messages to Cerebras format
for (const message of messages) {
if (message.role === "user") {
@@ -50,7 +70,7 @@ export class CerebrasHandler implements ApiHandler {
: message.content
cerebrasMessages.push({ role: "user", content })
} else if (message.role === "assistant") {
const content = Array.isArray(message.content)
let content = Array.isArray(message.content)
? message.content
.map((block) => {
if (block.type === "text") {
@@ -60,12 +80,19 @@ export class CerebrasHandler implements ApiHandler {
})
.join("\n")
: message.content || ""
// Strip thinking tags from assistant messages for reasoning models
// so the model doesn't see its own thinking in the conversation history
if (isReasoningModel) {
content = stripThinkingTags(content)
}
cerebrasMessages.push({ role: "assistant", content })
}
}
try {
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: this.getModel().id,
messages: cerebrasMessages,
temperature: 0,
@@ -74,8 +101,6 @@ export class CerebrasHandler implements ApiHandler {
// Handle streaming response
let reasoning: string | null = null // Track reasoning content for models that support thinking
const modelId = this.getModel().id
const isReasoningModel = modelId.includes("qwen") || modelId.includes("deepseek-r1-distill")
for await (const chunk of stream as any) {
// Type assertion for the streaming chunk
+1
View File
@@ -27,6 +27,7 @@ export class ClaudeCodeHandler implements ApiHandler {
messages: filteredMessages,
path: this.options.claudeCodePath,
modelId: this.getModel().id,
thinkingBudgetTokens: this.options.thinkingBudgetTokens,
})
// Usage is included with assistant messages,
+81 -27
View File
@@ -10,28 +10,42 @@ import { withRetry } from "../retry"
export class ClineHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
private client: OpenAI | undefined
lastGenerationId?: string
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://api.cline.bot/v1",
apiKey: this.options.clineApiKey || "",
defaultHeaders: {
"HTTP-Referer": "https://cline.bot", // Optional, for including your app on cline.bot rankings.
"X-Title": "Cline", // Optional. Shows in rankings on cline.bot.
"X-Task-ID": this.options.taskId || "", // Include the task ID in the request headers
},
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.clineApiKey) {
throw new Error("You don't seem to be logged in to a Cline account.")
}
try {
this.client = new OpenAI({
baseURL: "https://api.cline.bot/v1",
apiKey: this.options.clineApiKey || "",
defaultHeaders: {
"HTTP-Referer": "https://cline.bot", // Optional, for including your app on cline.bot rankings.
"X-Title": "Cline", // Optional. Shows in rankings on cline.bot.
"X-Task-ID": this.options.taskId || "", // Include the task ID in the request headers
},
})
} catch (error) {
throw new Error(`Error creating Cline client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
this.lastGenerationId = undefined
const stream = await createOpenRouterStream(
this.client,
client,
systemPrompt,
messages,
this.getModel(),
@@ -56,7 +70,21 @@ export class ClineHandler implements ApiHandler {
this.lastGenerationId = chunk.id
}
const delta = chunk.choices[0]?.delta
// Check for mid-stream error via finish_reason
const choice = chunk.choices?.[0]
// OpenRouter may return finish_reason = "error" with error details
if ((choice?.finish_reason as string) === "error") {
const choiceWithError = choice as any
if (choiceWithError.error) {
const error = choiceWithError.error
console.error(`Cline Mid-Stream Error: ${error.code || error.type || "Unknown"} - ${error.message}`)
throw new Error(`Cline Mid-Stream Error: ${error.code || error.type || "Unknown"} - ${error.message}`)
} else {
throw new Error("Cline Mid-Stream Error: Stream terminated with error status but no error details provided")
}
}
const delta = choice?.delta
if (delta?.content) {
yield {
type: "text",
@@ -84,14 +112,26 @@ export class ClineHandler implements ApiHandler {
totalCost = 0
}
yield {
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
// @ts-ignore-next-line
totalCost,
if (modelId.includes("gemini")) {
yield {
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
inputTokens: (chunk.usage.prompt_tokens || 0) - (chunk.usage.prompt_tokens_details?.cached_tokens || 0),
outputTokens: chunk.usage.completion_tokens || 0,
// @ts-ignore-next-line
totalCost,
}
} else {
yield {
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
// @ts-ignore-next-line
totalCost,
}
}
didOutputUsage = true
}
@@ -117,13 +157,27 @@ export class ClineHandler implements ApiHandler {
})
const generation = response.data
return {
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: generation?.native_tokens_cached || 0,
inputTokens: generation?.native_tokens_prompt || 0,
outputTokens: generation?.native_tokens_completion || 0,
totalCost: generation?.total_cost || 0,
let modelId = this.options.openRouterModelId
if (modelId && modelId.includes("gemini")) {
return {
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: generation?.native_tokens_cached || 0,
// openrouter generation endpoint fails often
inputTokens: (generation?.native_tokens_prompt || 0) - (generation?.native_tokens_cached || 0),
outputTokens: generation?.native_tokens_completion || 0,
totalCost: generation?.total_cost || 0,
}
} else {
return {
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: generation?.native_tokens_cached || 0,
// openrouter generation endpoint fails often
inputTokens: generation?.native_tokens_prompt || 0,
outputTokens: generation?.native_tokens_completion || 0,
totalCost: generation?.total_cost || 0,
}
}
} catch (error) {
// ignore if fails
+20 -6
View File
@@ -10,14 +10,27 @@ import { convertToR1Format } from "../transform/r1-format"
export class DeepSeekHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
private client: OpenAI | undefined
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://api.deepseek.com/v1",
apiKey: this.options.deepSeekApiKey,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.deepSeekApiKey) {
throw new Error("DeepSeek API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://api.deepseek.com/v1",
apiKey: this.options.deepSeekApiKey,
})
} catch (error) {
throw new Error(`Error creating DeepSeek client: ${error.message}`)
}
}
return this.client
}
private async *yieldUsage(info: ModelInfo, usage: OpenAI.Completions.CompletionUsage | undefined): ApiStream {
@@ -54,6 +67,7 @@ export class DeepSeekHandler implements ApiHandler {
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
const isDeepseekReasoner = model.id.includes("deepseek-reasoner")
@@ -67,7 +81,7 @@ export class DeepSeekHandler implements ApiHandler {
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
}
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: model.id,
max_completion_tokens: model.info.maxTokens,
messages: openAiMessages,
+20 -6
View File
@@ -8,13 +8,26 @@ import { withRetry } from "../retry"
export class DoubaoHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
private client: OpenAI | undefined
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://ark.cn-beijing.volces.com/api/v3/",
apiKey: this.options.doubaoApiKey,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.doubaoApiKey) {
throw new Error("Doubao API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://ark.cn-beijing.volces.com/api/v3/",
apiKey: this.options.doubaoApiKey,
})
} catch (error) {
throw new Error(`Error creating Doubao client: ${error.message}`)
}
}
return this.client
}
getModel(): { id: DoubaoModelId; info: ModelInfo } {
@@ -31,12 +44,13 @@ export class DoubaoHandler implements ApiHandler {
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: model.id,
max_completion_tokens: model.info.maxTokens,
messages: openAiMessages,
+20 -6
View File
@@ -15,18 +15,32 @@ import { ApiStream } from "../transform/stream"
export class FireworksHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
private client: OpenAI | undefined
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://api.fireworks.ai/inference/v1",
apiKey: this.options.fireworksApiKey,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.fireworksApiKey) {
throw new Error("Fireworks API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://api.fireworks.ai/inference/v1",
apiKey: this.options.fireworksApiKey,
})
} catch (error) {
throw new Error(`Error creating Fireworks client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const modelId = this.options.fireworksModelId ?? ""
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
@@ -34,7 +48,7 @@ export class FireworksHandler implements ApiHandler {
...convertToOpenAiMessages(messages),
]
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: modelId,
...(this.options.fireworksModelMaxCompletionTokens
? { max_completion_tokens: this.options.fireworksModelMaxCompletionTokens }
+35 -18
View File
@@ -38,30 +38,45 @@ interface GeminiHandlerOptions extends ApiHandlerOptions {
*/
export class GeminiHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: GoogleGenAI
private client: GoogleGenAI | undefined
constructor(options: GeminiHandlerOptions) {
// Store the options
this.options = options
}
if (options.isVertex) {
// Initialize with Vertex AI configuration
const project = this.options.vertexProjectId ?? "not-provided"
const location = this.options.vertexRegion ?? "not-provided"
private ensureClient(): GoogleGenAI {
if (!this.client) {
const options = this.options as GeminiHandlerOptions
this.client = new GoogleGenAI({
vertexai: true,
project,
location,
})
} else {
// Initialize with standard API key
if (!options.geminiApiKey) {
throw new Error("API key is required for Google Gemini when not using Vertex AI")
if (options.isVertex) {
// Initialize with Vertex AI configuration
const project = this.options.vertexProjectId ?? "not-provided"
const location = this.options.vertexRegion ?? "not-provided"
try {
this.client = new GoogleGenAI({
vertexai: true,
project,
location,
})
} catch (error) {
throw new Error(`Error creating Gemini Vertex AI client: ${error.message}`)
}
} else {
// Initialize with standard API key
if (!options.geminiApiKey) {
throw new Error("API key is required for Google Gemini when not using Vertex AI")
}
try {
this.client = new GoogleGenAI({ apiKey: options.geminiApiKey })
} catch (error) {
throw new Error(`Error creating Gemini client: ${error.message}`)
}
}
this.client = new GoogleGenAI({ apiKey: options.geminiApiKey })
}
return this.client
}
/**
@@ -80,6 +95,7 @@ export class GeminiHandler implements ApiHandler {
maxDelay: 15000,
})
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const { id: modelId, info } = this.getModel()
const contents = messages.map(convertAnthropicMessageToGemini)
@@ -117,7 +133,7 @@ export class GeminiHandler implements ApiHandler {
let lastUsageMetadata: GenerateContentResponseUsageMetadata | undefined
try {
const result = await this.client.models.generateContentStream({
const result = await client.models.generateContentStream({
model: modelId,
contents: contents,
config: {
@@ -351,6 +367,7 @@ export class GeminiHandler implements ApiHandler {
*/
async countTokens(content: Array<any>): Promise<number> {
try {
const client = this.ensureClient()
const { id: model } = this.getModel()
// Convert content to Gemini format
@@ -362,7 +379,7 @@ export class GeminiHandler implements ApiHandler {
})
// Use Gemini's token counting API
const response = await this.client.models.countTokens({
const response = await client.models.countTokens({
model,
contents: [{ parts: geminiContent }],
})
+24 -15
View File
@@ -8,21 +8,35 @@ import { withRetry } from "../retry"
export class LiteLlmHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
private client: OpenAI | undefined
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: this.options.liteLlmBaseUrl || "http://localhost:4000",
apiKey: this.options.liteLlmApiKey || "noop",
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.liteLlmApiKey) {
throw new Error("LiteLLM API key is required")
}
try {
this.client = new OpenAI({
baseURL: this.options.liteLlmBaseUrl || "http://localhost:4000",
apiKey: this.options.liteLlmApiKey || "noop",
})
} catch (error) {
throw new Error(`Error creating LiteLLM client: ${error.message}`)
}
}
return this.client
}
async calculateCost(prompt_tokens: number, completion_tokens: number): Promise<number | undefined> {
// Reference: https://github.com/BerriAI/litellm/blob/122ee634f434014267af104814022af1d9a0882f/litellm/proxy/spend_tracking/spend_management_endpoints.py#L1473
const client = this.ensureClient()
const modelId = this.options.liteLlmModelId || liteLlmDefaultModelId
try {
const response = await fetch(`${this.client.baseURL}/spend/calculate`, {
const response = await fetch(`${client.baseURL}/spend/calculate`, {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -54,6 +68,7 @@ export class LiteLlmHandler implements ApiHandler {
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const formattedMessages = convertToOpenAiMessages(messages)
const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
role: "system",
@@ -101,21 +116,15 @@ export class LiteLlmHandler implements ApiHandler {
return message
})
const requestPayload: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming & {
metadata?: { cline_task_id: string }
} = {
const stream = await client.chat.completions.create({
model: this.options.liteLlmModelId || liteLlmDefaultModelId,
messages: [enhancedSystemMessage, ...enhancedMessages],
temperature,
stream: true,
stream_options: { include_usage: true },
...(thinkingConfig && { thinking: thinkingConfig }), // Add thinking configuration when applicable
...(this.options.taskId && {
metadata: { cline_task_id: this.options.taskId },
}),
}
const stream = await this.client.chat.completions.create(requestPayload)
...(this.options.taskId && { litellm_session_id: `cline-${this.options.taskId}` }), // Add session ID for LiteLLM tracking
})
const inputCost = (await this.calculateCost(1e6, 0)) || 0
const outputCost = (await this.calculateCost(0, 1e6)) || 0
+17 -6
View File
@@ -8,25 +8,36 @@ import { withRetry } from "../retry"
export class LmStudioHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
private client: OpenAI | undefined
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: (this.options.lmStudioBaseUrl || "http://localhost:1234") + "/v1",
apiKey: "noop",
})
}
private ensureClient(): OpenAI {
if (!this.client) {
try {
this.client = new OpenAI({
baseURL: (this.options.lmStudioBaseUrl || "http://localhost:1234") + "/v1",
apiKey: "noop",
})
} catch (error) {
throw new Error(`Error creating LM Studio client: ${error.message}`)
}
}
return this.client
}
@withRetry({ retryAllErrors: true })
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
try {
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: this.getModel().id,
messages: openAiMessages,
stream: true,
+19 -5
View File
@@ -8,18 +8,32 @@ import { ApiStream } from "../transform/stream"
export class MistralHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: Mistral
private client: Mistral | undefined
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new Mistral({
apiKey: this.options.mistralApiKey,
})
}
private ensureClient(): Mistral {
if (!this.client) {
if (!this.options.mistralApiKey) {
throw new Error("Mistral API key is required")
}
try {
this.client = new Mistral({
apiKey: this.options.mistralApiKey,
})
} catch (error) {
throw new Error(`Error creating Mistral client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const stream = await this.client.chat
const client = this.ensureClient()
const stream = await client.chat
.stream({
model: this.getModel().id,
// max_completion_tokens: this.getModel().info.maxTokens,
+20 -7
View File
@@ -8,24 +8,37 @@ import { convertToR1Format } from "../transform/r1-format"
import { nebiusDefaultModelId, nebiusModels, type ModelInfo, type ApiHandlerOptions, type NebiusModelId } from "../../shared/api"
export class NebiusHandler implements ApiHandler {
private client: OpenAI
private client: OpenAI | undefined
constructor(private readonly options: ApiHandlerOptions) {
this.client = new OpenAI({
baseURL: "https://api.studio.nebius.ai/v1",
apiKey: this.options.nebiusApiKey,
})
constructor(private readonly options: ApiHandlerOptions) {}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.nebiusApiKey) {
throw new Error("Nebius API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://api.studio.nebius.ai/v1",
apiKey: this.options.nebiusApiKey,
})
} catch (error) {
throw new Error(`Error creating Nebius client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = model.id.includes("DeepSeek-R1")
? convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)]
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: model.id,
messages: openAiMessages,
temperature: 0,
+14 -3
View File
@@ -8,15 +8,26 @@ import { withRetry } from "../retry"
export class OllamaHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: Ollama
private client: Ollama | undefined
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new Ollama({ host: this.options.ollamaBaseUrl || "http://localhost:11434" })
}
private ensureClient(): Ollama {
if (!this.client) {
try {
this.client = new Ollama({ host: this.options.ollamaBaseUrl || "http://localhost:11434" })
} catch (error) {
throw new Error(`Error creating Ollama client: ${error.message}`)
}
}
return this.client
}
@withRetry({ retryAllErrors: true })
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const ollamaMessages: Message[] = [{ role: "system", content: systemPrompt }, ...convertToOllamaMessages(messages)]
try {
@@ -27,7 +38,7 @@ export class OllamaHandler implements ApiHandler {
})
// Create the actual API request promise
const apiPromise = this.client.chat({
const apiPromise = client.chat({
model: this.getModel().id,
messages: ollamaMessages,
stream: true,
+21 -7
View File
@@ -10,13 +10,26 @@ import type { ChatCompletionReasoningEffort } from "openai/resources/chat/comple
export class OpenAiNativeHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
private client: OpenAI | undefined
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
apiKey: this.options.openAiNativeApiKey,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.openAiNativeApiKey) {
throw new Error("OpenAI API key is required")
}
try {
this.client = new OpenAI({
apiKey: this.options.openAiNativeApiKey,
})
} catch (error: any) {
throw new Error(`Error creating OpenAI client: ${error.message}`)
}
}
return this.client
}
private async *yieldUsage(info: ModelInfo, usage: OpenAI.Completions.CompletionUsage | undefined): ApiStream {
@@ -38,6 +51,7 @@ export class OpenAiNativeHandler implements ApiHandler {
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
switch (model.id) {
@@ -45,7 +59,7 @@ export class OpenAiNativeHandler implements ApiHandler {
case "o1-preview":
case "o1-mini": {
// o1 doesn't support streaming, non-1 temp, or system prompt
const response = await this.client.chat.completions.create({
const response = await client.chat.completions.create({
model: model.id,
messages: [{ role: "user", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
})
@@ -61,7 +75,7 @@ export class OpenAiNativeHandler implements ApiHandler {
case "o4-mini":
case "o3":
case "o3-mini": {
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: model.id,
messages: [{ role: "developer", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
stream: true,
@@ -85,7 +99,7 @@ export class OpenAiNativeHandler implements ApiHandler {
break
}
default: {
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: model.id,
// max_completion_tokens: this.getModel().info.maxTokens,
temperature: 0,
+36 -22
View File
@@ -10,35 +10,49 @@ import type { ChatCompletionReasoningEffort } from "openai/resources/chat/comple
export class OpenAiHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
private client: OpenAI | undefined
constructor(options: ApiHandlerOptions) {
this.options = options
// Azure API shape slightly differs from the core API shape: https://github.com/openai/openai-node?tab=readme-ov-file#microsoft-azure-openai
// Use azureApiVersion to determine if this is an Azure endpoint, since the URL may not always contain 'azure.com'
if (
this.options.azureApiVersion ||
((this.options.openAiBaseUrl?.toLowerCase().includes("azure.com") ||
this.options.openAiBaseUrl?.toLowerCase().includes("azure.us")) &&
!this.options.openAiModelId?.toLowerCase().includes("deepseek"))
) {
this.client = new AzureOpenAI({
baseURL: this.options.openAiBaseUrl,
apiKey: this.options.openAiApiKey,
apiVersion: this.options.azureApiVersion || azureOpenAiDefaultApiVersion,
defaultHeaders: this.options.openAiHeaders,
})
} else {
this.client = new OpenAI({
baseURL: this.options.openAiBaseUrl,
apiKey: this.options.openAiApiKey,
defaultHeaders: this.options.openAiHeaders,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.openAiApiKey) {
throw new Error("OpenAI API key is required")
}
try {
// Azure API shape slightly differs from the core API shape: https://github.com/openai/openai-node?tab=readme-ov-file#microsoft-azure-openai
// Use azureApiVersion to determine if this is an Azure endpoint, since the URL may not always contain 'azure.com'
if (
this.options.azureApiVersion ||
((this.options.openAiBaseUrl?.toLowerCase().includes("azure.com") ||
this.options.openAiBaseUrl?.toLowerCase().includes("azure.us")) &&
!this.options.openAiModelId?.toLowerCase().includes("deepseek"))
) {
this.client = new AzureOpenAI({
baseURL: this.options.openAiBaseUrl,
apiKey: this.options.openAiApiKey,
apiVersion: this.options.azureApiVersion || azureOpenAiDefaultApiVersion,
defaultHeaders: this.options.openAiHeaders,
})
} else {
this.client = new OpenAI({
baseURL: this.options.openAiBaseUrl,
apiKey: this.options.openAiApiKey,
defaultHeaders: this.options.openAiHeaders,
})
}
} catch (error: any) {
throw new Error(`Error creating OpenAI client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const modelId = this.options.openAiModelId ?? ""
const isDeepseekReasoner = modelId.includes("deepseek-reasoner")
const isR1FormatRequired = this.options.openAiModelInfo?.isR1FormatRequired ?? false
@@ -68,7 +82,7 @@ export class OpenAiHandler implements ApiHandler {
reasoningEffort = (this.options.reasoningEffort as ChatCompletionReasoningEffort) || "medium"
}
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: modelId,
messages: openAiMessages,
temperature,
+90 -26
View File
@@ -11,27 +11,41 @@ import { OpenRouterErrorResponse } from "./types"
export class OpenRouterHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
private client: OpenAI | undefined
lastGenerationId?: string
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://openrouter.ai/api/v1",
apiKey: this.options.openRouterApiKey,
defaultHeaders: {
"HTTP-Referer": "https://cline.bot", // Optional, for including your app on openrouter.ai rankings.
"X-Title": "Cline", // Optional. Shows in rankings on openrouter.ai.
},
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.openRouterApiKey) {
throw new Error("OpenRouter API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://openrouter.ai/api/v1",
apiKey: this.options.openRouterApiKey,
defaultHeaders: {
"HTTP-Referer": "https://cline.bot", // Optional, for including your app on openrouter.ai rankings.
"X-Title": "Cline", // Optional. Shows in rankings on openrouter.ai.
},
})
} catch (error: any) {
throw new Error(`Error creating OpenRouter client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
this.lastGenerationId = undefined
const stream = await createOpenRouterStream(
this.client,
client,
systemPrompt,
messages,
this.getModel(),
@@ -44,6 +58,7 @@ export class OpenRouterHandler implements ApiHandler {
for await (const chunk of stream) {
// openrouter returns an error object instead of the openai sdk throwing an error
// Check for error field directly on chunk
if ("error" in chunk) {
const error = chunk.error as OpenRouterErrorResponse["error"]
console.error(`OpenRouter API Error: ${error?.code} - ${error?.message}`)
@@ -52,6 +67,29 @@ export class OpenRouterHandler implements ApiHandler {
throw new Error(`OpenRouter API Error ${error.code}: ${error.message}${metadataStr}`)
}
// Check for error in choices[0].finish_reason
// OpenRouter may return errors in a non-standard way within choices
const choice = chunk.choices?.[0]
// Use type assertion since OpenRouter uses non-standard "error" finish_reason
if ((choice?.finish_reason as string) === "error") {
// Use type assertion since OpenRouter adds non-standard error property
const choiceWithError = choice as any
if (choiceWithError.error) {
const error = choiceWithError.error
console.error(
`OpenRouter Mid-Stream Error: ${error?.code || "Unknown"} - ${error?.message || "Unknown error"}`,
)
// Format error details
const errorDetails = typeof error === "object" ? JSON.stringify(error, null, 2) : String(error)
throw new Error(`OpenRouter Mid-Stream Error: ${errorDetails}`)
} else {
// Fallback if error details are not available
throw new Error(
`OpenRouter Mid-Stream Error: Stream terminated with error status but no error details provided`,
)
}
}
if (!this.lastGenerationId && chunk.id) {
this.lastGenerationId = chunk.id
}
@@ -74,14 +112,27 @@ export class OpenRouterHandler implements ApiHandler {
}
if (!didOutputUsage && chunk.usage) {
yield {
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
// @ts-ignore-next-line
totalCost: (chunk.usage.cost || 0) + (chunk.usage.cost_details?.upstream_inference_cost || 0),
let modelId = this.options.openRouterModelId
if (modelId && modelId.includes("gemini")) {
yield {
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
inputTokens: (chunk.usage.prompt_tokens || 0) - (chunk.usage.prompt_tokens_details?.cached_tokens || 0),
outputTokens: chunk.usage.completion_tokens || 0,
// @ts-ignore-next-line
totalCost: (chunk.usage.cost || 0) + (chunk.usage.cost_details?.upstream_inference_cost || 0),
}
} else {
yield {
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
// @ts-ignore-next-line
totalCost: (chunk.usage.cost || 0) + (chunk.usage.cost_details?.upstream_inference_cost || 0),
}
}
didOutputUsage = true
}
@@ -103,14 +154,27 @@ export class OpenRouterHandler implements ApiHandler {
const generationIterator = this.fetchGenerationDetails(this.lastGenerationId)
const generation = (await generationIterator.next()).value
// console.log("OpenRouter generation details:", generation)
return {
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: generation?.native_tokens_cached || 0,
// openrouter generation endpoint fails often
inputTokens: generation?.native_tokens_prompt || 0,
outputTokens: generation?.native_tokens_completion || 0,
totalCost: generation?.total_cost || 0,
let modelId = this.options.openRouterModelId
if (modelId && modelId.includes("gemini")) {
return {
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: generation?.native_tokens_cached || 0,
// openrouter generation endpoint fails often
inputTokens: (generation?.native_tokens_prompt || 0) - (generation?.native_tokens_cached || 0),
outputTokens: generation?.native_tokens_completion || 0,
totalCost: generation?.total_cost || 0,
}
} else {
return {
type: "usage",
cacheWriteTokens: 0,
cacheReadTokens: generation?.native_tokens_cached || 0,
// openrouter generation endpoint fails often
inputTokens: generation?.native_tokens_prompt || 0,
outputTokens: generation?.native_tokens_completion || 0,
totalCost: generation?.total_cost || 0,
}
}
} catch (error) {
// ignore if fails
+23 -9
View File
@@ -18,17 +18,30 @@ import { withRetry } from "../retry"
export class QwenHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
private client: OpenAI | undefined
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL:
this.options.qwenApiLine === "china"
? "https://dashscope.aliyuncs.com/compatible-mode/v1"
: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
apiKey: this.options.qwenApiKey,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.qwenApiKey) {
throw new Error("Alibaba API key is required")
}
try {
this.client = new OpenAI({
baseURL:
this.options.qwenApiLine === "china"
? "https://dashscope.aliyuncs.com/compatible-mode/v1"
: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
apiKey: this.options.qwenApiKey,
})
} catch (error: any) {
throw new Error(`Error creating Alibaba client: ${error.message}`)
}
}
return this.client
}
getModel(): { id: MainlandQwenModelId | InternationalQwenModelId; info: ModelInfo } {
@@ -51,6 +64,7 @@ export class QwenHandler implements ApiHandler {
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
const isDeepseekReasoner = model.id.includes("deepseek-r1")
const isReasoningModelFamily = model.id.includes("qwen3") || ["qwen-plus-latest", "qwen-turbo-latest"].includes(model.id)
@@ -76,7 +90,7 @@ export class QwenHandler implements ApiHandler {
temperature = undefined
}
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: model.id,
max_completion_tokens: model.info.maxTokens,
messages: openAiMessages,
+24 -10
View File
@@ -19,22 +19,36 @@ interface RequestyUsage extends OpenAI.CompletionUsage {
export class RequestyHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
private client: OpenAI | undefined
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://router.requesty.ai/v1",
apiKey: this.options.requestyApiKey,
defaultHeaders: {
"HTTP-Referer": "https://cline.bot",
"X-Title": "Cline",
},
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.requestyApiKey) {
throw new Error("Requesty API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://router.requesty.ai/v1",
apiKey: this.options.requestyApiKey,
defaultHeaders: {
"HTTP-Referer": "https://cline.bot",
"X-Title": "Cline",
},
})
} catch (error: any) {
throw new Error(`Error creating Requesty client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
@@ -57,7 +71,7 @@ export class RequestyHandler implements ApiHandler {
: {}
// @ts-ignore-next-line
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: model.id,
max_tokens: model.info.maxTokens || undefined,
messages: openAiMessages,
+20 -6
View File
@@ -9,18 +9,32 @@ import { convertToR1Format } from "@api/transform/r1-format"
export class SambanovaHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
private client: OpenAI | undefined
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://api.sambanova.ai/v1",
apiKey: this.options.sambanovaApiKey,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.sambanovaApiKey) {
throw new Error("SambaNova API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://api.sambanova.ai/v1",
apiKey: this.options.sambanovaApiKey,
})
} catch (error: any) {
throw new Error(`Error creating SambaNova client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const model = this.getModel()
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
@@ -34,7 +48,7 @@ export class SambanovaHandler implements ApiHandler {
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
}
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: this.getModel().id,
messages: openAiMessages,
temperature: 0,
+145 -2
View File
@@ -122,6 +122,8 @@ export class SapAiCoreHandler implements ApiHandler {
const deploymentId = await this.getDeploymentForModel(model.id)
const anthropicModels = [
"anthropic--claude-4-sonnet",
"anthropic--claude-4-opus",
"anthropic--claude-3.7-sonnet",
"anthropic--claude-3.5-sonnet",
"anthropic--claude-3-sonnet",
@@ -131,12 +133,18 @@ export class SapAiCoreHandler implements ApiHandler {
const openAIModels = ["gpt-4o", "gpt-4", "gpt-4o-mini", "o1", "gpt-4.1", "gpt-4.1-nano", "o3-mini", "o3", "o4-mini"]
const geminiModels = ["gemini-2.5-flash", "gemini-2.5-pro"]
let url: string
let payload: any
if (anthropicModels.includes(model.id)) {
url = `${this.options.sapAiCoreBaseUrl}/v2/inference/deployments/${deploymentId}/invoke-with-response-stream`
if (model.id === "anthropic--claude-3.7-sonnet") {
if (
model.id === "anthropic--claude-4-sonnet" ||
model.id === "anthropic--claude-4-opus" ||
model.id === "anthropic--claude-3.7-sonnet"
) {
url = `${this.options.sapAiCoreBaseUrl}/v2/inference/deployments/${deploymentId}/converse-stream`
payload = {
inferenceConfig: {
@@ -181,6 +189,9 @@ export class SapAiCoreHandler implements ApiHandler {
delete payload.stream
delete payload.stream_options
}
} else if (geminiModels.includes(model.id)) {
url = `${this.options.sapAiCoreBaseUrl}/v2/inference/deployments/${deploymentId}/models/${model.id}:streamGenerateContent`
payload = this.convertToGeminiFormat(systemPrompt, messages)
} else {
throw new Error(`Unsupported model: ${model.id}`)
}
@@ -221,8 +232,14 @@ export class SapAiCoreHandler implements ApiHandler {
}
} else if (openAIModels.includes(model.id)) {
yield* this.streamCompletionGPT(response.data, model)
} else if (model.id === "anthropic--claude-3.7-sonnet") {
} else if (
model.id === "anthropic--claude-4-sonnet" ||
model.id === "anthropic--claude-4-opus" ||
model.id === "anthropic--claude-3.7-sonnet"
) {
yield* this.streamCompletionSonnet37(response.data, model)
} else if (geminiModels.includes(model.id)) {
yield* this.streamCompletionGemini(response.data, model)
} else {
yield* this.streamCompletion(response.data, model)
}
@@ -451,6 +468,88 @@ export class SapAiCoreHandler implements ApiHandler {
}
}
private async *streamCompletionGemini(
stream: any,
model: { id: SapAiCoreModelId; info: ModelInfo },
): AsyncGenerator<any, void, unknown> {
let promptTokens = 0
let outputTokens = 0
let cacheReadTokens = 0
let thoughtsTokenCount = 0
try {
for await (const chunk of stream) {
const lines = chunk.toString().split("\n").filter(Boolean)
for (const line of lines) {
if (line.startsWith("data: ")) {
const jsonData = line.slice(6)
try {
const data = JSON.parse(jsonData)
const candidateForThoughts = data?.candidates?.[0]
const partsForThoughts = candidateForThoughts?.content?.parts
let thoughts = ""
if (partsForThoughts) {
for (const part of partsForThoughts) {
const { thought, text } = part
if (thought && text) {
thoughts += text + "\n"
}
}
}
if (thoughts.trim() !== "") {
yield {
type: "reasoning",
reasoning: thoughts.trim(),
}
}
if (data.text) {
yield {
type: "text",
text: data.text,
}
}
if (data.candidates && data.candidates[0]?.content?.parts) {
for (const part of data.candidates[0].content.parts) {
if (part.text && !part.thought) {
// Only non-thought text
yield {
type: "text",
text: part.text,
}
}
}
}
if (data.usageMetadata) {
promptTokens = data.usageMetadata.promptTokenCount ?? promptTokens
outputTokens = data.usageMetadata.candidatesTokenCount ?? outputTokens
thoughtsTokenCount = data.usageMetadata.thoughtsTokenCount ?? thoughtsTokenCount
cacheReadTokens = data.usageMetadata.cachedContentTokenCount ?? cacheReadTokens
yield {
type: "usage",
inputTokens: promptTokens - cacheReadTokens,
outputTokens,
thoughtsTokenCount,
cacheReadTokens,
}
}
} catch (error) {
console.error("Failed to parse Gemini JSON data:", error)
}
}
}
}
} catch (error) {
console.error("Error streaming Gemini completion:", error)
throw error
}
}
createUserReadableRequest(
userContent: Array<
Anthropic.TextBlockParam | Anthropic.ImageBlockParam | Anthropic.ToolUseBlockParam | Anthropic.ToolResultBlockParam
@@ -485,6 +584,50 @@ export class SapAiCoreHandler implements ApiHandler {
throw new Error(`Unsupported image format: ${format}`)
}
private convertToGeminiFormat(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]) {
const contents = messages.map(this.convertAnthropicMessageToGemini)
const payload = {
contents,
systemInstruction: {
parts: [
{
text: systemPrompt,
},
],
},
generationConfig: {
maxOutputTokens: this.getModel().info.maxTokens,
temperature: 0.0,
},
}
return payload
}
private convertAnthropicMessageToGemini(message: Anthropic.Messages.MessageParam) {
const role = message.role === "assistant" ? "model" : "user"
const parts = []
if (typeof message.content === "string") {
parts.push({ text: message.content })
} else if (Array.isArray(message.content)) {
for (const block of message.content) {
if (block.type === "text") {
parts.push({ text: block.text })
} else if (block.type === "image") {
parts.push({
inlineData: {
mimeType: block.source.media_type,
data: block.source.data,
},
})
}
}
}
return { role, parts }
}
private formatAnthropicMessages(messages: Anthropic.Messages.MessageParam[]): any[] {
return messages.map((m) => {
const contentBlocks: any[] = []
+20 -6
View File
@@ -9,18 +9,32 @@ import { convertToR1Format } from "@api/transform/r1-format"
export class TogetherHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
private client: OpenAI | undefined
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://api.together.xyz/v1",
apiKey: this.options.togetherApiKey,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.togetherApiKey) {
throw new Error("Together API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://api.together.xyz/v1",
apiKey: this.options.togetherApiKey,
})
} catch (error: any) {
throw new Error(`Error creating Together client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const modelId = this.options.togetherModelId ?? ""
const isDeepseekReasoner = modelId.includes("deepseek-reasoner")
@@ -33,7 +47,7 @@ export class TogetherHandler implements ApiHandler {
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
}
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: modelId,
messages: openAiMessages,
temperature: 0,
+43 -16
View File
@@ -7,25 +7,49 @@ import { ApiStream } from "@api/transform/stream"
import { GeminiHandler } from "./gemini"
export class VertexHandler implements ApiHandler {
private geminiHandler: GeminiHandler
private clientAnthropic: AnthropicVertex
private geminiHandler: GeminiHandler | undefined
private clientAnthropic: AnthropicVertex | undefined
private options: ApiHandlerOptions
constructor(options: ApiHandlerOptions) {
this.options = options
}
// Create a GeminiHandler with isVertex flag for Gemini models
this.geminiHandler = new GeminiHandler({
...options,
isVertex: true,
})
private ensureGeminiHandler(): GeminiHandler {
if (!this.geminiHandler) {
try {
// Create a GeminiHandler with isVertex flag for Gemini models
this.geminiHandler = new GeminiHandler({
...this.options,
isVertex: true,
})
} catch (error: any) {
throw new Error(`Error creating Vertex AI Gemini handler: ${error.message}`)
}
}
return this.geminiHandler
}
// Initialize Anthropic client for Claude models
this.clientAnthropic = new AnthropicVertex({
projectId: this.options.vertexProjectId,
// https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude#regions
region: this.options.vertexRegion,
})
private ensureAnthropicClient(): AnthropicVertex {
if (!this.clientAnthropic) {
if (!this.options.vertexProjectId) {
throw new Error("Vertex AI project ID is required")
}
if (!this.options.vertexRegion) {
throw new Error("Vertex AI region is required")
}
try {
// Initialize Anthropic client for Claude models
this.clientAnthropic = new AnthropicVertex({
projectId: this.options.vertexProjectId,
// https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude#regions
region: this.options.vertexRegion,
})
} catch (error: any) {
throw new Error(`Error creating Vertex AI Anthropic client: ${error.message}`)
}
}
return this.clientAnthropic
}
@withRetry()
@@ -35,10 +59,13 @@ export class VertexHandler implements ApiHandler {
// For Gemini models, use the GeminiHandler
if (!modelId.includes("claude")) {
yield* this.geminiHandler.createMessage(systemPrompt, messages)
const geminiHandler = this.ensureGeminiHandler()
yield* geminiHandler.createMessage(systemPrompt, messages)
return
}
const clientAnthropic = this.ensureAnthropicClient()
// Claude implementation
let budget_tokens = this.options.thinkingBudgetTokens || 0
const reasoningOn =
@@ -63,7 +90,7 @@ export class VertexHandler implements ApiHandler {
)
const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
stream = await this.clientAnthropic.beta.messages.create(
stream = await clientAnthropic.beta.messages.create(
{
model: modelId,
max_tokens: model.info.maxTokens || 8192,
@@ -125,7 +152,7 @@ export class VertexHandler implements ApiHandler {
break
}
default: {
stream = await this.clientAnthropic.beta.messages.create({
stream = await clientAnthropic.beta.messages.create({
model: modelId,
max_tokens: model.info.maxTokens || 8192,
temperature: 0,
+20 -6
View File
@@ -9,18 +9,32 @@ import { withRetry } from "../retry"
export class XAIHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
private client: OpenAI | undefined
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://api.x.ai/v1",
apiKey: this.options.xaiApiKey,
})
}
private ensureClient(): OpenAI {
if (!this.client) {
if (!this.options.xaiApiKey) {
throw new Error("xAI API key is required")
}
try {
this.client = new OpenAI({
baseURL: "https://api.x.ai/v1",
apiKey: this.options.xaiApiKey,
})
} catch (error: any) {
throw new Error(`Error creating xAI client: ${error.message}`)
}
}
return this.client
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = this.ensureClient()
const modelId = this.getModel().id
// ensure reasoning effort is either "low" or "high" for grok-3-mini
let reasoningEffort: ChatCompletionReasoningEffort | undefined
@@ -30,7 +44,7 @@ export class XAIHandler implements ApiHandler {
reasoningEffort = undefined
}
}
const stream = await this.client.chat.completions.create({
const stream = await client.chat.completions.create({
model: modelId,
max_completion_tokens: this.getModel().info.maxTokens,
temperature: 0,
+39
View File
@@ -156,6 +156,45 @@ replaced
expected: "line2\nreplaced\nline4",
isFinal: true,
},
{
name: "malformed diff - missing separator",
original: "line1\nline2\nline3",
diff: `------- SEARCH
line2
+++++++ REPLACE
replaced`,
shouldThrow: true,
},
{
name: "malformed diff - trailing space on separator",
original: "line1\nline2\nline3",
diff: `------- SEARCH
line2
=======
replaced
+++++++ REPLACE`,
shouldThrow: true,
},
{
name: "malformed diff - double replace markers",
original: "line1\nline2\nline3",
diff: `------- SEARCH
line2
+++++++ REPLACE
first replacement
+++++++ REPLACE`,
shouldThrow: true,
},
{
name: "malformed diff - malformed separator with dashes",
original: "line1\nline2\nline3",
diff: `------- SEARCH
line2
------- =======
replaced
+++++++ REPLACE`,
shouldThrow: true,
},
]
//.filter(({name}) => name === "multiple ordered replacements")
//.filter(({name}) => name === "delete then replace")
+4
View File
@@ -380,6 +380,10 @@ async function constructNewFileContentV1(diffContent: string, originalContent: s
if (isReplaceBlockEnd(line)) {
// Finished one replace block
if (searchMatchIndex === -1) {
throw new Error(`The SEARCH block:\n${currentSearchContent.trimEnd()}\n...is malformatted.`)
}
// Store this replacement
replacements.push({
start: searchMatchIndex,
@@ -6,6 +6,9 @@ import * as path from "path"
import { FileContextTracker } from "./FileContextTracker"
import * as diskModule from "@core/storage/disk"
import type { TaskMetadata, FileMetadataEntry } from "./ContextTrackerTypes"
import type { WebviewProviderCreator } from "@/hosts/host-providers"
import * as hostProviders from "@hosts/host-providers"
import { vscodeHostBridgeClient } from "@/hosts/vscode/client/host-grpc-client"
describe("FileContextTracker", () => {
let sandbox: sinon.SinonSandbox
@@ -37,7 +40,6 @@ describe("FileContextTracker", () => {
}
// Use a function replacement instead of a direct stub
const originalCreateFileSystemWatcher = vscode.workspace.createFileSystemWatcher
vscode.workspace.createFileSystemWatcher = function () {
return mockFileSystemWatcher
}
@@ -51,6 +53,7 @@ describe("FileContextTracker", () => {
mockTaskMetadata = { files_in_context: [], model_usage: [] }
getTaskMetadataStub = sandbox.stub(diskModule, "getTaskMetadata").resolves(mockTaskMetadata)
saveTaskMetadataStub = sandbox.stub(diskModule, "saveTaskMetadata").resolves()
hostProviders.initializeHostProviders(((_) => {}) as WebviewProviderCreator, vscodeHostBridgeClient)
// Create tracker instance
taskId = "test-task-id"
@@ -5,6 +5,8 @@ import { getWorkspaceState, updateWorkspaceState } from "@core/storage/state"
import { getGlobalState } from "@core/storage/state"
import type { FileMetadataEntry } from "./ContextTrackerTypes"
import type { ClineMessage } from "@shared/ExtensionMessage"
import { getHostBridgeProvider } from "@/hosts/host-providers"
import { getCwd } from "@/utils/path"
// This class is responsible for tracking file operations that may result in stale context.
// If a user modifies a file outside of Cline, the context may become stale and need to be updated.
@@ -34,17 +36,6 @@ export class FileContextTracker {
this.taskId = taskId
}
/**
* Gets the current working directory or returns undefined if it cannot be determined
*/
private getCwd(): string | undefined {
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
if (!cwd) {
console.info("No workspace folder available - cannot determine current working directory")
}
return cwd
}
/**
* File watchers are set up for each file that is tracked in the task metadata.
*/
@@ -54,8 +45,9 @@ export class FileContextTracker {
return
}
const cwd = this.getCwd()
const cwd = await getCwd()
if (!cwd) {
console.info("No workspace folder available - cannot determine current working directory")
return
}
@@ -85,8 +77,9 @@ export class FileContextTracker {
*/
async trackFileContext(filePath: string, operation: "read_tool" | "user_edited" | "cline_edited" | "file_mentioned") {
try {
const cwd = this.getCwd()
const cwd = await getCwd()
if (!cwd) {
console.info("No workspace folder available - cannot determine current working directory")
return
}
@@ -242,7 +235,9 @@ export class FileContextTracker {
async storePendingFileContextWarning(files: string[]): Promise<void> {
try {
const key = `pendingFileContextWarning_${this.taskId}`
await updateWorkspaceState(this.context, key, files)
// NOTE: Using 'as any' because dynamic keys like pendingFileContextWarning_${taskId}
// are legitimate workspace state keys but don't fit the strict LocalStateKey type system
await updateWorkspaceState(this.context, key as any, files)
} catch (error) {
console.error("Error storing pending file context warning:", error)
}
@@ -254,7 +249,7 @@ export class FileContextTracker {
async retrievePendingFileContextWarning(): Promise<string[] | undefined> {
try {
const key = `pendingFileContextWarning_${this.taskId}`
const files = (await getWorkspaceState(this.context, key)) as string[]
const files = (await getWorkspaceState(this.context, key as any)) as string[]
return files
} catch (error) {
console.error("Error retrieving pending file context warning:", error)
@@ -269,7 +264,7 @@ export class FileContextTracker {
try {
const files = await this.retrievePendingFileContextWarning()
if (files) {
await updateWorkspaceState(this.context, `pendingFileContextWarning_${this.taskId}`, undefined)
await updateWorkspaceState(this.context, `pendingFileContextWarning_${this.taskId}` as any, undefined)
return files
}
} catch (error) {
@@ -300,7 +295,7 @@ export class FileContextTracker {
if (orphanedPendingContextTasks.length > 0) {
for (const key of orphanedPendingContextTasks) {
await updateWorkspaceState(context, key, undefined)
await updateWorkspaceState(context, key as any, undefined)
}
}
+2 -1
View File
@@ -1,6 +1,7 @@
import * as vscode from "vscode"
import { Controller } from ".."
import { Empty, StringRequest } from "../../../shared/proto/common"
import { writeTextToClipboard } from "@/utils/env"
/**
* Copies text to the system clipboard
@@ -11,7 +12,7 @@ import { Empty, StringRequest } from "../../../shared/proto/common"
export async function copyToClipboard(controller: Controller, request: StringRequest): Promise<Empty> {
try {
if (request.value) {
await vscode.env.clipboard.writeText(request.value)
await writeTextToClipboard(request.value)
}
} catch (error) {
console.error("Error copying to clipboard:", error)
+2 -1
View File
@@ -6,8 +6,8 @@ import { createRuleFile as createRuleFileImpl } from "@core/context/instructions
import * as vscode from "vscode"
import * as path from "path"
import { handleFileServiceRequest } from "./index"
import { cwd } from "@core/task"
import { refreshWorkflowToggles } from "@/core/context/instructions/user-instructions/workflows"
import { getCwd, getDesktopDir } from "@/utils/path"
/**
* Creates a rule file in either global or workspace rules directory
@@ -32,6 +32,7 @@ export const createRuleFile: FileMethodHandler = async (controller: Controller,
throw new Error("Missing or invalid parameters")
}
const cwd = await getCwd(getDesktopDir())
const { filePath, fileExists } = await createRuleFileImpl(request.isGlobal, request.filename, cwd, request.type)
if (!filePath) {
+4 -8
View File
@@ -1,13 +1,9 @@
import { Controller } from ".."
import { RuleFileRequest, RuleFile } from "@shared/proto/file"
import { FileMethodHandler } from "./index"
import { refreshClineRulesToggles } from "@core/context/instructions/user-instructions/cline-rules"
import { deleteRuleFile as deleteRuleFileImpl } from "@core/context/instructions/user-instructions/rule-helpers"
import { refreshExternalRulesToggles } from "@core/context/instructions/user-instructions/external-rules"
import { refreshWorkflowToggles } from "@core/context/instructions/user-instructions/workflows"
import * as vscode from "vscode"
import { RuleFile, RuleFileRequest } from "@shared/proto/file"
import * as path from "path"
import { cwd } from "@core/task"
import * as vscode from "vscode"
import { Controller } from ".."
import { FileMethodHandler } from "./index"
/**
* Deletes a rule file from either global or workspace rules directory
+1 -2
View File
@@ -3,7 +3,7 @@ import { RelativePathsRequest, RelativePaths } from "@shared/proto/file"
import { FileMethodHandler } from "./index"
import * as vscode from "vscode"
import * as path from "path"
import { Metadata, StringRequest } from "@shared/proto/common"
import { StringRequest } from "@shared/proto/common"
import { getHostBridgeProvider } from "@hosts/host-providers"
/**
@@ -22,7 +22,6 @@ export const getRelativePaths: FileMethodHandler = async (
// Use the host URI service client instead of directly using vscode.Uri.parse
const parseResponse = await getHostBridgeProvider().uriServiceClient.parse(
StringRequest.create({
metadata: Metadata.create({}),
value: uriString,
}),
)
+1 -1
View File
@@ -8,7 +8,7 @@ import { openMention as coreOpenMention } from "../../mentions"
* @param request The string request containing the mention text
* @returns Empty response
*/
export async function openMention(controller: Controller, request: StringRequest): Promise<Empty> {
export async function openMention(_controller: Controller, request: StringRequest): Promise<Empty> {
coreOpenMention(request.value)
return Empty.create()
}
+2 -1
View File
@@ -4,7 +4,7 @@ import type { Controller } from "../index"
import { refreshClineRulesToggles } from "@core/context/instructions/user-instructions/cline-rules"
import { refreshExternalRulesToggles } from "@core/context/instructions/user-instructions/external-rules"
import { refreshWorkflowToggles } from "@core/context/instructions/user-instructions/workflows"
import { cwd } from "@core/task"
import { getCwd, getDesktopDir } from "@/utils/path"
/**
* Refreshes all rule toggles (Cline, External, and Workflows)
@@ -14,6 +14,7 @@ import { cwd } from "@core/task"
*/
export async function refreshRules(controller: Controller, _request: EmptyRequest): Promise<RefreshedRules> {
try {
const cwd = await getCwd(getDesktopDir())
const { globalToggles, localToggles } = await refreshClineRulesToggles(controller.context, cwd)
const { cursorLocalToggles, windsurfLocalToggles } = await refreshExternalRulesToggles(controller.context, cwd)
const { localWorkflowToggles, globalWorkflowToggles } = await refreshWorkflowToggles(controller.context, cwd)
+1 -1
View File
@@ -13,7 +13,7 @@ import { convertGitCommitsToProtoGitCommits } from "@shared/proto-conversions/fi
* @returns GitCommits containing the matching commits
*/
export const searchCommits: FileMethodHandler = async (controller: Controller, request: StringRequest): Promise<GitCommits> => {
const cwd = getWorkspacePath()
const cwd = await getWorkspacePath()
if (!cwd) {
return GitCommits.create({ commits: [] })
}
+2 -2
View File
@@ -12,10 +12,10 @@ import { convertSearchResultsToProtoFileInfos } from "@shared/proto-conversions/
* @returns Results containing matching files/folders
*/
export const searchFiles: FileMethodHandler = async (
controller: Controller,
_controller: Controller,
request: FileSearchRequest,
): Promise<FileSearchResults> => {
const workspacePath = getWorkspacePath()
const workspacePath = await getWorkspacePath()
if (!workspacePath) {
// Handle case where workspace path is not available
+1 -1
View File
@@ -37,6 +37,7 @@ export class ServiceRegistry {
* @param serviceName The name of the service (used for logging)
*/
constructor(serviceName: string) {
console.log(`Registering Protobus service: ${serviceName}...`)
this.serviceName = serviceName
}
@@ -56,7 +57,6 @@ export class ServiceRegistry {
}
this.methodMetadata[methodName] = { isStreaming, ...metadata }
console.log(`Registered ${this.serviceName} method: ${methodName}${isStreaming ? " (streaming)" : ""}`)
}
/**
+83 -104
View File
@@ -1,31 +1,31 @@
import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
import { getCwd, getDesktopDir } from "@/utils/path"
import { Anthropic } from "@anthropic-ai/sdk"
import { buildApiHandler } from "@api/index"
import { cleanupLegacyCheckpoints } from "@integrations/checkpoints/CheckpointMigration"
import { extractCommitMessage } from "@integrations/git/commit-message-generator"
import { downloadTask } from "@integrations/misc/export-markdown"
import WorkspaceTracker from "@integrations/workspace/WorkspaceTracker"
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, StoredChatSettings } from "@shared/ChatSettings"
import { ClineRulesToggles } from "@shared/cline-rules"
import { ExtensionMessage, ExtensionState, Platform } from "@shared/ExtensionMessage"
import { HistoryItem } from "@shared/HistoryItem"
import { McpMarketplaceCatalog } from "@shared/mcp"
import { TelemetrySetting } from "@shared/TelemetrySetting"
import { UserInfo } from "@shared/UserInfo"
import { WebviewMessage } from "@shared/WebviewMessage"
import { fileExistsAtPath } from "@utils/fs"
import { getWorkingState } from "@utils/git"
import axios from "axios"
import { v4 as uuidv4 } from "uuid"
import fs from "fs/promises"
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
import pWaitFor from "p-wait-for"
import * as path from "path"
import * as vscode from "vscode"
import { handleGrpcRequest, handleGrpcRequestCancel } from "./grpc-handler"
import { buildApiHandler } from "@api/index"
import { cleanupLegacyCheckpoints } from "@integrations/checkpoints/CheckpointMigration"
import { downloadTask } from "@integrations/misc/export-markdown"
import WorkspaceTracker from "@integrations/workspace/WorkspaceTracker"
import { ClineAccountService } from "@services/account/ClineAccountService"
import { McpHub } from "@services/mcp/McpHub"
import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
import { ApiProvider, ModelInfo } from "@shared/api"
import { ChatContent } from "@shared/ChatContent"
import { ChatSettings, StoredChatSettings } from "@shared/ChatSettings"
import { ExtensionMessage, ExtensionState, Platform } from "@shared/ExtensionMessage"
import { HistoryItem } from "@shared/HistoryItem"
import { McpMarketplaceCatalog } from "@shared/mcp"
import { UserInfo } from "@shared/UserInfo"
import { TelemetrySetting } from "@shared/TelemetrySetting"
import { WebviewMessage } from "@shared/WebviewMessage"
import { fileExistsAtPath } from "@utils/fs"
import { getWorkingState } from "@utils/git"
import { extractCommitMessage } from "@integrations/git/commit-message-generator"
import { ensureMcpServersDirectoryExists, ensureSettingsDirectoryExists, GlobalFileNames } from "../storage/disk"
import {
getAllExtensionState,
@@ -33,19 +33,15 @@ import {
getSecret,
getWorkspaceState,
storeSecret,
updateApiConfiguration,
updateGlobalState,
updateWorkspaceState,
} from "../storage/state"
import { Task } from "../task"
import { ClineRulesToggles } from "@shared/cline-rules"
import { sendAuthCallbackEvent } from "./account/subscribeToAuthCallback"
import { handleGrpcRequest, handleGrpcRequestCancel } from "./grpc-handler"
import { sendMcpMarketplaceCatalogEvent } from "./mcp/subscribeToMcpMarketplaceCatalog"
import { sendStateUpdate } from "./state/subscribeToState"
import { sendAddToInputEvent } from "./ui/subscribeToAddToInput"
import { sendAuthCallbackEvent } from "./account/subscribeToAuthCallback"
import { sendMcpMarketplaceCatalogEvent } from "./mcp/subscribeToMcpMarketplaceCatalog"
import { sendRelinquishControlEvent } from "./ui/subscribeToRelinquishControl"
import { handleTaskServiceRequest } from "./task"
import { BooleanRequest } from "@shared/proto/common"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -63,7 +59,7 @@ export class Controller {
workspaceTracker: WorkspaceTracker
mcpHub: McpHub
accountService: ClineAccountService
latestAnnouncementId = "may-22-2025_16:11:00" // update to some unique identifier when we add a new announcement
latestAnnouncementId = "june-25-2025_16:11:00" // update to some unique identifier when we add a new announcement
constructor(
readonly context: vscode.ExtensionContext,
@@ -82,13 +78,10 @@ export class Controller {
(msg) => this.postMessageToWebview(msg),
this.context.extension?.packageJSON?.version ?? "1.0.0",
)
this.accountService = new ClineAccountService(
(msg) => this.postMessageToWebview(msg),
async () => {
const { apiConfiguration } = await this.getStateToPostToWebview()
return apiConfiguration?.clineApiKey
},
)
this.accountService = new ClineAccountService(async () => {
const { apiConfiguration } = await this.getStateToPostToWebview()
return apiConfiguration?.clineApiKey
})
// Clean up legacy checkpoints
cleanupLegacyCheckpoints(this.context.globalStorageUri.fsPath, this.outputChannel).catch((error) => {
@@ -120,7 +113,7 @@ export class Controller {
try {
await storeSecret(this.context, "clineApiKey", undefined)
await updateGlobalState(this.context, "userInfo", undefined)
await updateWorkspaceState(this.context, "apiProvider", "openrouter")
await updateGlobalState(this.context, "apiProvider", "openrouter")
await this.postStateToWebview()
vscode.window.showInformationMessage("Successfully logged out of Cline")
} catch (error) {
@@ -175,7 +168,6 @@ export class Controller {
this.workspaceTracker,
(historyItem) => this.updateTaskHistory(historyItem),
() => this.postStateToWebview(),
(message) => this.postMessageToWebview(message),
(taskId) => this.reinitExistingTaskFromId(taskId),
() => this.cancelTask(),
apiConfiguration,
@@ -187,6 +179,7 @@ export class Controller {
terminalOutputLineLimit ?? 500,
defaultTerminalProfile ?? "default",
enableCheckpointsSetting ?? true,
await getCwd(getDesktopDir()),
task,
images,
files,
@@ -240,6 +233,7 @@ export class Controller {
await updateGlobalState(this.context, "telemetrySetting", telemetrySetting)
const isOptedIn = telemetrySetting !== "disabled"
telemetryService.updateTelemetryState(isOptedIn)
await this.postStateToWebview()
}
async togglePlanActModeWithChatSettings(chatSettings: ChatSettings, chatContent?: ChatContent): Promise<boolean> {
@@ -262,11 +256,6 @@ export class Controller {
previousModeReasoningEffort: newReasoningEffort,
previousModeAwsBedrockCustomSelected: newAwsBedrockCustomSelected,
previousModeAwsBedrockCustomModelBaseId: newAwsBedrockCustomModelBaseId,
previousModeSapAiCoreClientId: newSapAiCoreClientId,
previousModeSapAiCoreClientSecret: newSapAiCoreClientSecret,
previousModeSapAiCoreBaseUrl: newSapAiCoreBaseUrl,
previousModeSapAiCoreTokenUrl: newSapAiCoreTokenUrl,
previousModeSapAiCoreResourceGroup: newSapAiResourceGroup,
previousModeSapAiCoreModelId: newSapAiCoreModelId,
planActSeparateModelsSetting,
} = await getAllExtensionState(this.context)
@@ -275,9 +264,9 @@ export class Controller {
if (shouldSwitchModel) {
// Save the last model used in this mode
await updateWorkspaceState(this.context, "previousModeApiProvider", apiConfiguration.apiProvider)
await updateWorkspaceState(this.context, "previousModeThinkingBudgetTokens", apiConfiguration.thinkingBudgetTokens)
await updateWorkspaceState(this.context, "previousModeReasoningEffort", apiConfiguration.reasoningEffort)
await updateGlobalState(this.context, "previousModeApiProvider", apiConfiguration.apiProvider)
await updateGlobalState(this.context, "previousModeThinkingBudgetTokens", apiConfiguration.thinkingBudgetTokens)
await updateGlobalState(this.context, "previousModeReasoningEffort", apiConfiguration.reasoningEffort)
switch (apiConfiguration.apiProvider) {
case "anthropic":
case "vertex":
@@ -287,16 +276,16 @@ export class Controller {
case "qwen":
case "deepseek":
case "xai":
await updateWorkspaceState(this.context, "previousModeModelId", apiConfiguration.apiModelId)
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.apiModelId)
break
case "bedrock":
await updateWorkspaceState(this.context, "previousModeModelId", apiConfiguration.apiModelId)
await updateWorkspaceState(
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.apiModelId)
await updateGlobalState(
this.context,
"previousModeAwsBedrockCustomSelected",
apiConfiguration.awsBedrockCustomSelected,
)
await updateWorkspaceState(
await updateGlobalState(
this.context,
"previousModeAwsBedrockCustomModelBaseId",
apiConfiguration.awsBedrockCustomModelBaseId,
@@ -304,51 +293,38 @@ export class Controller {
break
case "openrouter":
case "cline":
await updateWorkspaceState(this.context, "previousModeModelId", apiConfiguration.openRouterModelId)
await updateWorkspaceState(this.context, "previousModeModelInfo", apiConfiguration.openRouterModelInfo)
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.openRouterModelId)
await updateGlobalState(this.context, "previousModeModelInfo", apiConfiguration.openRouterModelInfo)
break
case "vscode-lm":
// Important we don't set modelId to this, as it's an object not string (webview expects model id to be a string)
await updateWorkspaceState(
await updateGlobalState(
this.context,
"previousModeVsCodeLmModelSelector",
apiConfiguration.vsCodeLmModelSelector,
)
break
case "openai":
await updateWorkspaceState(this.context, "previousModeModelId", apiConfiguration.openAiModelId)
await updateWorkspaceState(this.context, "previousModeModelInfo", apiConfiguration.openAiModelInfo)
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.openAiModelId)
await updateGlobalState(this.context, "previousModeModelInfo", apiConfiguration.openAiModelInfo)
break
case "ollama":
await updateWorkspaceState(this.context, "previousModeModelId", apiConfiguration.ollamaModelId)
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.ollamaModelId)
break
case "lmstudio":
await updateWorkspaceState(this.context, "previousModeModelId", apiConfiguration.lmStudioModelId)
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.lmStudioModelId)
break
case "litellm":
await updateWorkspaceState(this.context, "previousModeModelId", apiConfiguration.liteLlmModelId)
await updateWorkspaceState(this.context, "previousModeModelInfo", apiConfiguration.liteLlmModelInfo)
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.liteLlmModelId)
await updateGlobalState(this.context, "previousModeModelInfo", apiConfiguration.liteLlmModelInfo)
break
case "requesty":
await updateWorkspaceState(this.context, "previousModeModelId", apiConfiguration.requestyModelId)
await updateWorkspaceState(this.context, "previousModeModelInfo", apiConfiguration.requestyModelInfo)
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.requestyModelId)
await updateGlobalState(this.context, "previousModeModelInfo", apiConfiguration.requestyModelInfo)
break
case "sapaicore":
await updateWorkspaceState(this.context, "previousModeModelId", apiConfiguration.apiModelId)
await updateWorkspaceState(this.context, "previousModeSapAiCoreClientId", apiConfiguration.sapAiCoreClientId)
await updateWorkspaceState(
this.context,
"previousModeSapAiCoreClientSecret",
apiConfiguration.sapAiCoreClientSecret,
)
await updateWorkspaceState(this.context, "previousModeSapAiCoreBaseUrl", apiConfiguration.sapAiCoreBaseUrl)
await updateWorkspaceState(this.context, "previousModeSapAiCoreTokenUrl", apiConfiguration.sapAiCoreTokenUrl)
await updateWorkspaceState(
this.context,
"previousModeSapAiCoreResourceGroup",
apiConfiguration.sapAiResourceGroup,
)
await updateWorkspaceState(this.context, "previousModeSapAiCoreModelId", apiConfiguration.sapAiCoreModelId)
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.apiModelId)
await updateGlobalState(this.context, "previousModeSapAiCoreModelId", apiConfiguration.sapAiCoreModelId)
break
}
@@ -360,9 +336,9 @@ export class Controller {
newReasoningEffort ||
newVsCodeLmModelSelector
) {
await updateWorkspaceState(this.context, "apiProvider", newApiProvider)
await updateWorkspaceState(this.context, "thinkingBudgetTokens", newThinkingBudgetTokens)
await updateWorkspaceState(this.context, "reasoningEffort", newReasoningEffort)
await updateGlobalState(this.context, "apiProvider", newApiProvider)
await updateGlobalState(this.context, "thinkingBudgetTokens", newThinkingBudgetTokens)
await updateGlobalState(this.context, "reasoningEffort", newReasoningEffort)
switch (newApiProvider) {
case "anthropic":
case "vertex":
@@ -372,41 +348,42 @@ export class Controller {
case "qwen":
case "deepseek":
case "xai":
await updateWorkspaceState(this.context, "apiModelId", newModelId)
await updateGlobalState(this.context, "apiModelId", newModelId)
break
case "bedrock":
await updateWorkspaceState(this.context, "apiModelId", newModelId)
await updateWorkspaceState(this.context, "awsBedrockCustomSelected", newAwsBedrockCustomSelected)
await updateWorkspaceState(this.context, "awsBedrockCustomModelBaseId", newAwsBedrockCustomModelBaseId)
await updateGlobalState(this.context, "apiModelId", newModelId)
await updateGlobalState(this.context, "awsBedrockCustomSelected", newAwsBedrockCustomSelected)
await updateGlobalState(this.context, "awsBedrockCustomModelBaseId", newAwsBedrockCustomModelBaseId)
break
case "openrouter":
case "cline":
await updateWorkspaceState(this.context, "openRouterModelId", newModelId)
await updateWorkspaceState(this.context, "openRouterModelInfo", newModelInfo)
await updateGlobalState(this.context, "openRouterModelId", newModelId)
await updateGlobalState(this.context, "openRouterModelInfo", newModelInfo)
break
case "vscode-lm":
await updateWorkspaceState(this.context, "vsCodeLmModelSelector", newVsCodeLmModelSelector)
await updateGlobalState(this.context, "vsCodeLmModelSelector", newVsCodeLmModelSelector)
break
case "openai":
await updateWorkspaceState(this.context, "openAiModelId", newModelId)
await updateWorkspaceState(this.context, "openAiModelInfo", newModelInfo)
await updateGlobalState(this.context, "openAiModelId", newModelId)
await updateGlobalState(this.context, "openAiModelInfo", newModelInfo)
break
case "ollama":
await updateWorkspaceState(this.context, "ollamaModelId", newModelId)
await updateGlobalState(this.context, "ollamaModelId", newModelId)
break
case "lmstudio":
await updateWorkspaceState(this.context, "lmStudioModelId", newModelId)
await updateGlobalState(this.context, "lmStudioModelId", newModelId)
break
case "litellm":
await updateWorkspaceState(this.context, "liteLlmModelId", newModelId)
await updateWorkspaceState(this.context, "liteLlmModelInfo", newModelInfo)
await updateGlobalState(this.context, "liteLlmModelId", newModelId)
await updateGlobalState(this.context, "liteLlmModelInfo", newModelInfo)
break
case "requesty":
await updateWorkspaceState(this.context, "requestyModelId", newModelId)
await updateWorkspaceState(this.context, "requestyModelInfo", newModelInfo)
await updateGlobalState(this.context, "requestyModelId", newModelId)
await updateGlobalState(this.context, "requestyModelInfo", newModelInfo)
break
case "sapaicore":
await updateWorkspaceState(this.context, "apiModelId", newModelId)
await updateGlobalState(this.context, "apiModelId", newModelId)
await updateGlobalState(this.context, "sapAiCoreModelId", newSapAiCoreModelId)
break
}
@@ -419,7 +396,7 @@ export class Controller {
// Save only non-mode properties to workspace storage
const { mode, ...persistentChatSettings }: { mode: string } & StoredChatSettings = chatSettings
await updateWorkspaceState(this.context, "chatSettings", persistentChatSettings)
await updateGlobalState(this.context, "chatSettings", persistentChatSettings)
await this.postStateToWebview()
if (this.task) {
@@ -493,7 +470,7 @@ export class Controller {
await sendAuthCallbackEvent(customToken)
const clineProvider: ApiProvider = "cline"
await updateWorkspaceState(this.context, "apiProvider", clineProvider)
await updateGlobalState(this.context, "apiProvider", clineProvider)
// Update API configuration with the new provider and API key
const { apiConfiguration } = await getAllExtensionState(this.context)
@@ -652,7 +629,7 @@ export class Controller {
}
const openrouter: ApiProvider = "openrouter"
await updateWorkspaceState(this.context, "apiProvider", openrouter)
await updateGlobalState(this.context, "apiProvider", openrouter)
await storeSecret(this.context, "openRouterApiKey", apiKey)
await this.postStateToWebview()
if (this.task) {
@@ -683,8 +660,8 @@ export class Controller {
// Context menus and code actions
getFileMentionFromPath(filePath: string) {
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
async getFileMentionFromPath(filePath: string) {
const cwd = await getCwd()
if (!cwd) {
return "@/" + filePath
}
@@ -699,7 +676,7 @@ export class Controller {
await setTimeoutPromise(100)
// Post message to webview with the selected code
const fileMention = this.getFileMentionFromPath(filePath)
const fileMention = await this.getFileMentionFromPath(filePath)
let input = `${fileMention}\n\`\`\`\n${code}\n\`\`\``
if (diagnostics) {
@@ -736,7 +713,7 @@ export class Controller {
await vscode.commands.executeCommand("claude-dev.SidebarProvider.focus")
await setTimeoutPromise(100)
const fileMention = this.getFileMentionFromPath(filePath)
const fileMention = await this.getFileMentionFromPath(filePath)
const problemsString = this.convertDiagnosticsToProblemsString(diagnostics)
await this.initTask(`Fix the following code in ${fileMention}\n\`\`\`\n${code}\n\`\`\`\n\nProblems:\n${problemsString}`)
@@ -852,6 +829,7 @@ export class Controller {
terminalReuseEnabled,
defaultTerminalProfile,
isNewUser,
welcomeViewCompleted,
mcpResponsesCollapsed,
terminalOutputLineLimit,
} = await getAllExtensionState(this.context)
@@ -906,6 +884,7 @@ export class Controller {
terminalReuseEnabled,
defaultTerminalProfile,
isNewUser,
welcomeViewCompleted: welcomeViewCompleted as boolean, // Can be undefined but is set to either true or false by the migration that runs on extension launch in extension.ts
mcpResponsesCollapsed,
terminalOutputLineLimit,
}
@@ -915,7 +894,7 @@ export class Controller {
if (this.task) {
await telemetryService.sendCollectedEvents(this.task.taskId)
}
this.task?.abortTask()
await this.task?.abortTask()
this.task = undefined // removes reference to it, so once promises end it will be garbage collected
}
@@ -988,7 +967,7 @@ export class Controller {
async generateGitCommitMessage() {
try {
// Check if there's a workspace folder open
const cwd = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
const cwd = await getCwd()
if (!cwd) {
vscode.window.showErrorMessage("No workspace folder open")
return
@@ -0,0 +1,25 @@
import type { BooleanRequest } from "../../../shared/proto/common"
import { Empty } from "../../../shared/proto/common"
import type { Controller } from "../index"
import { updateGlobalState } from "../../storage/state"
/**
* Sets the welcomeViewCompleted flag to the specified boolean value
* @param controller The controller instance
* @param request The boolean request containing the value to set
* @returns Empty response
*/
export async function setWelcomeViewCompleted(controller: Controller, request: BooleanRequest): Promise<Empty> {
try {
// Update the global state to set welcomeViewCompleted to the requested value
await updateGlobalState(controller.context, "welcomeViewCompleted", request.value)
await controller.postStateToWebview()
console.log(`Welcome view completed set to: ${request.value}`)
return Empty.create({})
} catch (error) {
console.error("Failed to set welcome view completed:", error)
throw error
}
}
@@ -22,7 +22,7 @@ export async function updateAutoApprovalSettings(controller: Controller, request
await updateGlobalState(controller.context, "autoApprovalSettings", settings)
if (controller.task) {
controller.task.autoApprovalSettings = settings
controller.task.updateAutoApprovalSettings(settings)
}
await controller.postStateToWebview()
@@ -2,7 +2,6 @@ import path from "path"
import fs from "fs/promises"
import { Controller } from ".."
import { DeleteAllTaskHistoryCount } from "../../../shared/proto/task"
import { BooleanRequest } from "../../../shared/proto/common"
import { getGlobalState, updateGlobalState } from "../../storage/state"
import { fileExistsAtPath } from "../../../utils/fs"
import vscode from "vscode"
@@ -13,7 +12,7 @@ import vscode from "vscode"
* @param request Request with option to preserve favorites
* @returns Results with count of deleted tasks
*/
export async function deleteAllTaskHistory(controller: Controller, request: BooleanRequest): Promise<DeleteAllTaskHistoryCount> {
export async function deleteAllTaskHistory(controller: Controller): Promise<DeleteAllTaskHistoryCount> {
try {
// Clear current task first
await controller.clearTask()
@@ -22,8 +21,22 @@ export async function deleteAllTaskHistory(controller: Controller, request: Bool
const taskHistory = ((await getGlobalState(controller.context, "taskHistory")) as any[]) || []
const totalTasks = taskHistory.length
const userChoice = await vscode.window.showWarningMessage(
"What would you like to delete?",
{ modal: true },
"Delete All Except Favorites",
"Delete Everything",
)
// Default VS Code Cancel button returns `undefined` - don't delete anything
if (userChoice === undefined) {
return DeleteAllTaskHistoryCount.create({
tasksDeleted: 0,
})
}
// If preserving favorites, filter out non-favorites
if (request.value) {
if (userChoice === "Delete All Except Favorites") {
const favoritedTasks = taskHistory.filter((task) => task.isFavorited === true)
// If there are favorited tasks, update state
@@ -45,9 +58,20 @@ export async function deleteAllTaskHistory(controller: Controller, request: Bool
tasksDeleted: totalTasks - favoritedTasks.length,
})
} else {
return DeleteAllTaskHistoryCount.create({
tasksDeleted: 0,
})
// No favorited tasks found - show warning and ask user what to do
const answer = await vscode.window.showWarningMessage(
"No favorited tasks found. Would you like to delete all tasks anyway?",
{ modal: true },
"Delete All Tasks",
)
// User cancelled - don't delete anything
if (answer === undefined) {
return DeleteAllTaskHistoryCount.create({
tasksDeleted: 0,
})
}
// If user chose "Delete All Tasks", fall through to the `delete everything` section below
}
}
@@ -1,88 +0,0 @@
import path from "path"
import fs from "fs/promises"
import { Controller } from ".."
import { EmptyRequest } from "../../../shared/proto/common"
import { DeleteNonFavoritedTasksResults } from "../../../shared/proto/task"
import { getGlobalState, updateGlobalState } from "../../storage/state"
import { fileExistsAtPath } from "../../../utils/fs"
/**
* Deletes all non-favorited tasks, preserving only favorited ones
* @param controller The controller instance
* @param request Empty request
* @returns DeleteNonFavoritedTasksResults with counts of preserved and deleted tasks
*/
export async function deleteNonFavoritedTasks(
controller: Controller,
_request: EmptyRequest,
): Promise<DeleteNonFavoritedTasksResults> {
try {
// Clear current task first
await controller.clearTask()
// Get existing task history
const taskHistory = ((await getGlobalState(controller.context, "taskHistory")) as any[]) || []
// Filter out non-favorited tasks
const favoritedTasks = taskHistory.filter((task) => task.isFavorited === true)
const deletedCount = taskHistory.length - favoritedTasks.length
console.log(`[deleteNonFavoritedTasks] Found ${favoritedTasks.length} favorited tasks to preserve`)
// Update global state
if (favoritedTasks.length > 0) {
await updateGlobalState(controller.context, "taskHistory", favoritedTasks)
} else {
await updateGlobalState(controller.context, "taskHistory", undefined)
}
// Handle file system cleanup for deleted tasks
const preserveTaskIds = favoritedTasks.map((task) => task.id)
await cleanupTaskFiles(controller, preserveTaskIds)
// Update webview
try {
await controller.postStateToWebview()
} catch (webviewErr) {
console.error("Error posting to webview:", webviewErr)
}
return DeleteNonFavoritedTasksResults.create({
tasksPreserved: favoritedTasks.length,
tasksDeleted: deletedCount,
})
} catch (error) {
console.error("Error in deleteNonFavoritedTasks:", error)
throw error
}
}
/**
* Helper function to cleanup task files while preserving specified tasks
*/
async function cleanupTaskFiles(controller: Controller, preserveTaskIds: string[]) {
const taskDirPath = path.join(controller.context.globalStorageUri.fsPath, "tasks")
try {
if (await fileExistsAtPath(taskDirPath)) {
if (preserveTaskIds.length > 0) {
const taskDirs = await fs.readdir(taskDirPath)
console.debug(`[cleanupTaskFiles] Found ${taskDirs.length} task directories`)
// Delete only non-preserved task directories
for (const dir of taskDirs) {
if (!preserveTaskIds.includes(dir)) {
await fs.rm(path.join(taskDirPath, dir), { recursive: true, force: true })
}
}
} else {
// No tasks to preserve, delete everything
await fs.rm(taskDirPath, { recursive: true, force: true })
}
}
} catch (error) {
console.error("Error cleaning up task files:", error)
}
return true
}
@@ -1,5 +1,6 @@
import path from "path"
import fs from "fs/promises"
import vscode from "vscode"
import { Controller } from ".."
import { Empty, StringArrayRequest, BooleanRequest } from "../../../shared/proto/common"
import { TaskMethodHandler } from "./index"
@@ -20,6 +21,18 @@ export const deleteTasksWithIds: TaskMethodHandler = async (
throw new Error("Missing task IDs")
}
const taskCount = request.value.length
const message =
taskCount === 1
? "Are you sure you want to delete this task? This action cannot be undone."
: `Are you sure you want to delete these ${taskCount} tasks? This action cannot be undone.`
const userChoice = await vscode.window.showWarningMessage(message, { modal: true }, "Delete")
if (userChoice === undefined) {
return Empty.create()
}
for (const id of request.value) {
await deleteTaskWithId(controller, id)
}
+1 -1
View File
@@ -15,7 +15,7 @@ export async function getTaskHistory(controller: Controller, request: GetTaskHis
// Get task history from global state
const taskHistory = ((await getGlobalState(controller.context, "taskHistory")) as any[]) || []
const workspacePath = getWorkspacePath()
const workspacePath = await getWorkspacePath()
// Apply filters
let filteredTasks = taskHistory.filter((item) => {
+2 -2
View File
@@ -1,7 +1,7 @@
import type { Controller } from "../index"
import { EmptyRequest, Empty } from "@shared/proto/common"
import { handleModelsServiceRequest } from "../models"
import { getAllExtensionState, getGlobalState, updateWorkspaceState } from "../../storage/state"
import { getAllExtensionState, getGlobalState, updateGlobalState } from "../../storage/state"
import { sendOpenRouterModelsEvent } from "../models/subscribeToOpenRouterModels"
import { sendMcpMarketplaceCatalogEvent } from "../mcp/subscribeToMcpMarketplaceCatalog"
import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
@@ -32,7 +32,7 @@ export async function initializeWebview(controller: Controller, request: EmptyRe
// Update model info in state (this needs to be done here since we don't want to update state while settings is open, and we may refresh models there)
const { apiConfiguration } = await getAllExtensionState(controller.context)
if (apiConfiguration.openRouterModelId && response.models[apiConfiguration.openRouterModelId]) {
await updateWorkspaceState(
await updateGlobalState(
controller.context,
"openRouterModelInfo",
response.models[apiConfiguration.openRouterModelId],
+3 -2
View File
@@ -11,13 +11,14 @@ import { getLatestTerminalOutput } from "@integrations/terminal/get-latest-outpu
import { getCommitInfo } from "@utils/git"
import { getWorkingState } from "@utils/git"
import { FileContextTracker } from "../context/context-tracking/FileContextTracker"
import { getCwd } from "@/utils/path"
export function openMention(mention?: string): void {
export async function openMention(mention?: string): Promise<void> {
if (!mention) {
return
}
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
const cwd = await getCwd()
if (!cwd) {
return
}
@@ -234,10 +234,9 @@ In each user message, the environment_details will specify the current mode. The
## What is PLAN MODE?
- While you are usually in ACT MODE, the user may switch to PLAN MODE in order to have a back and forth with you to plan how to best accomplish the task.
- When starting in PLAN MODE, depending on the user's request, you may need to do some information gathering e.g. using ${readTool.name} or ${grepToolDefinition.name} to get more context about the task. You may also ask the user clarifying questions to get a better understanding of the task. You may return mermaid diagrams to visually display your understanding.
- Once you've gained more context about the user's request, you should architect a detailed plan for how you will accomplish the task. Returning mermaid diagrams may be helpful here as well.
- When starting in PLAN MODE, depending on the user's request, you may need to do some information gathering e.g. using ${readTool.name} or ${grepToolDefinition.name} to get more context about the task. You may also ask the user clarifying questions to get a better understanding of the task.
- Once you've gained more context about the user's request, you should architect a detailed plan for how you will accomplish the task.
- Then you might ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and plan the best way to accomplish it.
- If at any point a mermaid diagram would make your plan clearer to help the user quickly see the structure, you are encouraged to include a Mermaid code block in the response. (Note: if you use colors in your mermaid diagrams, be sure to use high contrast colors so the text is readable.)
- Finally once it seems like you've reached a good plan, ask the user to switch you back to ACT MODE to implement the solution.
====
@@ -257,7 +256,6 @@ CAPABILITIES
: ""
}
- You have access to MCP servers that may provide additional tools and resources. Each server may provide different capabilities that you can use to accomplish tasks more effectively.
- You can use LaTeX syntax in your responses to render mathematical expressions
====
+2 -4
View File
@@ -574,10 +574,9 @@ In each user message, the environment_details will specify the current mode. The
## What is PLAN MODE?
- While you are usually in ACT MODE, the user may switch to PLAN MODE in order to have a back and forth with you to plan how to best accomplish the task.
- When starting in PLAN MODE, depending on the user's request, you may need to do some information gathering e.g. using read_file or search_files to get more context about the task. You may also ask the user clarifying questions with ask_followup_question to get a better understanding of the task. You may return mermaid diagrams to visually display your understanding.
- Once you've gained more context about the user's request, you should architect a detailed plan for how you will accomplish the task. Returning mermaid diagrams may be helpful here as well. Present the plan to the user using the plan_mode_respond tool.
- When starting in PLAN MODE, depending on the user's request, you may need to do some information gathering e.g. using read_file or search_files to get more context about the task. You may also ask the user clarifying questions with ask_followup_question to get a better understanding of the task.
- Once you've gained more context about the user's request, you should architect a detailed plan for how you will accomplish the task. Present the plan to the user using the plan_mode_respond tool.
- Then you might ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and plan the best way to accomplish it.
- If at any point a mermaid diagram would make your plan clearer to help the user quickly see the structure, you are encouraged to include a Mermaid code block in the response. (Note: if you use colors in your mermaid diagrams, be sure to use high contrast colors so the text is readable.)
- Finally once it seems like you've reached a good plan, ask the user to switch you back to ACT MODE to implement the solution.
====
@@ -597,7 +596,6 @@ CAPABILITIES
: ""
}
- You have access to MCP servers that may provide additional tools and resources. Each server may provide different capabilities that you can use to accomplish tasks more effectively.
- You can use LaTeX syntax in your responses to render mathematical expressions
====
+3
View File
@@ -35,6 +35,9 @@ Otherwise, if you have not completed the task and do not need additional informa
tooManyMistakes: (feedback?: string) =>
`You seem to be having trouble proceeding. The user has provided the following feedback to help guide you:\n<feedback>\n${feedback}\n</feedback>`,
autoApprovalMaxReached: (feedback?: string) =>
`Auto-approval limit reached. The user has provided the following feedback to help guide you:\n<feedback>\n${feedback}\n</feedback>`,
missingToolParameterError: (paramName: string) =>
`Missing value for required parameter '${paramName}'. Please retry with complete response.\n\n${toolUseInstructionsReminder}`,
+2 -4
View File
@@ -568,10 +568,9 @@ In each user message, the environment_details will specify the current mode. The
## What is PLAN MODE?
- While you are usually in ACT MODE, the user may switch to PLAN MODE in order to have a back and forth with you to plan how to best accomplish the task.
- When starting in PLAN MODE, depending on the user's request, you may need to do some information gathering e.g. using read_file or search_files to get more context about the task. You may also ask the user clarifying questions to get a better understanding of the task. You may return mermaid diagrams to visually display your understanding.
- Once you've gained more context about the user's request, you should architect a detailed plan for how you will accomplish the task. Returning mermaid diagrams may be helpful here as well.
- When starting in PLAN MODE, depending on the user's request, you may need to do some information gathering e.g. using read_file or search_files to get more context about the task. You may also ask the user clarifying questions to get a better understanding of the task.
- Once you've gained more context about the user's request, you should architect a detailed plan for how you will accomplish the task.
- Then you might ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and plan the best way to accomplish it.
- If at any point a mermaid diagram would make your plan clearer to help the user quickly see the structure, you are encouraged to include a Mermaid code block in the response. (Note: if you use colors in your mermaid diagrams, be sure to use high contrast colors so the text is readable.)
- Finally once it seems like you've reached a good plan, ask the user to switch you back to ACT MODE to implement the solution.
====
@@ -591,7 +590,6 @@ CAPABILITIES
: ""
}
- You have access to MCP servers that may provide additional tools and resources. Each server may provide different capabilities that you can use to accomplish tasks more effectively.
- You can use LaTeX syntax in your responses to render mathematical expressions
====
+5 -11
View File
@@ -71,18 +71,14 @@ export type GlobalStateKey =
| "terminalReuseEnabled"
| "defaultTerminalProfile"
| "isNewUser"
| "welcomeViewCompleted"
| "terminalOutputLineLimit"
| "mcpRichDisplayEnabled"
| "sapAiCoreTokenUrl"
| "sapAiCoreBaseUrl"
| "sapAiResourceGroup"
| "sapAiCoreClientId"
| "sapAiCoreClientSecret"
| "sapAiCoreModelId"
| "claudeCodePath"
export type LocalStateKey =
| "localClineRulesToggles"
// Settings around plan/act and ephemeral model configuration
| "chatSettings"
// Current active model configuration (per workspace)
| "apiProvider"
@@ -104,6 +100,7 @@ export type LocalStateKey =
| "requestyModelInfo"
| "togetherModelId"
| "fireworksModelId"
| "sapAiCoreModelId"
// Previous mode saved configurations (per workspace)
| "previousModeApiProvider"
| "previousModeModelId"
@@ -113,9 +110,6 @@ export type LocalStateKey =
| "previousModeReasoningEffort"
| "previousModeAwsBedrockCustomSelected"
| "previousModeAwsBedrockCustomModelBaseId"
| "previousModeSapAiCoreClientId"
| "previousModeSapAiCoreClientSecret"
| "previousModeSapAiCoreBaseUrl"
| "previousModeSapAiCoreTokenUrl"
| "previousModeSapAiCoreResourceGroup"
| "previousModeSapAiCoreModelId"
export type LocalStateKey = "localClineRulesToggles" | "localCursorRulesToggles" | "localWindsurfRulesToggles" | "workflowToggles"
+95 -20
View File
@@ -2,11 +2,11 @@ import * as vscode from "vscode"
import { ensureRulesDirectoryExists } from "./disk"
import fs from "fs/promises"
import path from "path"
import { getGlobalState, getWorkspaceState, updateGlobalState, updateWorkspaceState } from "./state"
import { updateGlobalState, getAllExtensionState, getGlobalState } from "./state"
import { GlobalStateKey } from "./state-keys"
export async function migratePlanActGlobalToWorkspaceStorage(context: vscode.ExtensionContext) {
// Keys that were migrated from global storage to workspace storage
export async function migrateWorkspaceToGlobalStorage(context: vscode.ExtensionContext) {
// Keys to migrate from workspace storage back to global storage
const keysToMigrate = [
// Core settings
"apiProvider",
@@ -31,6 +31,7 @@ export async function migratePlanActGlobalToWorkspaceStorage(context: vscode.Ext
"requestyModelInfo",
"togetherModelId",
"fireworksModelId",
"sapAiCoreModelId",
// Previous mode settings
"previousModeApiProvider",
@@ -41,17 +42,24 @@ export async function migratePlanActGlobalToWorkspaceStorage(context: vscode.Ext
"previousModeReasoningEffort",
"previousModeAwsBedrockCustomSelected",
"previousModeAwsBedrockCustomModelBaseId",
"previousModeSapAiCoreModelId",
]
for (const key of keysToMigrate) {
const globalValue = await getGlobalState(context, key as GlobalStateKey)
if (globalValue !== undefined) {
const workspaceValue = await getWorkspaceState(context, key)
if (workspaceValue === undefined) {
await updateWorkspaceState(context, key, globalValue)
}
// Delete from global storage regardless of whether we copied it
await updateGlobalState(context, key as GlobalStateKey, undefined)
// Use raw workspace state since these keys shouldn't be in workspace storage
const workspaceValue = await context.workspaceState.get(key)
const globalValue = await context.globalState.get(key)
if (workspaceValue !== undefined && globalValue === undefined) {
console.log(`[Storage Migration] migrating key: ${key} to global storage. Current value: ${workspaceValue}`)
// Move to global storage
await updateGlobalState(context, key as GlobalStateKey, workspaceValue)
// Remove from workspace storage
await context.workspaceState.update(key, undefined)
const newWorkspaceValue = await context.workspaceState.get(key)
console.log(`[Storage Migration] migrated key: ${key} to global storage. Current value: ${newWorkspaceValue}`)
}
}
}
@@ -126,22 +134,89 @@ export async function migrateCustomInstructionsToGlobalRules(context: vscode.Ext
export async function migrateModeFromWorkspaceStorageToControllerState(context: vscode.ExtensionContext) {
try {
// Get current chatSettings from workspace storage
const chatSettings = (await getWorkspaceState(context, "chatSettings")) as any
// Check legacy workspace storage (use raw methods since chatSettings is now global)
const workspaceChatSettings = (await context.workspaceState.get("chatSettings")) as any
if (chatSettings && typeof chatSettings === "object" && "mode" in chatSettings) {
console.log("Cleaning up mode from workspace storage...")
if (workspaceChatSettings && typeof workspaceChatSettings === "object" && "mode" in workspaceChatSettings) {
console.log("Cleaning up mode from legacy workspace storage...")
// Remove mode property from chatSettings
const { mode, ...cleanedChatSettings } = chatSettings
const { mode, ...cleanedChatSettings } = workspaceChatSettings
// Save cleaned chatSettings back to workspace storage
await updateWorkspaceState(context, "chatSettings", cleanedChatSettings)
// Save cleaned chatSettings back to workspace storage (will be migrated later)
await context.workspaceState.update("chatSettings", cleanedChatSettings)
console.log("Successfully removed mode from workspace storage chatSettings")
console.log("Successfully removed mode from legacy workspace storage chatSettings")
}
// Also check global storage for any mode cleanup needed
const globalChatSettings = (await context.globalState.get("chatSettings")) as any
if (globalChatSettings && typeof globalChatSettings === "object" && "mode" in globalChatSettings) {
console.log("Cleaning up mode from global storage...")
// Remove mode property from chatSettings
const { mode, ...cleanedChatSettings } = globalChatSettings
// Save cleaned chatSettings back to global storage
await updateGlobalState(context, "chatSettings", cleanedChatSettings)
console.log("Successfully removed mode from global storage chatSettings")
}
} catch (error) {
console.error("Failed to cleanup mode from workspace storage:", error)
console.error("Failed to cleanup mode from storage:", error)
// Continue execution - migration failure shouldn't break extension startup
}
}
export async function migrateWelcomeViewCompleted(context: vscode.ExtensionContext) {
try {
// Check if welcomeViewCompleted is already set
const welcomeViewCompleted = await getGlobalState(context, "welcomeViewCompleted")
if (welcomeViewCompleted === undefined) {
console.log("Migrating welcomeViewCompleted setting...")
// Get all extension state to check for existing API keys
const extensionState = await getAllExtensionState(context)
const config = extensionState.apiConfiguration
// This is the original logic used for checking is the welcome view should be shown
// It was located in the ExtensionStateContextProvider
const hasKey = config
? [
config.apiKey,
config.openRouterApiKey,
config.awsRegion,
config.vertexProjectId,
config.openAiApiKey,
config.ollamaModelId,
config.lmStudioModelId,
config.liteLlmApiKey,
config.geminiApiKey,
config.openAiNativeApiKey,
config.deepSeekApiKey,
config.requestyApiKey,
config.togetherApiKey,
config.qwenApiKey,
config.doubaoApiKey,
config.mistralApiKey,
config.vsCodeLmModelSelector,
config.clineApiKey,
config.asksageApiKey,
config.xaiApiKey,
config.sambanovaApiKey,
config.sapAiCoreClientId,
].some((key) => key !== undefined)
: false
// Set welcomeViewCompleted based on whether user has keys
await updateGlobalState(context, "welcomeViewCompleted", hasKey)
console.log(`Migration: Set welcomeViewCompleted to ${hasKey} based on existing API keys`)
}
} catch (error) {
console.error("Failed to migrate welcomeViewCompleted:", error)
// Continue execution - migration failure shouldn't break extension startup
}
}
+59 -72
View File
@@ -2,7 +2,7 @@ import * as vscode from "vscode"
import { DEFAULT_CHAT_SETTINGS } from "@shared/ChatSettings"
import { DEFAULT_BROWSER_SETTINGS } from "@shared/BrowserSettings"
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "@shared/AutoApprovalSettings"
import { GlobalStateKey, SecretKey } from "./state-keys"
import { GlobalStateKey, LocalStateKey, SecretKey } from "./state-keys"
import { ApiConfiguration, ApiProvider, BedrockModelId, ModelInfo } from "@shared/api"
import { HistoryItem } from "@shared/HistoryItem"
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
@@ -44,17 +44,18 @@ export async function getSecret(context: vscode.ExtensionContext, key: SecretKey
// workspace
export async function updateWorkspaceState(context: vscode.ExtensionContext, key: string, value: any) {
export async function updateWorkspaceState(context: vscode.ExtensionContext, key: LocalStateKey, value: any) {
await context.workspaceState.update(key, value)
}
export async function getWorkspaceState(context: vscode.ExtensionContext, key: string) {
export async function getWorkspaceState(context: vscode.ExtensionContext, key: LocalStateKey) {
return await context.workspaceState.get(key)
}
export async function getAllExtensionState(context: vscode.ExtensionContext) {
const [
isNewUser,
welcomeViewCompleted,
apiKey,
openRouterApiKey,
clineApiKey,
@@ -124,10 +125,10 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
sapAiCoreBaseUrl,
sapAiCoreTokenUrl,
sapAiResourceGroup,
sapAiCoreModelId,
claudeCodePath,
] = await Promise.all([
getGlobalState(context, "isNewUser") as Promise<boolean | undefined>,
getGlobalState(context, "welcomeViewCompleted") as Promise<boolean | undefined>,
getSecret(context, "apiKey") as Promise<string | undefined>,
getSecret(context, "openRouterApiKey") as Promise<string | undefined>,
getSecret(context, "clineApiKey") as Promise<string | undefined>,
@@ -197,7 +198,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
getGlobalState(context, "sapAiCoreBaseUrl") as Promise<string | undefined>,
getGlobalState(context, "sapAiCoreTokenUrl") as Promise<string | undefined>,
getGlobalState(context, "sapAiResourceGroup") as Promise<string | undefined>,
getGlobalState(context, "sapAiCoreModelId") as Promise<string | undefined>,
getGlobalState(context, "claudeCodePath") as Promise<string | undefined>,
])
@@ -232,47 +232,39 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
previousModeReasoningEffort,
previousModeAwsBedrockCustomSelected,
previousModeAwsBedrockCustomModelBaseId,
previousModeSapAiCoreClientId,
previousModeSapAiCoreClientSecret,
previousModeSapAiCoreBaseUrl,
previousModeSapAiCoreTokenUrl,
previousModeSapAiCoreResourceGroup,
previousModeSapAiCoreModelId,
sapAiCoreModelId,
] = await Promise.all([
getWorkspaceState(context, "chatSettings") as Promise<StoredChatSettings | undefined>,
getWorkspaceState(context, "apiProvider") as Promise<ApiProvider | undefined>,
getWorkspaceState(context, "apiModelId") as Promise<string | undefined>,
getWorkspaceState(context, "thinkingBudgetTokens") as Promise<number | undefined>,
getWorkspaceState(context, "reasoningEffort") as Promise<string | undefined>,
getWorkspaceState(context, "vsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
getWorkspaceState(context, "awsBedrockCustomSelected") as Promise<boolean | undefined>,
getWorkspaceState(context, "awsBedrockCustomModelBaseId") as Promise<BedrockModelId | undefined>,
getWorkspaceState(context, "openRouterModelId") as Promise<string | undefined>,
getWorkspaceState(context, "openRouterModelInfo") as Promise<ModelInfo | undefined>,
getWorkspaceState(context, "openAiModelId") as Promise<string | undefined>,
getWorkspaceState(context, "openAiModelInfo") as Promise<ModelInfo | undefined>,
getWorkspaceState(context, "ollamaModelId") as Promise<string | undefined>,
getWorkspaceState(context, "lmStudioModelId") as Promise<string | undefined>,
getWorkspaceState(context, "liteLlmModelId") as Promise<string | undefined>,
getWorkspaceState(context, "liteLlmModelInfo") as Promise<ModelInfo | undefined>,
getWorkspaceState(context, "requestyModelId") as Promise<string | undefined>,
getWorkspaceState(context, "requestyModelInfo") as Promise<ModelInfo | undefined>,
getWorkspaceState(context, "togetherModelId") as Promise<string | undefined>,
getWorkspaceState(context, "fireworksModelId") as Promise<string | undefined>,
getWorkspaceState(context, "previousModeApiProvider") as Promise<ApiProvider | undefined>,
getWorkspaceState(context, "previousModeModelId") as Promise<string | undefined>,
getWorkspaceState(context, "previousModeModelInfo") as Promise<ModelInfo | undefined>,
getWorkspaceState(context, "previousModeVsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
getWorkspaceState(context, "previousModeThinkingBudgetTokens") as Promise<number | undefined>,
getWorkspaceState(context, "previousModeReasoningEffort") as Promise<string | undefined>,
getWorkspaceState(context, "previousModeAwsBedrockCustomSelected") as Promise<boolean | undefined>,
getWorkspaceState(context, "previousModeAwsBedrockCustomModelBaseId") as Promise<BedrockModelId | undefined>,
getWorkspaceState(context, "previousModeSapAiCoreClientId") as Promise<string | undefined>,
getWorkspaceState(context, "previousModeSapAiCoreClientSecret") as Promise<string | undefined>,
getWorkspaceState(context, "previousModeSapAiCoreBaseUrl") as Promise<string | undefined>,
getWorkspaceState(context, "previousModeSapAiCoreTokenUrl") as Promise<string | undefined>,
getWorkspaceState(context, "previousModeSapAiCoreResourceGroup") as Promise<string | undefined>,
getWorkspaceState(context, "previousModeSapAiCoreModelId") as Promise<string | undefined>,
getGlobalState(context, "chatSettings") as Promise<StoredChatSettings | undefined>,
getGlobalState(context, "apiProvider") as Promise<ApiProvider | undefined>,
getGlobalState(context, "apiModelId") as Promise<string | undefined>,
getGlobalState(context, "thinkingBudgetTokens") as Promise<number | undefined>,
getGlobalState(context, "reasoningEffort") as Promise<string | undefined>,
getGlobalState(context, "vsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
getGlobalState(context, "awsBedrockCustomSelected") as Promise<boolean | undefined>,
getGlobalState(context, "awsBedrockCustomModelBaseId") as Promise<BedrockModelId | undefined>,
getGlobalState(context, "openRouterModelId") as Promise<string | undefined>,
getGlobalState(context, "openRouterModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "openAiModelId") as Promise<string | undefined>,
getGlobalState(context, "openAiModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "ollamaModelId") as Promise<string | undefined>,
getGlobalState(context, "lmStudioModelId") as Promise<string | undefined>,
getGlobalState(context, "liteLlmModelId") as Promise<string | undefined>,
getGlobalState(context, "liteLlmModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "requestyModelId") as Promise<string | undefined>,
getGlobalState(context, "requestyModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "togetherModelId") as Promise<string | undefined>,
getGlobalState(context, "fireworksModelId") as Promise<string | undefined>,
getGlobalState(context, "previousModeApiProvider") as Promise<ApiProvider | undefined>,
getGlobalState(context, "previousModeModelId") as Promise<string | undefined>,
getGlobalState(context, "previousModeModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "previousModeVsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
getGlobalState(context, "previousModeThinkingBudgetTokens") as Promise<number | undefined>,
getGlobalState(context, "previousModeReasoningEffort") as Promise<string | undefined>,
getGlobalState(context, "previousModeAwsBedrockCustomSelected") as Promise<boolean | undefined>,
getGlobalState(context, "previousModeAwsBedrockCustomModelBaseId") as Promise<BedrockModelId | undefined>,
getGlobalState(context, "previousModeSapAiCoreModelId") as Promise<string | undefined>,
getGlobalState(context, "sapAiCoreModelId") as Promise<string | undefined>,
])
let apiProvider: ApiProvider
@@ -388,6 +380,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
sapAiCoreModelId,
},
isNewUser: isNewUser ?? true,
welcomeViewCompleted,
lastShownAnnouncementId,
taskHistory,
autoApprovalSettings: autoApprovalSettings || DEFAULT_AUTO_APPROVAL_SETTINGS, // default value can be 0 or empty string
@@ -407,11 +400,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
previousModeReasoningEffort,
previousModeAwsBedrockCustomSelected,
previousModeAwsBedrockCustomModelBaseId,
previousModeSapAiCoreClientId,
previousModeSapAiCoreClientSecret,
previousModeSapAiCoreBaseUrl,
previousModeSapAiCoreTokenUrl,
previousModeSapAiCoreResourceGroup,
previousModeSapAiCoreModelId,
mcpMarketplaceEnabled: mcpMarketplaceEnabled,
mcpRichDisplayEnabled: mcpRichDisplayEnabled ?? true,
@@ -502,26 +490,28 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
sapAiCoreModelId,
claudeCodePath,
} = apiConfiguration
// Workspace state updates
await updateWorkspaceState(context, "apiProvider", apiProvider)
await updateWorkspaceState(context, "apiModelId", apiModelId)
await updateWorkspaceState(context, "thinkingBudgetTokens", thinkingBudgetTokens)
await updateWorkspaceState(context, "reasoningEffort", reasoningEffort)
await updateWorkspaceState(context, "vsCodeLmModelSelector", vsCodeLmModelSelector)
await updateWorkspaceState(context, "awsBedrockCustomSelected", awsBedrockCustomSelected)
await updateWorkspaceState(context, "awsBedrockCustomModelBaseId", awsBedrockCustomModelBaseId)
await updateWorkspaceState(context, "openRouterModelId", openRouterModelId)
await updateWorkspaceState(context, "openRouterModelInfo", openRouterModelInfo)
await updateWorkspaceState(context, "openAiModelId", openAiModelId)
await updateWorkspaceState(context, "openAiModelInfo", openAiModelInfo)
await updateWorkspaceState(context, "ollamaModelId", ollamaModelId)
await updateWorkspaceState(context, "lmStudioModelId", lmStudioModelId)
await updateWorkspaceState(context, "liteLlmModelId", liteLlmModelId)
await updateWorkspaceState(context, "liteLlmModelInfo", liteLlmModelInfo)
await updateWorkspaceState(context, "requestyModelId", requestyModelId)
await updateWorkspaceState(context, "requestyModelInfo", requestyModelInfo)
await updateWorkspaceState(context, "togetherModelId", togetherModelId)
await updateWorkspaceState(context, "fireworksModelId", fireworksModelId)
// Ephemeral model config updates
await updateGlobalState(context, "apiProvider", apiProvider)
await updateGlobalState(context, "apiModelId", apiModelId)
await updateGlobalState(context, "thinkingBudgetTokens", thinkingBudgetTokens)
await updateGlobalState(context, "reasoningEffort", reasoningEffort)
await updateGlobalState(context, "vsCodeLmModelSelector", vsCodeLmModelSelector)
await updateGlobalState(context, "awsBedrockCustomSelected", awsBedrockCustomSelected)
await updateGlobalState(context, "awsBedrockCustomModelBaseId", awsBedrockCustomModelBaseId)
await updateGlobalState(context, "openRouterModelId", openRouterModelId)
await updateGlobalState(context, "openRouterModelInfo", openRouterModelInfo)
await updateGlobalState(context, "openAiModelId", openAiModelId)
await updateGlobalState(context, "openAiModelInfo", openAiModelInfo)
await updateGlobalState(context, "ollamaModelId", ollamaModelId)
await updateGlobalState(context, "lmStudioModelId", lmStudioModelId)
await updateGlobalState(context, "liteLlmModelId", liteLlmModelId)
await updateGlobalState(context, "liteLlmModelInfo", liteLlmModelInfo)
await updateGlobalState(context, "requestyModelId", requestyModelId)
await updateGlobalState(context, "requestyModelInfo", requestyModelInfo)
await updateGlobalState(context, "togetherModelId", togetherModelId)
await updateGlobalState(context, "fireworksModelId", fireworksModelId)
await updateGlobalState(context, "sapAiCoreModelId", sapAiCoreModelId)
// Global state updates
await updateGlobalState(context, "awsRegion", awsRegion)
@@ -549,12 +539,9 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
await updateGlobalState(context, "requestTimeoutMs", apiConfiguration.requestTimeoutMs)
await updateGlobalState(context, "fireworksModelMaxCompletionTokens", fireworksModelMaxCompletionTokens)
await updateGlobalState(context, "fireworksModelMaxTokens", fireworksModelMaxTokens)
await updateGlobalState(context, "favoritedModelIds", favoritedModelIds)
await updateGlobalState(context, "requestTimeoutMs", apiConfiguration.requestTimeoutMs)
await updateGlobalState(context, "sapAiCoreBaseUrl", sapAiCoreBaseUrl)
await updateGlobalState(context, "sapAiCoreTokenUrl", sapAiCoreTokenUrl)
await updateGlobalState(context, "sapAiResourceGroup", sapAiResourceGroup)
await updateGlobalState(context, "sapAiCoreModelId", sapAiCoreModelId)
await updateGlobalState(context, "claudeCodePath", claudeCodePath)
// Secret updates
+80 -66
View File
@@ -1,16 +1,23 @@
import * as vscode from "vscode"
import { TaskState } from "./TaskState"
import { MessageStateHandler } from "./message-state"
import { showSystemNotification } from "@/integrations/notifications"
import { listFiles } from "@/services/glob/list-files"
import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
import { regexSearchFiles } from "@/services/ripgrep"
import { parseSourceCodeForDefinitionsTopLevel } from "@/services/tree-sitter"
import { findLast, findLastIndex, parsePartialArrayString } from "@/shared/array"
import { createAndOpenGitHubIssue } from "@/utils/github-url-utils"
import { getReadablePath, isLocatedInWorkspace } from "@/utils/path"
import Anthropic from "@anthropic-ai/sdk"
import { ApiHandler } from "@api/index"
import { TerminalManager } from "@integrations/terminal/TerminalManager"
import { BrowserSession } from "@services/browser/BrowserSession"
import { DiffViewProvider } from "@integrations/editor/DiffViewProvider"
import { McpHub } from "@services/mcp/McpHub"
import { FileContextTracker } from "@core/context/context-tracking/FileContextTracker"
import { ClineIgnoreController } from "@core/ignore/ClineIgnoreController"
import { DiffViewProvider } from "@integrations/editor/DiffViewProvider"
import { extractTextFromFile, processFilesIntoText } from "@integrations/misc/extract-text"
import WorkspaceTracker from "@integrations/workspace/WorkspaceTracker"
import { BrowserSession } from "@services/browser/BrowserSession"
import { UrlContentFetcher } from "@services/browser/UrlContentFetcher"
import { McpHub } from "@services/mcp/McpHub"
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
import { ChatSettings } from "@shared/ChatSettings"
import { ToolParamName, ToolUse, ToolUseName } from "../assistant-message"
import { BrowserSettings } from "@shared/BrowserSettings"
import {
BrowserAction,
BrowserActionResult,
@@ -24,46 +31,51 @@ import {
ClineSayTool,
COMPLETION_RESULT_CHANGES_FLAG,
} from "@shared/ExtensionMessage"
import WorkspaceTracker from "@integrations/workspace/WorkspaceTracker"
import { BrowserSettings } from "@shared/BrowserSettings"
import { ClineAskResponse } from "@shared/WebviewMessage"
import { fixModelHtmlEscaping, removeInvalidChars } from "@utils/string"
import { fileExistsAtPath } from "@utils/fs"
import { formatResponse } from "../prompts/responses"
import { isClaude4ModelFamily, isGemini2dot5ModelFamily } from "@utils/model-utils"
import { ToolResponse, USE_EXPERIMENTAL_CLAUDE4_FEATURES } from "."
import { serializeError } from "serialize-error"
import * as path from "path"
import { extractTextFromFile, processFilesIntoText } from "@integrations/misc/extract-text"
import { COMMAND_REQ_APP_STRING } from "@shared/combineCommandSequences"
import { UrlContentFetcher } from "@services/browser/UrlContentFetcher"
import { constructNewFileContent } from "../assistant-message/diff"
import { getReadablePath, isLocatedInWorkspace } from "@/utils/path"
import { listFiles } from "@/services/glob/list-files"
import { showNotificationForApprovalIfAutoApprovalEnabled } from "./utils"
import { parseSourceCodeForDefinitionsTopLevel } from "@/services/tree-sitter"
import { regexSearchFiles } from "@/services/ripgrep"
import { showSystemNotification } from "@/integrations/notifications"
import { findLast, findLastIndex, parsePartialArrayString } from "@/shared/array"
import { ensureTaskDirectoryExists } from "../storage/disk"
import { telemetryService } from "@/services/posthog/telemetry/TelemetryService"
import { loadMcpDocumentation } from "../prompts/loadMcpDocumentation"
import Anthropic from "@anthropic-ai/sdk"
import { createAndOpenGitHubIssue } from "@/utils/github-url-utils"
import { getWorkspaceState } from "../storage/state"
import os from "os"
import { ContextManager } from "../context/context-management/ContextManager"
import { fileExistsAtPath } from "@utils/fs"
import { isClaude4ModelFamily, isGemini2dot5ModelFamily } from "@utils/model-utils"
import { fixModelHtmlEscaping, removeInvalidChars } from "@utils/string"
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
import os from "os"
import * as path from "path"
import { serializeError } from "serialize-error"
import * as vscode from "vscode"
import { ToolResponse, USE_EXPERIMENTAL_CLAUDE4_FEATURES } from "."
import { ToolParamName, ToolUse, ToolUseName } from "../assistant-message"
import { constructNewFileContent } from "../assistant-message/diff"
import { ChangeLocation, StreamingJsonReplacer } from "../assistant-message/diff-json"
import { ContextManager } from "../context/context-management/ContextManager"
import { loadMcpDocumentation } from "../prompts/loadMcpDocumentation"
import { formatResponse } from "../prompts/responses"
import { ensureTaskDirectoryExists } from "../storage/disk"
import { getGlobalState, getWorkspaceState } from "../storage/state"
import { TaskState } from "./TaskState"
import { MessageStateHandler } from "./message-state"
import { AutoApprove } from "./tools/autoApprove"
import { showNotificationForApprovalIfAutoApprovalEnabled } from "./utils"
export class ToolExecutor {
private autoApprover: AutoApprove
// Auto-approval methods using the AutoApprove class
private shouldAutoApproveTool(toolName: ToolUseName): boolean | [boolean, boolean] {
return this.autoApprover.shouldAutoApproveTool(toolName)
}
private async shouldAutoApproveToolWithPath(
blockname: ToolUseName,
autoApproveActionpath: string | undefined,
): Promise<boolean> {
return this.autoApprover.shouldAutoApproveToolWithPath(blockname, autoApproveActionpath)
}
constructor(
// Core Services & Managers
private context: vscode.ExtensionContext,
private taskState: TaskState,
private messageStateHandler: MessageStateHandler,
private api: ApiHandler,
private terminalManager: TerminalManager,
private urlContentFetcher: UrlContentFetcher,
private browserSession: BrowserSession,
private diffViewProvider: DiffViewProvider,
@@ -76,7 +88,6 @@ export class ToolExecutor {
// Configuration & Settings
private autoApprovalSettings: AutoApprovalSettings,
private browserSettings: BrowserSettings,
private chatSettings: ChatSettings,
private cwd: string,
private taskId: string,
@@ -94,15 +105,20 @@ export class ToolExecutor {
partial?: boolean,
) => Promise<{ response: ClineAskResponse; text?: string; images?: string[]; files?: string[] }>,
private saveCheckpoint: (isAttemptCompletionMessage?: boolean) => Promise<void>,
private reinitExistingTaskFromId: (taskId: string) => Promise<void>,
private cancelTask: () => Promise<void>,
private shouldAutoApproveTool: (toolName: ToolUseName) => boolean | [boolean, boolean],
private shouldAutoApproveToolWithPath: (blockname: ToolUseName, autoApproveActionpath: string | undefined) => boolean,
private sayAndCreateMissingParamError: (toolName: ToolUseName, paramName: string, relPath?: string) => Promise<any>,
private removeLastPartialMessageIfExistsWithType: (type: "ask" | "say", askOrSay: ClineAsk | ClineSay) => Promise<void>,
private executeCommandTool: (command: string) => Promise<[boolean, any]>,
private doesLatestTaskCompletionHaveNewChanges: () => Promise<boolean>,
) {}
) {
this.autoApprover = new AutoApprove(autoApprovalSettings)
}
/**
* Updates the auto approval settings
*/
public updateAutoApprovalSettings(settings: AutoApprovalSettings): void {
this.autoApprover.updateSettings(settings)
}
private pushToolResult = (content: ToolResponse, block: ToolUse) => {
const isNextGenModel = isClaude4ModelFamily(this.api) || isGemini2dot5ModelFamily(this.api)
@@ -317,8 +333,6 @@ export class ToolExecutor {
// Handle write error
return { shouldBreak: true, error: `Write error: ${e}` }
}
const newContentParsed = this.taskState.streamingJsonReplacer.getSuccessfullyParsedItems()
}
return { shouldBreak: true } // Wait for more chunks
@@ -555,14 +569,14 @@ export class ToolExecutor {
tool: fileExists ? "editedExistingFile" : "newFileCreated",
path: getReadablePath(this.cwd, this.removeClosingTag(block, "path", relPath)),
content: diff || content,
operationIsLocatedInWorkspace: isLocatedInWorkspace(relPath),
operationIsLocatedInWorkspace: await isLocatedInWorkspace(relPath),
}
if (block.partial) {
// update gui message
const partialMessage = JSON.stringify(sharedMessageProps)
if (this.shouldAutoApproveToolWithPath(block.name, relPath)) {
if (await this.shouldAutoApproveToolWithPath(block.name, relPath)) {
this.removeLastPartialMessageIfExistsWithType("ask", "tool") // in case the user changes auto-approval settings mid stream
await this.say("tool", partialMessage, undefined, undefined, block.partial)
} else {
@@ -626,7 +640,7 @@ export class ToolExecutor {
const completeMessage = JSON.stringify({
...sharedMessageProps,
content: diff || content,
operationIsLocatedInWorkspace: isLocatedInWorkspace(relPath),
operationIsLocatedInWorkspace: await isLocatedInWorkspace(relPath),
// ? formatResponse.createPrettyPatch(
// relPath,
// this.diffViewProvider.originalContent,
@@ -634,7 +648,7 @@ export class ToolExecutor {
// )
// : undefined,
} satisfies ClineSayTool)
if (this.shouldAutoApproveToolWithPath(block.name, relPath)) {
if (await this.shouldAutoApproveToolWithPath(block.name, relPath)) {
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
await this.say("tool", completeMessage, undefined, undefined, false)
this.taskState.consecutiveAutoApprovedRequestsCount++
@@ -769,9 +783,9 @@ export class ToolExecutor {
const partialMessage = JSON.stringify({
...sharedMessageProps,
content: undefined,
operationIsLocatedInWorkspace: isLocatedInWorkspace(relPath),
operationIsLocatedInWorkspace: await isLocatedInWorkspace(relPath),
} satisfies ClineSayTool)
if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
if (await this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
await this.say("tool", partialMessage, undefined, undefined, block.partial)
} else {
@@ -800,9 +814,9 @@ export class ToolExecutor {
const completeMessage = JSON.stringify({
...sharedMessageProps,
content: absolutePath,
operationIsLocatedInWorkspace: isLocatedInWorkspace(relPath),
operationIsLocatedInWorkspace: await isLocatedInWorkspace(relPath),
} satisfies ClineSayTool)
if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
if (await this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
await this.say("tool", completeMessage, undefined, undefined, false) // need to be sending partialValue bool, since undefined has its own purpose in that the message is treated neither as a partial or completion of a partial, but as a single complete message
this.taskState.consecutiveAutoApprovedRequestsCount++
@@ -851,9 +865,9 @@ export class ToolExecutor {
const partialMessage = JSON.stringify({
...sharedMessageProps,
content: "",
operationIsLocatedInWorkspace: isLocatedInWorkspace(block.params.path),
operationIsLocatedInWorkspace: await isLocatedInWorkspace(block.params.path),
} satisfies ClineSayTool)
if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
if (await this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
await this.say("tool", partialMessage, undefined, undefined, block.partial)
} else {
@@ -883,9 +897,9 @@ export class ToolExecutor {
const completeMessage = JSON.stringify({
...sharedMessageProps,
content: result,
operationIsLocatedInWorkspace: isLocatedInWorkspace(block.params.path),
operationIsLocatedInWorkspace: await isLocatedInWorkspace(block.params.path),
} satisfies ClineSayTool)
if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
if (await this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
await this.say("tool", completeMessage, undefined, undefined, false)
this.taskState.consecutiveAutoApprovedRequestsCount++
@@ -926,9 +940,9 @@ export class ToolExecutor {
const partialMessage = JSON.stringify({
...sharedMessageProps,
content: "",
operationIsLocatedInWorkspace: isLocatedInWorkspace(block.params.path),
operationIsLocatedInWorkspace: await isLocatedInWorkspace(block.params.path),
} satisfies ClineSayTool)
if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
if (await this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
await this.say("tool", partialMessage, undefined, undefined, block.partial)
} else {
@@ -955,9 +969,9 @@ export class ToolExecutor {
const completeMessage = JSON.stringify({
...sharedMessageProps,
content: result,
operationIsLocatedInWorkspace: isLocatedInWorkspace(block.params.path),
operationIsLocatedInWorkspace: await isLocatedInWorkspace(block.params.path),
} satisfies ClineSayTool)
if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
if (await this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
await this.say("tool", completeMessage, undefined, undefined, false)
this.taskState.consecutiveAutoApprovedRequestsCount++
@@ -1002,9 +1016,9 @@ export class ToolExecutor {
const partialMessage = JSON.stringify({
...sharedMessageProps,
content: "",
operationIsLocatedInWorkspace: isLocatedInWorkspace(block.params.path),
operationIsLocatedInWorkspace: await isLocatedInWorkspace(block.params.path),
} satisfies ClineSayTool)
if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
if (await this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
await this.say("tool", partialMessage, undefined, undefined, block.partial)
} else {
@@ -1039,9 +1053,9 @@ export class ToolExecutor {
const completeMessage = JSON.stringify({
...sharedMessageProps,
content: results,
operationIsLocatedInWorkspace: isLocatedInWorkspace(block.params.path),
operationIsLocatedInWorkspace: await isLocatedInWorkspace(block.params.path),
} satisfies ClineSayTool)
if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
if (await this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
await this.say("tool", completeMessage, undefined, undefined, false)
this.taskState.consecutiveAutoApprovedRequestsCount++
@@ -1903,7 +1917,7 @@ export class ToolExecutor {
const clineVersion =
vscode.extensions.getExtension("saoudrizwan.claude-dev")?.packageJSON.version || "Unknown"
const systemInfo = `VSCode: ${vscode.version}, Node.js: ${process.version}, Architecture: ${os.arch()}`
const providerAndModel = `${(await getWorkspaceState(this.context, "apiProvider")) as string} / ${this.api.getModel().id}`
const providerAndModel = `${await getGlobalState(this.context, "apiProvider")} / ${this.api.getModel().id}`
// Ask user for confirmation
const bugReportData = JSON.stringify({
+109 -148
View File
@@ -1,13 +1,4 @@
import { Anthropic } from "@anthropic-ai/sdk"
import cloneDeep from "clone-deep"
import { execa } from "execa"
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
import os from "os"
import pTimeout from "p-timeout"
import pWaitFor from "p-wait-for"
import * as path from "path"
import * as vscode from "vscode"
import { Logger } from "@services/logging/Logger"
import { ApiHandler, buildApiHandler } from "@api/index"
import { AnthropicHandler } from "@api/providers/anthropic"
import { ClineHandler } from "@api/providers/cline"
@@ -21,6 +12,7 @@ import { TerminalManager } from "@integrations/terminal/TerminalManager"
import { BrowserSession } from "@services/browser/BrowserSession"
import { UrlContentFetcher } from "@services/browser/UrlContentFetcher"
import { listFiles } from "@services/glob/list-files"
import { Logger } from "@services/logging/Logger"
import { telemetryService } from "@services/posthog/telemetry/TelemetryService"
import { ApiConfiguration } from "@shared/api"
import { findLast, findLastIndex } from "@shared/array"
@@ -29,42 +21,47 @@ import { BrowserSettings } from "@shared/BrowserSettings"
import { ChatSettings } from "@shared/ChatSettings"
import { combineApiRequests } from "@shared/combineApiRequests"
import { combineCommandSequences } from "@shared/combineCommandSequences"
import {
ClineApiReqCancelReason,
ClineApiReqInfo,
ClineAsk,
ClineMessage,
ClineSay,
ExtensionMessage,
} from "@shared/ExtensionMessage"
import { ClineApiReqCancelReason, ClineApiReqInfo, ClineAsk, ClineMessage, ClineSay } from "@shared/ExtensionMessage"
import { getApiMetrics } from "@shared/getApiMetrics"
import { HistoryItem } from "@shared/HistoryItem"
import { DEFAULT_LANGUAGE_SETTINGS, getLanguageKey, LanguageDisplay } from "@shared/Languages"
import { ClineAskResponse, ClineCheckpointRestore } from "@shared/WebviewMessage"
import { arePathsEqual } from "@utils/path"
import { getGitRemoteUrls } from "@utils/git"
import { arePathsEqual, getDesktopDir } from "@utils/path"
import cloneDeep from "clone-deep"
import { execa } from "execa"
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
import pTimeout from "p-timeout"
import pWaitFor from "p-wait-for"
import * as path from "path"
import * as vscode from "vscode"
import {
AssistantMessageContent,
parseAssistantMessageV2,
parseAssistantMessageV3,
ToolParamName,
ToolUseName,
} from "@core/assistant-message"
import { ClineIgnoreController } from "@core/ignore/ClineIgnoreController"
import { parseMentions } from "@core/mentions"
import { formatResponse } from "@core/prompts/responses"
import { addUserInstructions, SYSTEM_PROMPT } from "@core/prompts/system"
import { sendPartialMessageEvent } from "@core/controller/ui/subscribeToPartialMessage"
import { sendRelinquishControlEvent } from "@core/controller/ui/subscribeToRelinquishControl"
import { convertClineMessageToProto } from "@shared/proto-conversions/cline-message"
import { getContextWindowInfo } from "@core/context/context-management/context-window-utils"
import { FileContextTracker } from "@core/context/context-tracking/FileContextTracker"
import { ModelContextTracker } from "@core/context/context-tracking/ModelContextTracker"
import { parseAssistantMessageV2, parseAssistantMessageV3, ToolUseName } from "@core/assistant-message"
import {
checkIsAnthropicContextWindowError,
checkIsOpenRouterContextWindowError,
} from "@core/context/context-management/context-error-handling"
import { getContextWindowInfo } from "@core/context/context-management/context-window-utils"
import { ContextManager } from "@core/context/context-management/ContextManager"
import { FileContextTracker } from "@core/context/context-tracking/FileContextTracker"
import { ModelContextTracker } from "@core/context/context-tracking/ModelContextTracker"
import {
getGlobalClineRules,
getLocalClineRules,
refreshClineRulesToggles,
} from "@core/context/instructions/user-instructions/cline-rules"
import {
getLocalCursorRules,
getLocalWindsurfRules,
refreshExternalRulesToggles,
} from "@core/context/instructions/user-instructions/external-rules"
import { sendPartialMessageEvent } from "@core/controller/ui/subscribeToPartialMessage"
import { sendRelinquishControlEvent } from "@core/controller/ui/subscribeToRelinquishControl"
import { ClineIgnoreController } from "@core/ignore/ClineIgnoreController"
import { parseMentions } from "@core/mentions"
import { formatResponse } from "@core/prompts/responses"
import { addUserInstructions, SYSTEM_PROMPT } from "@core/prompts/system"
import { parseSlashCommands } from "@core/slash-commands"
import {
ensureRulesDirectoryExists,
ensureTaskDirectoryExists,
@@ -72,35 +69,22 @@ import {
getSavedClineMessages,
GlobalFileNames,
} from "@core/storage/disk"
import {
getGlobalClineRules,
getLocalClineRules,
refreshClineRulesToggles,
} from "@core/context/instructions/user-instructions/cline-rules"
import { ensureLocalClineDirExists } from "../context/instructions/user-instructions/rule-helpers"
import {
refreshExternalRulesToggles,
getLocalWindsurfRules,
getLocalCursorRules,
} from "@core/context/instructions/user-instructions/external-rules"
import { refreshWorkflowToggles } from "../context/instructions/user-instructions/workflows"
import { getWorkspaceState } from "@core/storage/state"
import { parseSlashCommands } from "@core/slash-commands"
import { getGlobalState } from "@core/storage/state"
import { processFilesIntoText } from "@integrations/misc/extract-text"
import WorkspaceTracker from "@integrations/workspace/WorkspaceTracker"
import { McpHub } from "@services/mcp/McpHub"
import { isInTestMode } from "../../services/test/TestMode"
import { processFilesIntoText } from "@integrations/misc/extract-text"
import { convertClineMessageToProto } from "@shared/proto-conversions/cline-message"
import { isClaude4ModelFamily, isGemini2dot5ModelFamily } from "@utils/model-utils"
import { isInTestMode } from "../../services/test/TestMode"
import { ensureLocalClineDirExists } from "../context/instructions/user-instructions/rule-helpers"
import { refreshWorkflowToggles } from "../context/instructions/user-instructions/workflows"
import { MessageStateHandler } from "./message-state"
import { formatErrorWithStatusCode, updateApiReqMsg } from "./utils"
import { TaskState } from "./TaskState"
import { ToolExecutor } from "./ToolExecutor"
import { formatErrorWithStatusCode, updateApiReqMsg } from "./utils"
export const USE_EXPERIMENTAL_CLAUDE4_FEATURES = false
export const cwd =
vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution
export type ToolResponse = string | Array<Anthropic.TextBlockParam | Anthropic.ImageBlockParam>
type UserContent = Array<Anthropic.ContentBlockParam>
@@ -108,6 +92,7 @@ export class Task {
// Core task variables
readonly taskId: string
private taskIsFavorited?: boolean
private cwd: string
taskState: TaskState
@@ -137,7 +122,6 @@ export class Task {
// Callbacks
private updateTaskHistory: (historyItem: HistoryItem) => Promise<HistoryItem[]>
private postStateToWebview: () => Promise<void>
private postMessageToWebview: (message: ExtensionMessage) => Promise<void>
private reinitExistingTaskFromId: (taskId: string) => Promise<void>
private cancelTask: () => Promise<void>
@@ -154,7 +138,6 @@ export class Task {
workspaceTracker: WorkspaceTracker,
updateTaskHistory: (historyItem: HistoryItem) => Promise<HistoryItem[]>,
postStateToWebview: () => Promise<void>,
postMessageToWebview: (message: ExtensionMessage) => Promise<void>,
reinitExistingTaskFromId: (taskId: string) => Promise<void>,
cancelTask: () => Promise<void>,
apiConfiguration: ApiConfiguration,
@@ -166,6 +149,7 @@ export class Task {
terminalOutputLineLimit: number,
defaultTerminalProfile: string,
enableCheckpointsSetting: boolean,
cwd: string,
task?: string,
images?: string[],
files?: string[],
@@ -177,7 +161,6 @@ export class Task {
this.workspaceTracker = workspaceTracker
this.updateTaskHistory = updateTaskHistory
this.postStateToWebview = postStateToWebview
this.postMessageToWebview = postMessageToWebview
this.reinitExistingTaskFromId = reinitExistingTaskFromId
this.cancelTask = cancelTask
this.clineIgnoreController = new ClineIgnoreController(cwd)
@@ -196,6 +179,7 @@ export class Task {
this.browserSettings = browserSettings
this.chatSettings = chatSettings
this.enableCheckpoints = enableCheckpointsSetting
this.cwd = cwd
// Set up MCP notification callback for real-time notifications
this.mcpHub.setNotificationCallback(async (serverName: string, level: string, message: string) => {
@@ -295,7 +279,6 @@ export class Task {
this.taskState,
this.messageStateHandler,
this.api,
this.terminalManager,
this.urlContentFetcher,
this.browserSession,
this.diffViewProvider,
@@ -306,16 +289,11 @@ export class Task {
this.contextManager,
this.autoApprovalSettings,
this.browserSettings,
this.chatSettings,
cwd,
this.taskId,
this.say.bind(this),
this.ask.bind(this),
this.saveCheckpoint.bind(this),
this.reinitExistingTaskFromId.bind(this),
this.cancelTask.bind(this),
this.shouldAutoApproveTool.bind(this),
this.shouldAutoApproveToolWithPath.bind(this),
this.sayAndCreateMissingParamError.bind(this),
this.removeLastPartialMessageIfExistsWithType.bind(this),
this.executeCommandTool.bind(this),
@@ -333,6 +311,13 @@ export class Task {
return context
}
/**
* Updates the auto approval settings for this task
*/
public updateAutoApprovalSettings(settings: AutoApprovalSettings): void {
this.toolExecutor.updateAutoApprovalSettings(settings)
}
async restoreCheckpoint(messageTs: number, restoreType: ClineCheckpointRestore, offset?: number) {
const clineMessages = this.messageStateHandler.getClineMessages()
const messageIndex = clineMessages.findIndex((m) => m.ts === messageTs) - (offset || 0)
@@ -1111,7 +1096,7 @@ export class Task {
const [taskResumptionMessage, userResponseMessage] = formatResponse.taskResumption(
this.chatSettings?.mode === "plan" ? "plan" : "act",
agoText,
cwd,
this.cwd,
wasRecent,
responseText,
hasPendingFileContextWarnings,
@@ -1342,7 +1327,7 @@ export class Task {
// Create a child process
const childProcess = execa(command, {
shell: true,
cwd,
cwd: this.cwd,
reject: false,
all: true, // Merge stdout and stderr
})
@@ -1419,7 +1404,7 @@ export class Task {
}
Logger.info("Executing command in VS code terminal: " + command)
const terminalInfo = await this.terminalManager.getOrCreateTerminal(cwd)
const terminalInfo = await this.terminalManager.getOrCreateTerminal(this.cwd)
terminalInfo.terminal.show() // weird visual bug when creating new terminals (even manually) where there's an empty space at the top.
const process = this.terminalManager.runCommand(terminalInfo, command)
@@ -1451,7 +1436,10 @@ export class Task {
try {
const { response, text, images, files } = await this.ask("command_output", chunk)
if (response === "yesButtonClicked") {
// proceed while running
// proceed while running - but still capture user feedback if provided
if (text || (images && images.length > 0) || (files && files.length > 0)) {
userFeedback = { text, images, files }
}
} else {
userFeedback = { text, images, files }
}
@@ -1554,69 +1542,6 @@ export class Task {
}
}
// Check if the tool should be auto-approved based on the settings
// Returns bool for most tools, and tuple for tools with nested settings
shouldAutoApproveTool(toolName: ToolUseName): boolean | [boolean, boolean] {
if (this.autoApprovalSettings.enabled) {
switch (toolName) {
case "read_file":
case "list_files":
case "list_code_definition_names":
case "search_files":
return [
this.autoApprovalSettings.actions.readFiles,
this.autoApprovalSettings.actions.readFilesExternally ?? false,
]
case "new_rule":
case "write_to_file":
case "replace_in_file":
return [
this.autoApprovalSettings.actions.editFiles,
this.autoApprovalSettings.actions.editFilesExternally ?? false,
]
case "execute_command":
return [
this.autoApprovalSettings.actions.executeSafeCommands ?? false,
this.autoApprovalSettings.actions.executeAllCommands ?? false,
]
case "browser_action":
return this.autoApprovalSettings.actions.useBrowser
case "web_fetch":
return this.autoApprovalSettings.actions.useBrowser
case "access_mcp_resource":
case "use_mcp_tool":
return this.autoApprovalSettings.actions.useMcp
}
}
return false
}
// Check if the tool should be auto-approved based on the settings
// and the path of the action. Returns true if the tool should be auto-approved
// based on the user's settings and the path of the action.
shouldAutoApproveToolWithPath(blockname: ToolUseName, autoApproveActionpath: string | undefined): boolean {
let isLocalRead: boolean = false
if (autoApproveActionpath) {
const absolutePath = path.resolve(cwd, autoApproveActionpath)
isLocalRead = absolutePath.startsWith(cwd)
} else {
// If we do not get a path for some reason, default to a (safer) false return
isLocalRead = false
}
// Get auto-approve settings for local and external edits
const autoApproveResult = this.shouldAutoApproveTool(blockname)
const [autoApproveLocal, autoApproveExternal] = Array.isArray(autoApproveResult)
? autoApproveResult
: [autoApproveResult, false]
if ((isLocalRead && autoApproveLocal) || (!isLocalRead && autoApproveLocal && autoApproveExternal)) {
return true
} else {
return false
}
}
/**
* Migrates the disableBrowserTool setting from VSCode configuration to browserSettings
*/
@@ -1655,7 +1580,7 @@ export class Task {
const supportsBrowserUse = modelSupportsBrowserUse && !disableBrowserTool // only enable browser use if the model supports it and the user hasn't disabled it
const isNextGenModel = isClaude4ModelFamily(this.api) || isGemini2dot5ModelFamily(this.api)
let systemPrompt = await SYSTEM_PROMPT(cwd, supportsBrowserUse, this.mcpHub, this.browserSettings, isNextGenModel)
let systemPrompt = await SYSTEM_PROMPT(this.cwd, supportsBrowserUse, this.mcpHub, this.browserSettings, isNextGenModel)
await this.migratePreferredLanguageToolSetting()
const preferredLanguage = getLanguageKey(this.chatSettings.preferredLanguage as LanguageDisplay)
@@ -1664,18 +1589,18 @@ export class Task {
? `# Preferred Language\n\nSpeak in ${preferredLanguage}.`
: ""
const { globalToggles, localToggles } = await refreshClineRulesToggles(this.getContext(), cwd)
const { windsurfLocalToggles, cursorLocalToggles } = await refreshExternalRulesToggles(this.getContext(), cwd)
const { globalToggles, localToggles } = await refreshClineRulesToggles(this.getContext(), this.cwd)
const { windsurfLocalToggles, cursorLocalToggles } = await refreshExternalRulesToggles(this.getContext(), this.cwd)
const globalClineRulesFilePath = await ensureRulesDirectoryExists()
const globalClineRulesFileInstructions = await getGlobalClineRules(globalClineRulesFilePath, globalToggles)
const localClineRulesFileInstructions = await getLocalClineRules(cwd, localToggles)
const localClineRulesFileInstructions = await getLocalClineRules(this.cwd, localToggles)
const [localCursorRulesFileInstructions, localCursorRulesDirInstructions] = await getLocalCursorRules(
cwd,
this.cwd,
cursorLocalToggles,
)
const localWindsurfRulesFileInstructions = await getLocalWindsurfRules(cwd, windsurfLocalToggles)
const localWindsurfRulesFileInstructions = await getLocalWindsurfRules(this.cwd, windsurfLocalToggles)
const clineIgnoreContent = this.clineIgnoreController.clineIgnoreContent
let clineIgnoreInstructions: string | undefined
@@ -1947,7 +1872,7 @@ export class Task {
}
// Used to know what models were used in the task if user wants to export metadata for error reporting purposes
const currentProviderId = (await getWorkspaceState(this.getContext(), "apiProvider")) as string
const currentProviderId = (await getGlobalState(this.getContext(), "apiProvider")) as string
if (currentProviderId && this.api.getModel().id) {
try {
await this.modelContextTracker.recordModelUsage(currentProviderId, this.api.getModel().id, this.chatSettings.mode)
@@ -2005,12 +1930,42 @@ export class Task {
message: `Cline has auto-approved ${this.autoApprovalSettings.maxRequests.toString()} API requests.`,
})
}
await this.ask(
const { response, text, images, files } = await this.ask(
"auto_approval_max_req_reached",
`Cline has auto-approved ${this.autoApprovalSettings.maxRequests.toString()} API requests. Would you like to reset the count and proceed with the task?`,
)
// if we get past the promise it means the user approved and did not start a new task
this.taskState.consecutiveAutoApprovedRequestsCount = 0
// Process user feedback if provided
if (response === "messageResponse") {
// Display the user's message in the chat UI
await this.say("user_feedback", text, images, files)
// This userContent is for the *next* API call.
const feedbackUserContent: UserContent = []
feedbackUserContent.push({
type: "text",
text: formatResponse.autoApprovalMaxReached(text),
})
if (images && images.length > 0) {
feedbackUserContent.push(...formatResponse.imageBlocks(images))
}
let fileContentString = ""
if (files && files.length > 0) {
fileContentString = await processFilesIntoText(files)
}
if (fileContentString) {
feedbackUserContent.push({
type: "text",
text: fileContentString,
})
}
userContent = feedbackUserContent
}
}
// get previous api req's index to check token usage and determine if we need to truncate conversation history
@@ -2401,7 +2356,7 @@ export class Task {
// Track if we need to check clinerulesFile
let needsClinerulesFileCheck = false
const { localWorkflowToggles, globalWorkflowToggles } = await refreshWorkflowToggles(this.getContext(), cwd)
const { localWorkflowToggles, globalWorkflowToggles } = await refreshWorkflowToggles(this.getContext(), this.cwd)
const processUserContent = async () => {
// This is a temporary solution to dynamically load context mentions from tool results. It checks for the presence of tags that indicate that the tool was rejected and feedback was provided (see formatToolDeniedFeedback, attemptCompletion, executeCommand, and consecutiveMistakeCount >= 3) or "<answer>" (see askFollowupQuestion), we place all user generated content in these tags so they can effectively be used as markers for when we should parse mentions). However if we allow multiple tools responses in the future, we will need to parse mentions specifically within the user content tags.
@@ -2419,7 +2374,7 @@ export class Task {
) {
const parsedText = await parseMentions(
block.text,
cwd,
this.cwd,
this.urlContentFetcher,
this.fileContextTracker,
)
@@ -2455,7 +2410,7 @@ export class Task {
// After processing content, check clinerulesData if needed
let clinerulesError = false
if (needsClinerulesFileCheck) {
clinerulesError = await ensureLocalClineDirExists(cwd, GlobalFileNames.clineRules)
clinerulesError = await ensureLocalClineDirExists(this.cwd, GlobalFileNames.clineRules)
}
// Return all results
@@ -2470,7 +2425,7 @@ export class Task {
const visibleFilePaths = vscode.window.visibleTextEditors
?.map((editor) => editor.document?.uri?.fsPath)
.filter(Boolean)
.map((absolutePath) => path.relative(cwd, absolutePath))
.map((absolutePath) => path.relative(this.cwd, absolutePath))
// Filter paths through clineIgnoreController
const allowedVisibleFiles = this.clineIgnoreController
@@ -2489,7 +2444,7 @@ export class Task {
.flatMap((group) => group.tabs)
.map((tab) => (tab.input as vscode.TabInputText)?.uri?.fsPath)
.filter(Boolean)
.map((absolutePath) => path.relative(cwd, absolutePath))
.map((absolutePath) => path.relative(this.cwd, absolutePath))
// Filter paths through clineIgnoreController
const allowedOpenTabs = this.clineIgnoreController
@@ -2615,16 +2570,22 @@ export class Task {
details += `\n\n# Current Time\n${formatter.format(now)} (${timeZone}, UTC${timeZoneOffsetStr})`
if (includeFileDetails) {
details += `\n\n# Current Working Directory (${cwd.toPosix()}) Files\n`
const isDesktop = arePathsEqual(cwd, path.join(os.homedir(), "Desktop"))
details += `\n\n# Current Working Directory (${this.cwd.toPosix()}) Files\n`
const isDesktop = arePathsEqual(this.cwd, getDesktopDir())
if (isDesktop) {
// don't want to immediately access desktop since it would show permission popup
details += "(Desktop files not shown automatically. Use list_files to explore if needed.)"
} else {
const [files, didHitLimit] = await listFiles(cwd, true, 200)
const result = formatResponse.formatFilesList(cwd, files, didHitLimit, this.clineIgnoreController)
const [files, didHitLimit] = await listFiles(this.cwd, true, 200)
const result = formatResponse.formatFilesList(this.cwd, files, didHitLimit, this.clineIgnoreController)
details += result
}
// Add git remote URLs section
const gitRemotes = await getGitRemoteUrls(this.cwd)
if (gitRemotes.length > 0) {
details += `\n\n# Git Remote URLs\n${gitRemotes.join("\n")}`
}
}
// Add context window usage information
+2 -2
View File
@@ -12,6 +12,7 @@ import CheckpointTracker from "@integrations/checkpoints/CheckpointTracker"
import { HistoryItem } from "@/shared/HistoryItem"
import Anthropic from "@anthropic-ai/sdk"
import { TaskState } from "./TaskState"
import { getCwd, getDesktopDir } from "@/utils/path"
interface MessageStateHandlerParams {
context: vscode.ExtensionContext
@@ -21,8 +22,6 @@ interface MessageStateHandlerParams {
taskState: TaskState
}
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution
export class MessageStateHandler {
private apiConversationHistory: Anthropic.MessageParam[] = []
private clineMessages: ClineMessage[] = []
@@ -84,6 +83,7 @@ export class MessageStateHandler {
} catch (error) {
console.error("Failed to get task directory size:", taskDir, error)
}
const cwd = await getCwd(getDesktopDir())
await this.updateTaskHistory({
id: this.taskId,
ts: lastRelevantMessage.ts,
+81
View File
@@ -0,0 +1,81 @@
import { AutoApprovalSettings } from "@shared/AutoApprovalSettings"
import { ToolUseName } from "@core/assistant-message"
import * as path from "path"
import os from "os"
import { getCwd, getDesktopDir } from "@/utils/path"
export class AutoApprove {
autoApprovalSettings: AutoApprovalSettings
constructor(autoApprovalSettings: AutoApprovalSettings) {
this.autoApprovalSettings = autoApprovalSettings
}
// Check if the tool should be auto-approved based on the settings
// Returns bool for most tools, and tuple for tools with nested settings
shouldAutoApproveTool(toolName: ToolUseName): boolean | [boolean, boolean] {
if (this.autoApprovalSettings.enabled) {
switch (toolName) {
case "read_file":
case "list_files":
case "list_code_definition_names":
case "search_files":
return [
this.autoApprovalSettings.actions.readFiles,
this.autoApprovalSettings.actions.readFilesExternally ?? false,
]
case "new_rule":
case "write_to_file":
case "replace_in_file":
return [
this.autoApprovalSettings.actions.editFiles,
this.autoApprovalSettings.actions.editFilesExternally ?? false,
]
case "execute_command":
return [
this.autoApprovalSettings.actions.executeSafeCommands ?? false,
this.autoApprovalSettings.actions.executeAllCommands ?? false,
]
case "browser_action":
return this.autoApprovalSettings.actions.useBrowser
case "web_fetch":
return this.autoApprovalSettings.actions.useBrowser
case "access_mcp_resource":
case "use_mcp_tool":
return this.autoApprovalSettings.actions.useMcp
}
}
return false
}
// Check if the tool should be auto-approved based on the settings
// and the path of the action. Returns true if the tool should be auto-approved
// based on the user's settings and the path of the action.
async shouldAutoApproveToolWithPath(blockname: ToolUseName, autoApproveActionpath: string | undefined): Promise<boolean> {
let isLocalRead: boolean = false
if (autoApproveActionpath) {
const cwd = await getCwd(getDesktopDir())
const absolutePath = path.resolve(cwd, autoApproveActionpath)
isLocalRead = absolutePath.startsWith(cwd)
} else {
// If we do not get a path for some reason, default to a (safer) false return
isLocalRead = false
}
// Get auto-approve settings for local and external edits
const autoApproveResult = this.shouldAutoApproveTool(blockname)
const [autoApproveLocal, autoApproveExternal] = Array.isArray(autoApproveResult)
? autoApproveResult
: [autoApproveResult, false]
if ((isLocalRead && autoApproveLocal) || (!isLocalRead && autoApproveLocal && autoApproveExternal)) {
return true
} else {
return false
}
}
updateSettings(settings: AutoApprovalSettings): void {
this.autoApprovalSettings = settings
}
}
+1 -8
View File
@@ -165,8 +165,6 @@ export abstract class WebviewProvider {
// don't forget to add font-src ${webview.cspSource};
const codiconsUri = this.getExtensionUri("node_modules", "@vscode", "codicons", "dist", "codicon.css")
const katexCssUri = this.getExtensionUri("webview-ui", "node_modules", "katex", "dist", "katex.min.css")
// const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "main.js"))
// const styleResetUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "reset.css"))
@@ -198,7 +196,6 @@ export abstract class WebviewProvider {
<meta name="theme-color" content="#000000">
<link rel="stylesheet" type="text/css" href="${stylesUri}">
<link href="${codiconsUri}" rel="stylesheet" />
<link href="${katexCssUri}" rel="stylesheet" />
<meta http-equiv="Content-Security-Policy" content="default-src 'none';
connect-src https://*.posthog.com https://*.cline.bot https://*.firebaseauth.com https://*.firebaseio.com https://*.googleapis.com https://*.firebase.com;
font-src ${this.getCspSource()} data:;
@@ -274,9 +271,6 @@ export abstract class WebviewProvider {
const stylesUri = this.getExtensionUri("webview-ui", "build", "assets", "index.css")
const codiconsUri = this.getExtensionUri("node_modules", "@vscode", "codicons", "dist", "codicon.css")
// Get KaTeX resources
const katexCssUri = this.getExtensionUri("webview-ui", "node_modules", "katex", "dist", "katex.min.css")
const scriptEntrypoint = "src/main.tsx"
const scriptUri = `http://${localServerUrl}/${scriptEntrypoint}`
@@ -292,7 +286,7 @@ export abstract class WebviewProvider {
const csp = [
"default-src 'none'",
`font-src ${this.getCspSource()} data:`,
`font-src ${this.getCspSource()}`,
`style-src ${this.getCspSource()} 'unsafe-inline' https://* http://${localServerUrl} http://0.0.0.0:${localPort}`,
`img-src ${this.getCspSource()} https: data:`,
`script-src 'unsafe-eval' https://* http://${localServerUrl} http://0.0.0.0:${localPort} 'nonce-${nonce}'`,
@@ -309,7 +303,6 @@ export abstract class WebviewProvider {
<meta http-equiv="Content-Security-Policy" content="${csp.join("; ")}">
<link rel="stylesheet" type="text/css" href="${stylesUri}">
<link href="${codiconsUri}" rel="stylesheet" />
<link href="${katexCssUri}" rel="stylesheet" />
<title>Cline</title>
</head>
<body>
+23 -9
View File
@@ -23,9 +23,10 @@ import { WebviewProviderType } from "./shared/webview/types"
import { sendHistoryButtonClickedEvent } from "./core/controller/ui/subscribeToHistoryButtonClicked"
import { sendAccountButtonClickedEvent } from "./core/controller/ui/subscribeToAccountButtonClicked"
import {
migratePlanActGlobalToWorkspaceStorage,
migrateWorkspaceToGlobalStorage,
migrateCustomInstructionsToGlobalRules,
migrateModeFromWorkspaceStorageToControllerState,
migrateWelcomeViewCompleted,
} from "./core/storage/state-migrations"
import { sendFocusChatInputEvent } from "./core/controller/ui/subscribeToFocusChatInput"
@@ -34,6 +35,7 @@ import * as hostProviders from "@hosts/host-providers"
import { vscodeHostBridgeClient } from "@/hosts/vscode/client/host-grpc-client"
import { VscodeWebviewProvider } from "./core/webview/VscodeWebviewProvider"
import { ExtensionContext } from "vscode"
import { writeTextToClipboard, readTextFromClipboard } from "@/utils/env"
/*
Built using https://github.com/microsoft/vscode-webview-ui-toolkit
@@ -58,15 +60,18 @@ export async function activate(context: vscode.ExtensionContext) {
maybeSetupHostProviders(context)
// Migrate global storage values to workspace storage (one-time cleanup)
await migratePlanActGlobalToWorkspaceStorage(context)
// Migrate custom instructions to global Cline rules (one-time cleanup)
await migrateCustomInstructionsToGlobalRules(context)
// Migrate mode from workspace storage to controller state (one-time cleanup)
await migrateModeFromWorkspaceStorageToControllerState(context)
// Migrate welcomeViewCompleted setting based on existing API keys (one-time cleanup)
await migrateWelcomeViewCompleted(context)
// Migrate workspace storage values back to global storage (reverting previous migration)
await migrateWorkspaceToGlobalStorage(context)
// Clean up orphaned file context warnings (startup cleanup)
await FileContextTracker.cleanupOrphanedWarnings(context)
@@ -75,8 +80,9 @@ export async function activate(context: vscode.ExtensionContext) {
const previousVersion = context.globalState.get<string>("clineVersion")
const sidebarWebview = hostProviders.createWebviewProvider(WebviewProviderType.SIDEBAR)
const testModeWatchers = await initializeTestMode(sidebarWebview)
// Initialize test mode and add disposables to context
context.subscriptions.push(...initializeTestMode(context, sidebarWebview))
context.subscriptions.push(...testModeWatchers)
vscode.commands.executeCommand("setContext", "cline.isDevMode", IS_DEV && IS_DEV === "true")
@@ -369,17 +375,17 @@ export async function activate(context: vscode.ExtensionContext) {
}
// Save current clipboard content
const tempCopyBuffer = await vscode.env.clipboard.readText()
const tempCopyBuffer = await readTextFromClipboard()
try {
// Copy the *existing* terminal selection (without selecting all)
await vscode.commands.executeCommand("workbench.action.terminal.copySelection")
// Get copied content
let terminalContents = (await vscode.env.clipboard.readText()).trim()
let terminalContents = (await readTextFromClipboard()).trim()
// Restore original clipboard content
await vscode.env.clipboard.writeText(tempCopyBuffer)
await writeTextToClipboard(tempCopyBuffer)
if (!terminalContents) {
// No terminal content was copied (either nothing selected or some error)
@@ -405,7 +411,7 @@ export async function activate(context: vscode.ExtensionContext) {
await visibleWebview?.controller.addSelectedTerminalOutputToChat(terminalContents, terminal.name)
} catch (error) {
// Ensure clipboard is restored even if an error occurs
await vscode.env.clipboard.writeText(tempCopyBuffer)
await writeTextToClipboard(tempCopyBuffer)
console.error("Error getting terminal contents:", error)
vscode.window.showErrorMessage("Failed to get terminal contents")
}
@@ -638,6 +644,14 @@ export async function activate(context: vscode.ExtensionContext) {
}),
)
// Register the openWalkthrough command handler
context.subscriptions.push(
vscode.commands.registerCommand("cline.openWalkthrough", async () => {
await vscode.commands.executeCommand("workbench.action.openWalkthrough", "saoudrizwan.claude-dev#ClineWalkthrough")
telemetryService.captureButtonClick("command_openWalkthrough", undefined, true)
}),
)
// Register the generateGitCommitMessage command handler
context.subscriptions.push(
vscode.commands.registerCommand("cline.generateGitCommitMessage", async () => {
+4
View File
@@ -2,6 +2,8 @@ import {
UriServiceClientInterface,
WatchServiceClientInterface,
WorkspaceServiceClientInterface,
EnvServiceClientInterface,
WindowServiceClientInterface,
} from "@generated/hosts/host-bridge-client-types"
/**
@@ -11,6 +13,8 @@ export interface HostBridgeClientProvider {
uriServiceClient: UriServiceClientInterface
watchServiceClient: WatchServiceClientInterface
workspaceClient: WorkspaceServiceClientInterface
envClient: EnvServiceClientInterface
windowClient: WindowServiceClientInterface
}
/**
@@ -6,4 +6,6 @@ export const vscodeHostBridgeClient: HostBridgeClientProvider = {
uriServiceClient: createGrpcClient(host.UriServiceDefinition),
watchServiceClient: createGrpcClient(host.WatchServiceDefinition),
workspaceClient: createGrpcClient(host.WorkspaceServiceDefinition),
envClient: createGrpcClient(host.EnvServiceDefinition),
windowClient: createGrpcClient(host.WindowServiceDefinition),
}
+7
View File
@@ -0,0 +1,7 @@
import { EmptyRequest, String } from "@/shared/proto/common"
import * as vscode from "vscode"
export async function clipboardReadText(_: EmptyRequest): Promise<String> {
const text = await vscode.env.clipboard.readText()
return String.create({ value: text })
}
+7
View File
@@ -0,0 +1,7 @@
import { StringRequest, Empty } from "@/shared/proto/common"
import * as vscode from "vscode"
export async function clipboardWriteText(request: StringRequest): Promise<Empty> {
await vscode.env.clipboard.writeText(request.value)
return Empty.create({})
}
@@ -0,0 +1,27 @@
import * as vscode from "vscode"
import { ShowOpenDialogueRequest, SelectedResources } from "@/shared/proto/host/window"
export async function showOpenDialogue(request: ShowOpenDialogueRequest): Promise<SelectedResources> {
const options: vscode.OpenDialogOptions = {}
if (request.canSelectMany !== undefined) {
options.canSelectMany = request.canSelectMany
}
if (request.openLabel !== undefined) {
options.openLabel = request.openLabel
}
if (request.filters?.files) {
options.filters = {
Files: request.filters.files,
}
}
const selectedResources = await vscode.window.showOpenDialog(options)
// Convert back to path format
return SelectedResources.create({
paths: selectedResources ? selectedResources.map((uri) => uri.fsPath) : [],
})
}
@@ -0,0 +1,26 @@
import * as vscode from "vscode"
import { ShowTextDocumentRequest, TextEditorInfo } from "@/shared/proto/host/window"
export async function showTextDocument(request: ShowTextDocumentRequest): Promise<TextEditorInfo> {
// Convert file path to URI
const uri = vscode.Uri.file(request.path)
const options: vscode.TextDocumentShowOptions = {}
if (request.options?.preview !== undefined) {
options.preview = request.options.preview
}
if (request.options?.preserveFocus !== undefined) {
options.preserveFocus = request.options.preserveFocus
}
if (request.options?.viewColumn !== undefined) {
options.viewColumn = request.options.viewColumn
}
const editor = await vscode.window.showTextDocument(uri, options)
return TextEditorInfo.create({
documentPath: editor.document.uri.fsPath,
viewColumn: editor.viewColumn,
isActive: vscode.window.activeTextEditor === editor,
})
}
@@ -1,73 +0,0 @@
import * as vscode from "vscode"
import fs from "fs/promises"
import path from "path"
import os from "os"
import CheckpointTracker from "./CheckpointTracker"
export async function createTestEnvironment() {
// Create temp directory structure
const tempDir = path.join(os.tmpdir(), `checkpoint-test-${Date.now()}`)
await fs.mkdir(tempDir, { recursive: true })
// Create storage path outside of working directory to avoid submodule issues
const globalStoragePath = path.join(os.tmpdir(), `storage-${Date.now()}`)
await fs.mkdir(globalStoragePath, { recursive: true })
// Create test file in a subdirectory
const testDir = path.join(tempDir, "src")
await fs.mkdir(testDir, { recursive: true })
const testFilePath = path.join(testDir, "test.txt")
// Create .gitignore to prevent git from treating directories as submodules
await fs.writeFile(path.join(tempDir, ".gitignore"), "storage/\n")
// Mock VS Code workspace
const mockWorkspaceFolders = [
{
uri: { fsPath: tempDir },
name: "test",
index: 0,
},
]
const originalDescriptor = Object.getOwnPropertyDescriptor(vscode.workspace, "workspaceFolders")
Object.defineProperty(vscode.workspace, "workspaceFolders", {
get: () => mockWorkspaceFolders,
})
// Mock findFiles to return no nested git repos
const originalFindFiles = vscode.workspace.findFiles
vscode.workspace.findFiles = async () => []
// Mock VS Code configuration
const originalGetConfiguration = vscode.workspace.getConfiguration
vscode.workspace.getConfiguration = () =>
({
get: (key: string) => (key === "enableCheckpoints" ? true : undefined),
}) as any
return {
tempDir,
globalStoragePath,
testFilePath,
originalDescriptor,
originalFindFiles,
originalGetConfiguration,
cleanup: async () => {
// Restore VS Code mocks
if (originalDescriptor) {
Object.defineProperty(vscode.workspace, "workspaceFolders", originalDescriptor)
}
vscode.workspace.getConfiguration = originalGetConfiguration
vscode.workspace.findFiles = originalFindFiles
// Clean up temp directories
await fs.rm(tempDir, { recursive: true, force: true })
await fs.rm(globalStoragePath, { recursive: true, force: true })
}
}
}
export async function createTestTracker(globalStoragePath?: string, taskId = "test-task-1") {
return await CheckpointTracker.create(taskId, globalStoragePath)
}
@@ -1,155 +0,0 @@
import { expect } from "chai"
import { describe, it, beforeEach, afterEach } from "mocha"
import fs from "fs/promises"
import path from "path"
import { createTestEnvironment, createTestTracker } from "./Checkpoint-test-utils"
describe("Checkpoint Commit Operations", () => {
let env: Awaited<ReturnType<typeof createTestEnvironment>>
beforeEach(async () => {
env = await createTestEnvironment()
})
afterEach(async () => {
await env.cleanup()
})
it("should create commit with single file changes", async () => {
const tracker = await createTestTracker(env.globalStoragePath)
if (!tracker) {throw new Error("Failed to create tracker")}
// Create initial file
await fs.writeFile(env.testFilePath, "initial content")
// Create first commit
const firstCommit = await tracker.commit()
expect(firstCommit).to.be.a("string").and.not.empty
// Modify file
await fs.writeFile(env.testFilePath, "modified content")
// Create second commit
const secondCommit = await tracker.commit()
expect(secondCommit).to.be.a("string").and.not.empty
expect(secondCommit).to.not.equal(firstCommit)
// Verify commits are different
const diffSet = await tracker.getDiffSet(firstCommit, secondCommit)
expect(diffSet).to.have.lengthOf(1)
expect(diffSet[0].before).to.equal("initial content")
expect(diffSet[0].after).to.equal("modified content")
})
it("should create commit with multiple file changes", async () => {
const tracker = await createTestTracker(env.globalStoragePath)
if (!tracker) {throw new Error("Failed to create tracker")}
// Create initial files with newlines
const testFile2Path = path.join(env.tempDir, "src", "test2.txt")
await fs.writeFile(env.testFilePath, "file1 initial\n")
await fs.writeFile(testFile2Path, "file2 initial\n")
// Create first commit
const firstCommit = await tracker.commit()
expect(firstCommit).to.be.a("string").and.not.empty
// Modify both files with newlines
await fs.writeFile(env.testFilePath, "file1 modified\n")
await fs.writeFile(testFile2Path, "file2 modified\n")
// Create second commit
const secondCommit = await tracker.commit()
expect(secondCommit).to.be.a("string").and.not.empty
expect(secondCommit).to.not.equal(firstCommit)
// Get diff between commits
const diffSet = await tracker.getDiffSet(firstCommit, secondCommit)
expect(diffSet).to.have.lengthOf(2)
// Sort diffSet by path for consistent ordering
const sortedDiffs = diffSet.sort((a, b) => a.relativePath.localeCompare(b.relativePath))
// Verify file paths
expect(sortedDiffs[0].relativePath).to.equal("src/test.txt")
expect(sortedDiffs[1].relativePath).to.equal("src/test2.txt")
// Verify file contents
expect(sortedDiffs[0].before).to.equal("file1 initial\n")
expect(sortedDiffs[0].after).to.equal("file1 modified\n")
expect(sortedDiffs[1].before).to.equal("file2 initial\n")
expect(sortedDiffs[1].after).to.equal("file2 modified\n")
})
it("should create commit when files are deleted", async () => {
const tracker = await createTestTracker(env.globalStoragePath)
if (!tracker) {throw new Error("Failed to create tracker")}
// Create and commit initial file
await fs.writeFile(env.testFilePath, "initial content")
const firstCommit = await tracker.commit()
expect(firstCommit).to.be.a("string").and.not.empty
// Delete file
await fs.unlink(env.testFilePath)
// Create second commit
const secondCommit = await tracker.commit()
expect(secondCommit).to.be.a("string").and.not.empty
expect(secondCommit).to.not.equal(firstCommit)
// Verify file deletion was committed
const diffSet = await tracker.getDiffSet(firstCommit, secondCommit)
expect(diffSet).to.have.lengthOf(1)
expect(diffSet[0].before).to.equal("initial content")
expect(diffSet[0].after).to.equal("")
})
it("should create empty commit when no changes", async () => {
const tracker = await createTestTracker(env.globalStoragePath)
if (!tracker) {throw new Error("Failed to create tracker")}
// Create and commit initial file
await fs.writeFile(env.testFilePath, "test content")
const firstCommit = await tracker.commit()
expect(firstCommit).to.be.a("string").and.not.empty
// Create commit without changes
const secondCommit = await tracker.commit()
expect(secondCommit).to.be.a("string").and.not.empty
expect(secondCommit).to.not.equal(firstCommit)
// Verify no changes between commits
const diffSet = await tracker.getDiffSet(firstCommit, secondCommit)
expect(diffSet).to.have.lengthOf(0)
})
it("should handle files in nested directories", async () => {
const tracker = await createTestTracker(env.globalStoragePath)
if (!tracker) {throw new Error("Failed to create tracker")}
// Create nested directory structure
const nestedDir = path.join(env.tempDir, "src", "deep", "nested")
await fs.mkdir(nestedDir, { recursive: true })
const nestedFilePath = path.join(nestedDir, "nested.txt")
// Create and commit file in nested directory
await fs.writeFile(nestedFilePath, "nested content")
const firstCommit = await tracker.commit()
expect(firstCommit).to.be.a("string").and.not.empty
// Modify nested file
await fs.writeFile(nestedFilePath, "modified nested content")
// Create second commit
const secondCommit = await tracker.commit()
expect(secondCommit).to.be.a("string").and.not.empty
// Verify changes were committed
const diffSet = await tracker.getDiffSet(firstCommit, secondCommit)
expect(diffSet).to.have.lengthOf(1)
expect(diffSet[0].relativePath).to.equal("src/deep/nested/nested.txt")
expect(diffSet[0].before).to.equal("nested content")
expect(diffSet[0].after).to.equal("modified nested content")
})
})

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