Compare commits

...

115 Commits

Author SHA1 Message Date
Dennis Bartlett ba0cae3648 Add stub for skipAnimation
Co-authored-by: Mandeep Kumar <mandeep@deepcognition.ai>
2025-03-19 21:08:37 -07:00
Dennis Bartlett 974f698e54 Revert "Added option for skipping diff animation. Useful when diff animation …" (#2341)
This reverts commit 12f16a47e3.
2025-03-19 20:12:07 -07:00
deepman 12f16a47e3 Added option for skipping diff animation. Useful when diff animation … (#1765)
* Added option for skipping diff animation. Useful when diff animation is slow.

* Update README.md

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

* fixed copyright symbol and default value

* Update README.md

---------

Co-authored-by: Mandeep Kumar <mandeep@deepcognition.ai>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
Co-authored-by: Dennis Bartlett <bartlett.dc.1@gmail.com>
2025-03-19 16:34:51 -07:00
Lobay bc7d6f0c66 fix: improve Azure API detection to support custom endpoints (#2332)
* fix: improve Azure API detection to support custom endpoints

* chore: add changeset for Azure API version detection fix
2025-03-19 11:48:42 -07:00
Saoud Rizwan c8b446ab6c Refactor 2025-03-19 11:24:13 -07:00
Evan 2cc64973fb Size up the Task (#2246)
* Calculate and display total tasks and checkpoints size

* changeset

* Remove logs and add try/catch to file deletion

* call postMessage from HistoryView instead
2025-03-18 11:34:35 -07:00
Evan 599c3f6717 SuccessButton -> TW (#2289)
* migrate SuccessButton to Tailwind

* changeset
2025-03-17 23:54:25 -07:00
Saoud Rizwan 1c491bf719 Prepare for release 2025-03-17 21:04:51 -07:00
canvrno 10ac28b9d6 Issue with 'See More' button in task header (#2302)
* Fixes issue with 'See More' button in task header

* Removed requestAnimationFrame

* Cleanup

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-03-17 20:59:14 -07:00
omercelik 95b2e327a8 Added mistral-small-latest (Mistral Small 3.1) (#2300) 2025-03-17 17:38:37 -07:00
Evan 53d11c88bb Upgrade MCP SDK (#2301)
* upgrade mcp sdk

* changeset
2025-03-17 17:38:09 -07:00
canvrno 7ef0f6d1b6 Add --no-verify to checkpoint commits (#2298)
* initial

* Corrected options
2025-03-17 13:53:29 -07:00
Saoud Rizwan 9012d6a1fc Copy 2025-03-14 19:07:24 -07:00
Saoud Rizwan e3d363c56f Prepare for release 2025-03-14 18:56:30 -07:00
canvrno 62a04ad38a Add extension version to telemetry 2025-03-14 18:28:16 -07:00
Saoud Rizwan 1774cf8b79 Add options to question & plan tools (#2264)
* Add question options

* Fixes

* Fix taskheader for anthropic only showing cache info after first request

* Add plan mode options

* Fixes

* Start task header not expanded
2025-03-14 18:25:40 -07:00
minorunara 6133eb4864 fix: Correct VPC endpoint parameter for AnthropicBedrock (#2237)
The VPC endpoint for AnthropicBedrock should be specified using the
'baseURL' parameter instead of the 'endpoint' parameter.
2025-03-14 17:05:56 -07:00
canvrno 087f1d3be3 eng-245 / More checkpoints git telemetry (#2258)
* Checkpoints telemetry improvements

* Checkpoints telemetry improvements
2025-03-14 17:03:43 -07:00
Saoud Rizwan 8cc7759584 Fix bug where OpenRouter/Cline providers generation endpoint failed (#2262)
* Fix openrouter/cline provider cost endpoint bug

* Increase generation endpoint timeout
2025-03-14 17:01:48 -07:00
Evan f9d70ceb6c Stream claude thinking vertex (#2244)
* stream thinking

* changeset
2025-03-14 11:36:59 -07:00
watany e8bf106965 fix(bedrock): Support VPC Endpoint options for Deepseek (#2224)
* fix(bedrock): Support VPC Endpoint options for Deepseek

* change set
2025-03-12 23:26:51 -07:00
Saoud Rizwan b528640bd5 Add warning when checkpoints takes too long to load (#2230)
* Add warning for when checkpoints takes too long to load

* Add changeset
2025-03-12 23:25:13 -07:00
Evan 470778aa2f Display Sonnet Thinking - Bedrock (#2225)
* stream thinking blocks from anthropic

* changeset

* remove logger
2025-03-12 22:35:25 -07:00
dependabot[bot] 14f726c5e8 Bump @babel/runtime in /webview-ui in the npm_and_yarn group (#2222)
Bumps the npm_and_yarn group in /webview-ui with 1 update: [@babel/runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-runtime).


Updates `@babel/runtime` from 7.26.0 to 7.26.10
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.26.10/packages/babel-runtime)

---
updated-dependencies:
- dependency-name: "@babel/runtime"
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-12 19:42:33 -07:00
Saoud Rizwan 89e1086981 Fix lock 2025-03-12 19:39:46 -07:00
watany c762583d38 feat(bedrock): adding deepseek-r1 (#2185)
* feat(bedrock): adding deepseek-r1

* changeset

* fix(ellipsis-dev):Token usage

* fix(ellipsis): Consider propagating these errors or yielding an error response.

* fix msg

* fix: reuse the convertToR1Format

* fix: token count

* Revert lock

* Install package

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-03-12 19:35:27 -07:00
minorunara 73a340c75f feat: Enable VPC endpoint configuration for Amazon Bedrock in AWS Profile mode (#2214)
* feat: Enable VPC endpoint configuration for Amazon Bedrock in AWS Profile mode

This update allows specifying a VPC endpoint when connecting to Amazon Bedrock using AWS Profile mode. It ensures that all API requests stay within a private network and do not traverse the public internet.

* Hide VPC textfield behind toggle

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-03-12 19:15:19 -07:00
Evan d3bb6ff456 Configure Computer Use Openai Compatible (#2215)
* Set up branch

* feat: Allow to configure computer use for openapi compatible provider (#1923)

* feat: Allow to set computerUse for OpenAI Compatible provider

* Update webview-ui/src/components/settings/ApiOptions.tsx

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

---------

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

---------

Co-authored-by: FlavioInacta <flavio.pellanda@inacta.ch>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-03-12 19:00:40 -07:00
Evan 93cd64f031 Add SambaNova Provider (#2216)
* Set up branch

* Feat/add sambanova provider (#2142)

* feat(api): Add SambaNova provider type definitions

- Add 'sambanova' to ApiProvider type
- Add sambanovaApiKey to ApiHandlerOptions interface
- Add SambaNova model definitions

* feat(api): Implement SambaNova provider handler

- Create SambanovaHandler class
- Implement createMessage and getModel methods
- Register the provider in the API factory

* feat(ui): Add SambaNova provider UI components

- Add SambaNova to provider dropdown
- Add UI components for API key and model selection

* feat(storage): Update secret storage for SambaNova API key

- Update SecretKey type to include sambanovaApiKey
- Update getState method to include the SambaNova API key
- Update API configuration handler to store the SambaNova API key

* feat(sambanova): format fix

* feat(sambanova): adding previously untracked files

* fix: will ensure that when no model ID is explicitly selected for the Sambanova provider, it will use the default model ID that's already defined in the codebase (Meta-Llama-3.3-70B-Instruct)

* fix: changing how the dropdown is created by following existing pattern

* fix: changing how the dropdown is created by following existing pattern

* fix: added SamanovaHandler to buildApiHandler

---------

Co-authored-by: saad.khan.hd <saad.khan@hd.digital>
Co-authored-by: Dennis Bartlett <bartlett.dc.1@gmail.com>

* support deepseek and qwen models

* remove unused imports

* Add missing API provider implementation

---------

Co-authored-by: saad-noodleseed <saad@noodleseed.com>
Co-authored-by: saad.khan.hd <saad.khan@hd.digital>
Co-authored-by: Dennis Bartlett <bartlett.dc.1@gmail.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-03-12 18:59:42 -07:00
k-inoway 36836ad4d6 Fix temperature param for o3mini and o1mini when using LiteLLM (#2203)
* fix: prevent invalid temperature 0 param when using o3mini model

* fix: handle temperature param properly for o1-mini and o3-mini models
2025-03-12 11:33:12 -07:00
Taosh 4ade5daeff fix: #2153 make sure isolated qwen model setting on switching mode settings (#2210)
* fix: #2153 make sure isolated qwen model setting on switch mode settings

* Re-add openainative

---------

Co-authored-by: 青炽 <hanyuntao.hyt@alibaba-inc.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-03-12 11:26:49 -07:00
canvrno 24fe5c9c98 Add getDiffCount (#2195)
* changeset

* Track changes with working directory if rhs omitted

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-03-12 11:24:46 -07:00
TongZ 18d4a268e2 Fix tooltip layer bug (#2189) 2025-03-11 20:25:21 -07:00
Saoud Rizwan 686f51c248 Fix plan/act model switching for deepseek 2025-03-11 20:23:17 -07:00
Saoud Rizwan 9aa04a376f Add limit for files Cline reads into context (#2199)
* Revert "Merge pull request #1964 from cline/ocasta181/ENG-209"

This reverts commit ca17a06618, reversing
changes made to 6b9c2a1a45.

* Set hard 300kb limit on file size cline reads into context

* Add changeset
2025-03-11 19:46:28 -07:00
akfoster ca17a06618 Merge pull request #1964 from cline/ocasta181/ENG-209
[Fix] Cline crashes when reading files with large outputs
2025-03-11 17:23:07 -07:00
ocasta181 48ea04f2ea add test description 2025-03-11 15:06:24 -07:00
ocasta181 e11ad166e6 pass less data between functions 2025-03-11 15:01:38 -07:00
Daniel Steigman 6b9c2a1a45 Merge pull request #2182 from jiyeonseo/main
Add Korean language
2025-03-11 14:08:51 -07:00
Daniel Steigman bf0bbb4a16 Create selfish-bikes-decide.md 2025-03-11 14:07:27 -07:00
Daniel Steigman a8a24fcb62 Update locales/ko/README.md
Co-authored-by: Dennis Bartlett <bartlett.dc.1@gmail.com>
2025-03-11 14:07:09 -07:00
Daniel Steigman e570662d89 Merge pull request #2193 from cline/bug/readme_copyright
Update copyright for Readme Files
2025-03-11 14:06:28 -07:00
canvrno 78f5faf6d2 Create poor-zoos-jump.md 2025-03-11 13:01:31 -07:00
Dennis Bartlett cc28851ba8 Update copyright for Readme Files 2025-03-11 12:56:54 -07:00
Dennis Bartlett 0afa4c4fd8 Update src/utils/fs.ts
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-03-10 20:58:18 -07:00
ocasta181 0f0512363f read config in try/catch 2025-03-10 20:52:53 -07:00
ocasta181 ddc91a46c0 Merge branch 'main' into ocasta181/ENG-209 2025-03-10 20:18:50 -07:00
ocasta181 11a32d9a3a update changelog 2025-03-10 20:08:00 -07:00
ocasta181 45ddaf99e1 fix tests 2025-03-10 20:07:12 -07:00
ocasta181 6f21cb98cc back out non-file related checks 2025-03-10 20:03:33 -07:00
ocasta181 544d4503ac current progress 2025-03-10 19:32:38 -07:00
Cheese a5663e7cf8 Update locales/ko/CONTRIBUTING.md
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-03-11 01:47:36 +09:00
Cheese 09b0873e94 Update locales/ko/README.md
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-03-11 01:47:25 +09:00
cheese e54623bc7e Add Korean language 2025-03-11 01:39:47 +09:00
ryo-ma 8909ee77fc Support loading by directory in .clinerules/ (#2156)
* Support loading by directory in .clinerules/

* Update src/utils/fs.ts

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

* Update docs/prompting/README.md

* Add changeset

* Add tests of isDirectory

* Use relative path in instructions

* Remove log

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-03-09 20:08:12 -07:00
Evan 3a3943623b Hoist the Sails! (#2169)
* set up tailwind

* changeset
2025-03-09 19:49:09 -07:00
Saoud Rizwan 2446b16828 Fix plan/act model switching for OpenAI native 2025-03-09 19:19:46 -07:00
Saoud Rizwan 30f1d72eb5 Prepare for release 2025-03-09 19:16:41 -07:00
Saoud Rizwan c67edf4b91 Fix type error when opening openrouter model picker with invalid model id 2025-03-09 19:15:56 -07:00
Saoud Rizwan 07cf5cddad Refactor planActSeparateModelSetting initial value implementation 2025-03-09 18:47:42 -07:00
Saoud Rizwan eda2a60a69 Prepare for release 2025-03-09 18:24:24 -07:00
Saoud Rizwan c1b9fcd05c Fix issue where planActSeparateModelsSetting was initialized with non-undefined value 2025-03-09 18:24:06 -07:00
Saoud Rizwan d470ba9150 Prepare for release 2025-03-09 17:58:23 -07:00
Saoud Rizwan ea302ff4f1 Use text display for MCP response temporarily 2025-03-09 17:40:46 -07:00
Saoud Rizwan f11bddc4be Prepare for release 2025-03-09 00:25:06 -08:00
Saoud Rizwan aceb7e8ddd Fix bug where clicking on selected plan/act tab in settings would switch modes 2025-03-09 00:24:13 -08:00
Saoud Rizwan 8ca0568005 Prepare for release 2025-03-08 20:44:19 -08:00
Saoud Rizwan 42b17cfbaa Fix issue where setting default plan/act setting for new users affected users who used anthropic before multiple api options were provided 2025-03-08 20:44:02 -08:00
Saoud Rizwan 359dac9f20 Revert version change 2025-03-08 20:29:03 -08:00
Saoud Rizwan 3948b952cc Disable delete button if no tasks 2025-03-08 20:24:34 -08:00
Saoud Rizwan 867f36dabe Update changelog 2025-03-08 20:19:54 -08:00
Dennis Bartlett 822647c3e9 Fix context window size setting on OpenAI compatible provider (#2162) 2025-03-08 20:17:57 -08:00
Saoud Rizwan fa0c4e5c36 Add delete all task history button (#2163)
* Add delete all task history button

* Add changeset

* Add error message if deleting tasks fails
2025-03-08 20:17:17 -08:00
Saoud Rizwan 59dc92c65c Fix modifying thinking budget not being persisted between mode switches 2025-03-08 18:51:41 -08:00
Saoud Rizwan 6972b2a975 Modify presentation of plan/act settings 2025-03-08 18:17:37 -08:00
Saoud Rizwan 973d18f4f3 Update plan/act tab as soon as settings are saved 2025-03-08 17:56:38 -08:00
Saoud Rizwan 5ab50a41b1 Reduce timeout after tab change 2025-03-08 17:36:25 -08:00
Saoud Rizwan d92292e405 Prepare for release 2025-03-08 17:34:07 -08:00
Saoud Rizwan d0728c58af Fix issue where settings wouldn't all update if user used invalid api config 2025-03-08 17:29:44 -08:00
Saoud Rizwan 30f6a3ca29 Fix welcome screen 2025-03-08 17:05:52 -08:00
Saoud Rizwan 6aad12dc01 Remove log 2025-03-08 16:58:45 -08:00
Saoud Rizwan 9cd0a5ed6a Set plan/act switching to false for new users 2025-03-08 16:58:16 -08:00
Saoud Rizwan 311a4d6ee8 Fix plan/act switching if no model id set or thinking budget changed 2025-03-08 16:21:54 -08:00
Saoud Rizwan 4e2f3d9b0e Update telemetry setting 2025-03-08 16:03:50 -08:00
Saoud Rizwan bdccc40a0f Update privacy policy URL 2025-03-08 16:00:23 -08:00
Saoud Rizwan 5d10a0f89a Add setting to disable plan/act model switching 2025-03-08 15:54:28 -08:00
canvrno a41bb3a70a Checkpoints documentation cleanup (#2160)
* Checkpoints documentation cleanup

* Update src/integrations/checkpoints/CheckpointTracker.ts

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

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-03-08 13:44:36 -08:00
Evan 42e0c74c93 OpenAI Compatible - Temperature Controlled (#2092)
* add temperature to openai compatible model configuration

* prevent type errors

* improved options layout

* changeset

* move temperature to openaimodelinfo

* type error

* use nullish coalescing operator

* Allow user to pass decimal values

* Rename openAiTemperature to temperature

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-03-08 13:42:50 -08:00
Saoud Rizwan ef76c8ba5a Remove changeset release workflow 2025-03-08 13:42:05 -08:00
Saoud Rizwan 80542434c5 Disable changeset release workflow 2025-03-08 13:39:54 -08:00
kuu(Fumiya Kume) 69bda35df8 Feat: Add Kotlin Support for language paraer (#2121)
* feat: Add Kotlin support to the tree-sitter parser

* fix: Improve Define file for  kotlin

* Add Changeset

* Add kotlin tree-sitter-wasm

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-03-08 13:34:38 -08:00
canvrno e3b87ae56f Removed unused checkpoints telemetry (#2159)
* Removed unused checkpoints telemetry

* Changeset
2025-03-08 11:29:42 -08:00
Saoud Rizwan daf88ba19e Prepare for release 2025-03-08 07:43:14 -08:00
Saoud Rizwan 4759586a17 Revert "Fix Token Count when API incorrectly returns token count per chunk. (#2145)"
This reverts commit ca4fdcf3c4.
2025-03-08 07:41:04 -08:00
Saoud Rizwan 39359307dc Update package lock 2025-03-07 21:13:35 -08:00
Saoud Rizwan 9fa8a7a7a8 Prepare for release 2025-03-07 21:10:49 -08:00
Saoud Rizwan 7eaf1a7cbc Modify diff edit error warning and prompt response 2025-03-07 20:55:13 -08:00
乔浩 66911b89de Add support for qwq32b to OpenRouter. (#2135) 2025-03-07 20:36:38 -08:00
Evan 0de047116f Add Alibaba QwQ for Alibaba Provider - Mainland (#2128)
* add  qwen qwq-plus model

* rename variable

* set up branch

* segment alibaba models between mainland and international

* allow temperature passing to qwq

* changeset

---------

Co-authored-by: yaojunWang <gooqle.com.hk@gmail.com>
2025-03-07 20:25:50 -08:00
dependabot[bot] 60cd10eec6 Bump axios from 1.7.4 to 1.8.2 in the npm_and_yarn group (#2148)
Bumps the npm_and_yarn group with 1 update: [axios](https://github.com/axios/axios).


Updates `axios` from 1.7.4 to 1.8.2
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.7.4...v1.8.2)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-07 20:16:56 -08:00
Saoud Rizwan 209d0830c6 Prepare for release 2025-03-07 19:33:11 -08:00
Saoud Rizwan 8f938b34ab Replace branching strategy with one repo per workspace (#2147) 2025-03-07 19:31:04 -08:00
Dennis Bartlett ca4fdcf3c4 Fix Token Count when API incorrectly returns token count per chunk. (#2145)
* Fix Token Count when API incorrectly returns token count per chunk.

* changeset

* Update src/core/Cline.ts

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

* Update src/core/Cline.ts

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

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-03-07 17:27:09 -08:00
ocasta181 a2572ea233 Merge branch 'main' into ocasta181/ENG-209 2025-03-07 15:19:09 -08:00
ocasta181 f608b914b8 remove uneccesary pass through getMaxAllowedSize; fix tests 2025-02-28 22:09:54 -08:00
ocasta181 9f5df9836c remove exclusion from tsconfig 2025-02-28 21:58:08 -08:00
akfoster 0cdbdb057e Update src/integrations/terminal/TerminalProcess.ts
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-02-28 21:49:29 -08:00
ocasta181 4846476f6c remove more unused logic 2025-02-28 21:47:12 -08:00
ocasta181 a6f189c5c5 remove calculateUsedContext 2025-02-28 21:26:06 -08:00
ocasta181 ad8e0b19be remove usedContext references 2025-02-28 21:21:53 -08:00
ocasta181 22b2e50210 use real context window size 2025-02-28 18:50:28 -08:00
ocasta181 af0d2f31a7 add changeset 2025-02-28 18:35:04 -08:00
ocasta181 8313fa099c reduce complexity, get working 2025-02-28 18:35:04 -08:00
Ocasta 9f418b10f6 cleanup vscode module declaration 2025-02-28 18:35:04 -08:00
Ocasta 1b56c3fc11 new attempt 2025-02-28 18:34:43 -08:00
70 changed files with 5859 additions and 1368 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
fixes an issue with Azure API version detection in the OpenAI provider
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Add size calculation to "Delete all Tasks" button
@@ -2,4 +2,4 @@
"claude-dev": patch
---
Checkpoints Telemetry
SuccessButton to Tailwind
-162
View File
@@ -1,162 +0,0 @@
name: Changeset Release
run-name: Changeset Release ${{ github.actor != 'cline-bot' && '- Create PR' || '- Update Changelog' }}
permissions:
contents: write
pull-requests: write
on:
workflow_dispatch:
pull_request:
types: [closed, opened, labeled]
env:
REPO_PATH: ${{ github.repository }}
GIT_REF: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || 'main' }}
jobs:
# Job 1: Create version bump PR when changesets are merged to main
changeset-pr-version-bump:
if: >
( github.event_name == 'pull_request' &&
github.event.pull_request.merged == true &&
github.event.pull_request.base.ref == 'main' &&
github.actor != 'cline-bot' ) ||
github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Git Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
with:
fetch-depth: 0
ref: ${{ env.GIT_REF }}
- name: Setup Node.js
uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4
with:
node-version: 20
cache: "npm"
- name: Install Dependencies
run: npm run install:all
# Check if there are any new changesets to process
- name: Check for changesets
id: check-changesets
run: |
NEW_CHANGESETS=$(find .changeset -name "*.md" ! -name "README.md" | wc -l | tr -d ' ')
echo "Changesets diff with previous version: $NEW_CHANGESETS"
echo "new_changesets=$NEW_CHANGESETS" >> $GITHUB_OUTPUT
# Create version bump PR using changesets/action if there are new changesets
- name: Changeset Pull Request
if: steps.check-changesets.outputs.new_changesets != '0'
id: changesets
uses: changesets/action@e9cc34b540dd3ad1b030c57fd97269e8f6ad905a # v1
with:
commit: "changeset version bump"
title: "Changeset version bump"
version: npm run version-packages # This performs the changeset version bump
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Job 2: Process version bump PR created by cline-bot
changeset-pr-edit-approve:
name: Auto approve and merge Bump version PRs
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
if: >
github.event_name == 'pull_request' &&
github.event.pull_request.base.ref == 'main' &&
github.actor == 'cline-bot' &&
contains(github.event.pull_request.title, 'Changeset version bump')
steps:
- name: Determine checkout ref
id: checkout-ref
run: |
echo "Event action: ${{ github.event.action }}"
echo "Actor: ${{ github.actor }}"
echo "Head ref: ${{ github.head_ref }}"
echo "PR SHA: ${{ github.event.pull_request.head.sha }}"
if [[ "${{ github.event.action }}" == "opened" && "${{ github.actor }}" == "cline-bot" ]]; then
echo "Using branch ref: ${{ github.head_ref }}"
echo "git_ref=${{ github.head_ref }}" >> $GITHUB_OUTPUT
else
echo "Using SHA ref: ${{ github.event.pull_request.head.sha }}"
echo "git_ref=${{ github.event.pull_request.head.sha }}" >> $GITHUB_OUTPUT
fi
- name: Checkout Repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0
ref: ${{ steps.checkout-ref.outputs.git_ref }}
# Get current and previous versions to edit changelog entry
- name: Get version
id: get_version
run: |
VERSION=$(git show HEAD:package.json | jq -r '.version')
echo "version=$VERSION" >> $GITHUB_OUTPUT
PREV_VERSION=$(git show origin/main:package.json | jq -r '.version')
echo "prev_version=$PREV_VERSION" >> $GITHUB_OUTPUT
echo "version=$VERSION"
echo "prev_version=$PREV_VERSION"
# Update CHANGELOG.md with proper format
- name: Update Changelog Format
if: ${{ !contains(github.event.pull_request.labels.*.name, 'changelog-ready') }}
env:
VERSION: ${{ steps.get_version.outputs.version }}
PREV_VERSION: ${{ steps.get_version.outputs.prev_version }}
run: python .github/scripts/overwrite_changeset_changelog.py
# Commit and push changelog updates
- name: Push Changelog updates
if: ${{ !contains(github.event.pull_request.labels.*.name, 'changelog-ready') }}
run: |
git config user.name "cline-bot"
git config user.email github-actions@github.com
echo "Running git add and commit..."
git add CHANGELOG.md
git commit -m "Updating CHANGELOG.md format"
git status
echo "--------------------------------------------------------------------------------"
echo "Pushing to remote..."
echo "--------------------------------------------------------------------------------"
git push
# Add label to indicate changelog has been formatted
- name: Add changelog-ready label
if: ${{ !contains(github.event.pull_request.labels.*.name, 'changelog-ready') }}
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['changelog-ready']
});
# Auto-approve PR only after it has been labeled
- name: Auto approve PR
if: contains(github.event.pull_request.labels.*.name, 'changelog-ready')
uses: hmarr/auto-approve-action@de8bf34d0402c38aa2c8346973342b2cb02c4435 # v4
with:
review-message: "I'm approving since it's a bump version PR"
# Auto-merge PR
- name: Automerge on PR
if: false # Needs enablePullRequestAutoMerge in repo settings to work contains(github.event.pull_request.labels.*.name, 'changelog-ready')
run: gh pr merge --auto --merge ${{ github.event.pull_request.number }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+29
View File
@@ -1,5 +1,34 @@
# Changelog
## [3.7.1]
- Fix issue with 'See more' button in task header not showing when starting new tasks
- Fix issue with checkpoints using local git commit hooks
## [3.7.0]
- Cline now displays selectable options when asking questions or presenting a plan, saving you from having to type out responses!
- Add support for a `.clinerules/` directory to load multiple files at once (thanks @ryo-ma!)
- Prevent Cline from reading extremely large files into context that would overload context window
- Improve checkpoints loading performance and display warning for large projects not suited for checkpoints
- Add SambaNova API provider (thanks @saad-noodleseed!)
- Add VPC endpoint option for AWS Bedrock profiles (thanks @minorunara!)
- Add DeepSeek-R1 to AWS Bedrock (thanks @watany-dev!)
## [3.6.5]
- Add 'Delete all Task History' button to History view
- Add toggle to disable model switching between Plan/Act modes in Settings (new users default to disabled)
- Add temperature option to OpenAI Compatible
- Add Kotlin support to tree-sitter parser (thanks @fumiya-kume!)
## [3.6.3]
- Improve QwQ support for Alibaba (thanks @meglinge!) and OpenRouter
- Improve diff edit prompting to prevent immediately reverting to write_to_file when a model uses search patterns that don't match anything in the file
- Fix bug where new checkpoints system would revert file changes when switching between tasks
- Fix issue with incorrect token count for some OpenAI compatible providers
## [3.6.0]
- Add Cline API as a provider option, allowing new users to sign up and get started with Cline for free
+1 -1
View File
@@ -1,5 +1,5 @@
<div align="center"><sub>
English | <a href="https://github.com/cline/cline/blob/main/locales/es/README.md" target="_blank">Español</a> | <a href="https://github.com/cline/cline/blob/main/locales/de/README.md" target="_blank">Deutsch</a> | <a href="https://github.com/cline/cline/blob/main/locales/ja/README.md" target="_blank">日本語</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-cn/README.md" target="_blank">简体中文</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-tw/README.md" target="_blank">繁體中文</a>
English | <a href="https://github.com/cline/cline/blob/main/locales/es/README.md" target="_blank">Español</a> | <a href="https://github.com/cline/cline/blob/main/locales/de/README.md" target="_blank">Deutsch</a> | <a href="https://github.com/cline/cline/blob/main/locales/ja/README.md" target="_blank">日本語</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-cn/README.md" target="_blank">简体中文</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-tw/README.md" target="_blank">繁體中文</a> | <a href="https://github.com/cline/cline/blob/main/locales/ko/README.md" target="_blank">한국어</a>
</sub></div>
# Cline \#1 on OpenRouter
+1 -116
View File
@@ -1,116 +1 @@
# Cline Privacy Policy
Cline Bot Inc. ("Cline," "we," "our," and/or "us") values the privacy of individuals who use our VS Code extension and related services (collectively, our "Services"). This privacy policy explains how we collect, use, and disclose information from users of our Services.
## Key Points
- Cline operates entirely client-side as a VS Code extension
- No code or data is collected, stored, or transmitted to Cline's servers
- Your data is only sent to your chosen AI provider (e.g., Anthropic, OpenAI) when you explicitly request assistance
- All processing happens locally on your machine
- API keys are stored securely in VS Code's built-in settings storage
## Information We Process
### A. Information You Provide
- **API Keys**: When you choose to use certain AI model providers (OpenRouter, Anthropic, OpenAI, etc.), you provide API keys. These are stored securely and locally in your VS Code settings.
- **Communications**: If you contact us directly (e.g., via Discord or email), we may receive information like your name, email address, and message contents.
### B. Information Processing
Cline functions solely as a client-side VS Code extension that facilitates communication between your editor and your chosen AI model provider:
1. **File Contents**:
- Only sent to your chosen AI provider when you explicitly request assistance
- Never stored or transmitted to Cline's servers
- Only the specific files/content you select are included
2. **Terminal Commands**:
- Processed entirely locally on your machine
- Require explicit user confirmation before execution
- No command history is transmitted to Cline
3. **Browser Integration**:
- Screenshots and console logs are processed locally
- Temporary data is cleared after task completion
## Data Security
1. **Local-Only Processing**:
- All operations happen on your local machine
- No central servers or data collection by default
- Anonymous telemetry and usage statistics are only collected if you explicitly opt in
- No account creation required
2. **API Key Security**:
- Stored using VS Code's secure settings storage system
- Never transmitted to Cline's servers
- You can remove/modify keys at any time
3. **User Control**:
- Explicit approval required for file changes
- Terminal commands require confirmation
- Browser actions need explicit permission
- You control which AI provider to use
## Communication with AI Providers
When you request assistance:
1. Selected content is sent directly to your chosen AI provider
2. No data passes through Cline's servers
3. Provider's own privacy policy applies to this communication:
- [Anthropic Privacy Policy](https://www.anthropic.com/privacy)
- [OpenAI Privacy Policy](https://openai.com/privacy)
- [OpenRouter Privacy Policy](https://openrouter.ai/privacy)
## Error Handling & Debugging
- Error logs are processed locally
- No automatic error reporting to Cline
- Optional anonymous telemetry and error reporting via PostHog if you opt in
- You control what information to include when manually reporting issues
## Children's Privacy
We do not knowingly collect, maintain, or use personal information from children under 18 years of age, and no part of our Service(s) is directed to children. If you learn that a child has provided us with personal information in violation of this Privacy Policy, then you may alert us at support@cline.bot.
## Changes to Privacy Policy
We will post any changes to this policy on our GitHub repository. Significant changes will be announced in our Discord community.
## Security Concerns & Auditing
- Cline is open source and available for security audit
- Our client-side architecture ensures no central point of data collection
- You can inspect exactly what data is being sent to AI providers
- Enterprise users can implement additional access controls through VS Code
## Telemetry & Usage Statistics
If you choose to opt in to anonymous telemetry:
- Basic usage statistics and error reports are collected via PostHog
- A stable, anonymous identifier (VS Code's `machineId`) is used to understand unique usage patterns
- This identifier is not linked to any personal information
- It helps us understand how features are used across sessions
- It cannot be used to identify you personally
- All data is anonymized and cannot be linked to individual users
- No code content or sensitive information is ever included
- You can opt out at any time through:
- VS Code Settings > Cline > Enable Telemetry
- VS Code Settings > Telemetry > Telemetry Level (setting this to anything other than "all" will disable Cline's telemetry)
- Collected data helps us improve the extension's functionality and stability
## Contact Us
For privacy-related questions or concerns:
- Open an issue on our [GitHub repository](https://github.com/cline/cline)
- Join our [Discord community](https://discord.gg/cline)
- Email: support@cline.bot
See [https://cline.bot/privacy](https://cline.bot/privacy) for our privacy policy.
+21
View File
@@ -128,6 +128,27 @@ Cline's system prompt, on the other hand, is not user-editable ([here's where yo
- Focus on Desired Outcomes: Describe the results you want, not the specific steps.
- Test and Iterate: Experiment to find what works best for your workflow.
### Support for Loading Files from the `.clinerules/` Directory
All files under the `.clinerules/` directory are recursively loaded, and their contents are merged into clineRulesFileInstructions.
#### Example 1:
```
.clinerules/
├── .local-clinerules
└── .project-clinerules
```
#### Example 2:
```
.clinerules/
├── .clinerules-nextjs
├── .clinerules-serverside
└── tests/
├── .pytest-clinerules
└── .jest-clinerules
```
## Prompting Cline 💬
**Prompting is how you communicate your needs for a given task in the back-and-forth chat with Cline.** Cline understands natural language, so write conversationally.
+1
View File
@@ -52,6 +52,7 @@ const copyWasmFiles = {
"java",
"php",
"swift",
"kotlin",
]
languages.forEach((lang) => {
+1 -1
View File
@@ -158,5 +158,5 @@ Um zum Projekt beizutragen, beginnen Sie mit unserem [Beitragsleitfaden](CONTRIB
## Lizenz
[Apache 2.0 © 2024 Cline Bot Inc.](./LICENSE)
[Apache 2.0 © 2025 Cline Bot Inc.](./LICENSE)
+1 -1
View File
@@ -158,4 +158,4 @@ Para contribuir al proyecto, comience con nuestra [guía de contribución](CONTR
## Licencia
[Apache 2.0 © 2024 Cline Bot Inc.](./LICENSE)
[Apache 2.0 © 2025 Cline Bot Inc.](./LICENSE)
+1 -1
View File
@@ -158,4 +158,4 @@ Clineがタスクを進める中で、拡張機能は各ステップでワーク
## ライセンス
[Apache 2.0 © 2024 Cline Bot Inc.](./LICENSE)
[Apache 2.0 © 2025 Cline Bot Inc.](./LICENSE)
+47
View File
@@ -0,0 +1,47 @@
# 기여자 행동 강령
## 서약
우리는 개방적이고 환영하는 환경을 조성하기 위해 노력하며, 기여자 및 유지 관리자로서 모든 사람이 차별과 괴롭힘 없이 프로젝트와 커뮤니티에 참여할 수 있도록 최선을 다할 것을 서약합니다. 이는 연령, 체형, 장애, 민족성, 성적 특성, 성 정체성 및 표현, 경험 수준, 교육 수준, 사회·경제적 지위, 국적, 외모, 인종, 종교, 성 정체성과 성적 지향에 관계없이 모든 사람에게 적용됩니다.
## 행동 기준
긍정적인 환경을 조성하기 위한 바람직한 행동의 예시:
- 환영하고 포용적인 언어 사용하기
- 서로 다른 관점과 경험을 존중하기
- 건설적인 비판을 우아하게 수용하기
- 커뮤니티에 최선이 되는 것에 집중하기
- 다른 커뮤니티 구성원들에 대한 공감 보여주기
참여자가 해서는 안 되는 행동의 예시:
- 성적인 언어와 이미지 사용, 원치 않는 성적 관심이나 접근
- 트롤링, 모욕적/경멸적인 댓글, 개인적 또는 정치적 공격
- 공개적 또는 사적인 괴롭힘
- 상대방의 동의 없이 개인정보(실제 주소나 전자 주소 등) 공개하기
- 전문적 환경에서 부적절하다고 여겨질 수 있는 기타 행위
## 책임
프로젝트 유지 관리자는 허용 가능한 행동 기준을 명확히 설명할 책임이 있으며, 부적절한 행동이 발생할 경우 적절하고 공정한 시정 조치를 취해야 합니다.
프로젝트 유지 관리자는 본 행동 강령에 부합하지 않는 댓글, 커밋, 코드, 위키 수정, 이슈 및 기타 기여를 삭제, 수정 또는 거부할 권리와 책임이 있으며, 부적절하다고 판단되는 행동(위협적이거나, 공격적이거나, 해로운 행위 등)을 한 기여자를 일시적 또는 영구적으로 차단할 권리를 가집니다.
## 범위
이 행동 강령은 프로젝트 공간과 개인이 프로젝트나 커뮤니티를 대표하는 공개 공간에서 모두 적용됩니다. 프로젝트 또는 커뮤니티를 대표하는 예로는 공식 프로젝트 이메일 주소 사용, 공식 소셜 미디어 계정을 통한 게시, 온라인 또는 오프라인 행사에서 지정된 대표자로 활동하는 경우 등이 포함됩니다. 프로젝트의 대표성은 프로젝트 유지 관리자가 추가로 정의하고 명확히 할 수 있습니다.
## 집행
학대, 괴롭힘 또는 기타 용납할 수 없는 행동은 프로젝트 팀에 hi@cline.bot을 통해 신고 할 수 있습니다. 모든 신고는 검토 및 조사되며, 상황에 따라 필요하고 적절한 조치가 취해질 것입니다. 프로젝트 팀은 사건 신고자의 신원을 보호할 의무가 있습니다. 특정 시행 정책에 대한 추가 세부 사항은 별도로 게시될 수 있습니다.
행동 강령을 성실히 준수하거나 집행하지 않는 프로젝트 유지관리자는 프로젝트 리더십의 구성원에 의해 일시적 또는 영구적인 제재를 받을 수 있습니다.
## 출처
이 행동 강령은 [Contributor Covenant][homepage] 버전 1.4에서 수정되었으며, https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 에서 확인할 수 있습니다.
[homepage]: https://www.contributor-covenant.org
이 행동 강령에 대한 일반적인 질문에 대한 답변은 https://www.contributor-covenant.org/faq 를 참조하시기 바랍니다.
+92
View File
@@ -0,0 +1,92 @@
# Cline에 기여하기
Cline에 기여하는 것에 관심을 가져주셔서 감사합니다! 버그 수정, 기능 추가, 문서 개선 등 모든 기여는 Cline을 더욱 스마트하게 만드는 데 기여합니다. 활기차고 환영하는 커뮤니티를 유지하기 위해 모든 구성원은 [행동 강령](CODE_OF_CONDUCT.md)을 준수해야 합니다.
## 버그와 문제 보고
버그 보고는 Cline을 모두에게 더 나은 것으로 만드는 데 도움이 됩니다! 새로운 이슈를 생성하기 전에, 중복을 피하기 위해 [기존 이슈를 검색](https://github.com/cline/cline/issues)해 주세요. 버그를 보고할 준비가 되었다면, [이슈 페이지](https://github.com/cline/cline/issues/new/choose)로 이동하여 관련 정보를 작성하기 위한 템플릿을 사용해 주세요.
<blockquote class='warning-note'>
🔐 <b>중요:</b> 보안 취약점을 발견한 경우, <a href="https://github.com/cline/cline/security/advisories/new">GitHub 보안 도구를 사용하여 비공개로 보고</a>해 주세요.
</blockquote>
## 작업 내용 결정하기
첫 기여를 찾고 계신가요? ["good first issue"](https://github.com/cline/cline/labels/good%20first%20issue)나 ["help wanted"](https://github.com/cline/cline/labels/help%20wanted) 라벨이 붙은 이슈를 확인해 보세요. 이러한 이슈들은 새로운 기여자를 위해 특별히 선정된 작업으로, 도움이 필요한 영역이 표시되어 있습니다!
또한, [문서](https://github.com/cline/cline/tree/main/docs)에 대한 기여도 환영합니다! 오타 수정, 기존 가이드 개선, 새로운 교육 콘텐츠 작성 등, 커뮤니티 주도의 리소스 저장소를 구축하는 데 여러분의 도움이 필요합니다. `/docs`를 살펴보고 개선이 필요한 부분을 찾아보세요.
큰 기능에 대해 작업할 계획이 있다면, 먼저 [기능 요청](https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop)을 생성하여 이것이 Cline의 비전과 부합하는지 논의하는 것이 좋습니다.
## 개발 환경 설정
1. **VS Code 확장 프로그램**
- 프로젝트를 열면 VS Code가 권장 확장 프로그램 설치를 안내합니다
- 개발을 위해 이 확장 프로그램들이 필요하므로, 설치 안내를 수락해 주세요.
- 프롬프트를 닫은 경우 확장 프로그램 패널에서 수동으로 설치할 수 있습니다
2. **로컬 개발**
- `npm run install:all`을 실행하여 의존성을 설치합니다
- `npm run test`를 실행하여 로컬에서 테스트를 실행합니다
- PR을 제출하기 전에 `npm run format:fix`를 실행하여 코드를 포맷팅합니다
## 코드 작성과 제출
누구나 Cline에 코드를 기여할 수 있지만, 기여가 원활하게 통합되도록 다음 가이드라인을 따라주세요:
1. **Pull Request 집중하기**
- PR은 단일 기능 또는 버그 수정으로 제한해 주세요
- 큰 변경사항은 작은 관련 PR로 분할해 주세요
- 논리적으로 독립적인 커밋 단위로 나누어 리뷰가 용이하도록 구성하세요.
2. **코드 품질**
- `npm run lint`를 실행하여 코드 스타일을 체크합니다
- `npm run format`을 실행하여 코드를 자동으로 포맷팅합니다
- 모든 PR은 린팅과 포맷팅을 포함한 CI 체크를 통과해야 합니다
- 제출 전에 ESLint 경고나 에러를 모두 해결해 주세요
- TypeScript 모범 사례를 따르고, 타입 안전성을 유지해 주세요
3. **테스트**
- 새로운 기능에는 테스트를 추가해 주세요
- `npm test`를 실행하여 모든 테스트가 통과하는지 확인해 주세요
- 변경사항이 기존 테스트에 영향을 미치는 경우 해당 테스트를 업데이트해 주세요
- 적절한 경우 단위 테스트와 통합 테스트를 모두 포함해 주세요
4. **Changesets를 활용한 버전 관리**
- 사용자에게 영향을 미치는 변경 사항이 있는 경우, `npm run changeset`을 실행하여 changeset을 생성해 주세요
- 적절한 버전 증가 옵션을 선택하세요:
- `major` 호환되지 않는 변경 (1.0.0 → 2.0.0)
- `minor` 새로운 기능 추가 (1.0.0 → 1.1.0)
- `patch` 버그 수정 (1.0.0 → 1.0.1)
- 영향을 설명하는 명확한 변경사항 메시지를 작성해 주세요
- 문서 변경만 있는 경우 changeset이 필요하지 않습니다
5. **커밋 가이드라인**
- 명확하고 설명적인 커밋 메시지를 작성해 주세요
- 컨벤셔널 커밋 형식(예: "feat:", "fix:", "docs:")을 사용해 주세요
- 커밋에서 관련 이슈를 #issue-number를 사용하여 참조해 주세요
6. **제출 전 확인사항**
- 최신 main에 브랜치를 리베이스해 주세요
- 브랜치가 정상적으로 빌드되는지 확인해 주세요
- 모든 테스트가 통과하는지 다시 확인해 주세요
- 디버그 코드나 콘솔 로그가 없는지 변경사항을 확인해 주세요
7. **Pull Request 설명**
- 변경 내용을 명확하게 설명해 주세요
- 변경사항을 테스트하는 방법을 포함해 주세요
- 호환되지 않는 변경 사항이 있다면 목록으로 작성해주세요
- UI 변경이 있는 경우, 스크린샷을 추가해 주세요
## 기여 동의서
Pull Request를 제출함으로써, 귀하의 기여가 프로젝트와 동일한 라이선스([Apache 2.0](/LICENSE)) 에 따라 제공됨에 동의하는 것입니다.
기억하세요: Cline에 기여하는 것은 코드를 작성하는 것뿐만 아니라, AI 지원 개발의 미래를 형성하는 커뮤니티의 일원이 되는 것입니다. 함께 멋진 것을 만들어봅시다! 🚀
+172
View File
@@ -0,0 +1,172 @@
# Cline - 최고의 OpenRouter
<p align="center">
<img src="https://media.githubusercontent.com/media/cline/cline/main/assets/docs/demo.gif" width="100%" />
</p>
<div align="center">
<table>
<tbody>
<td align="center">
<a href="https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev" target="_blank"><strong>VS Marketplace에서 다운로드</strong></a>
</td>
<td align="center">
<a href="https://discord.gg/cline" target="_blank"><strong>Discord</strong></a>
</td>
<td align="center">
<a href="https://www.reddit.com/r/cline/" target="_blank"><strong>r/cline</strong></a>
</td>
<td align="center">
<a href="https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop" target="_blank"><strong>기능 요청</strong></a>
</td>
<td align="center">
<a href="https://cline.bot/join-us" target="_blank"><strong>채용 정보</strong></a>
</td>
</tbody>
</table>
</div>
Cline을 만나보세요, **CLI** 및 **에디터**를 활용할 수 있는 AI 어시스턴트입니다.
[Claude 3.7 Sonnet의 에이전트형 코딩 기능](https://www.anthropic.com/claude/sonnet) 덕분에, Cline은 복잡한 소프트웨어 개발 작업을 단계별로 처리할 수 있습니다. 파일 생성과 편집, 대규모 프로젝트 탐색, 브라우저 사용, 터미널 명령 실행(권한 허가 필요) 등의 도구를 사용하여 단순 코드 완성이나 기술 지원을 넘어서는 도움을 제공합니다. Cline은 Model Context Protocol(MCP)를 사용하여 새로운 도구를 만들고 자신의 기능을 확장할 수도 있습니다. 자율적인 AI 스크립트는 일반적으로 샌드박스 환경에서 실행되지만, 이 확장 프로그램은 모든 파일 변경 및 터미널 명령을 승인할 수 있는 사람이 개입가능한 GUI를 제공하여, 에이전트형 AI의 잠재력을 보다 안전하고 쉽게 탐색할 수 있도록 합니다.
1. 작업을 입력하고, 목업을 기능하는 앱으로 변환하거나 스크린샷으로 버그를 수정합니다.
2. Cline은 파일 구조와 소스코드 AST의 분석, 정규식 검색 실행, 관련 파일 읽기부터 시작하여 기존 프로젝트를 파악합니다. 또한, 어떤 정보를 컨텍스트에 추가할지를 신중하게 관리하여, 대규모 복잡한 프로젝트에서도 컨텍스트 윈도우를 과부하시키지 않으면서도 효과적인 지원을 제공합니다.
3. Cline이 필요한 정보를 얻은 후 다음과 같은 작업을 할 수 있습니다:
- 파일 생성과 편집 + 린터/컴파일러 오류 모니터링을 수행하여 누락된 임포트나 구문 오류 등의 문제를 자동으로 수정합니다.
- 터미널에서 명령을 직접 실행하고 작업 중에 출력을 모니터링합니다. 이를 통해 파일 편집 후 개발 서버의 문제에 대응할 수 있습니다.
- 웹 개발 작업에서는 헤드리스 브라우저로 사이트를 실행하고, 클릭, 입력, 스크롤, 스크린샷과 콘솔 로그 캡처를 수행하여 런타임 오류나 시각적 버그를 수정합니다.
4. 작업이 완료되면 Cline은 `open -a "Google Chrome" index.html`과 같은 터미널 명령을 제공하여 버튼 클릭 한 번으로 결과를 확인할 수 있도록 합니다.
> [!TIP]
> `CMD/CTRL + Shift + P` 단축키를 사용하여 명령 팔레트를 열고 "Cline: Open In New Tab"을 입력하여 에디터의 탭으로 확장 프로그램을 엽니다. 이를 통해 파일 탐색기와 병행하여 Cline을 사용하고 워크스페이스의 변경을 더 명확하게 확인할 수 있습니다.
---
<img align="right" width="340" src="https://github.com/user-attachments/assets/3cf21e04-7ce9-4d22-a7b9-ba2c595e88a4">
### 어떤 API나 모델이든 사용 가능
Cline은 OpenRouter, Anthropic, OpenAI, Google Gemini, AWS Bedrock, Azure, GCP Vertex 등의 API 제공자를 지원합니다. 또한 OpenAI 호환 API를 설정하거나 LM Studio/Ollama를 통해 로컬 모델을 사용할 수도 있습니다. OpenRouter를 사용하는 경우, 확장 프로그램에서 최신 모델 목록을 가져와 바로 최신 모델을 사용할 수 있게 합니다.
또한, Cline은 전체 작업 루프와 개별 요청별로 토큰 사용량과 API 비용을 추적하여, 진행 중인 작업의 비용을 실시간으로 확인할 수 있도록 도와줍니다.
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="left" width="370" src="https://github.com/user-attachments/assets/81be79a8-1fdb-4028-9129-5fe055e01e76">
### 터미널에서 명령 실행
VSCode v1.93의 새로운 [셸 통합 업데이트](https://code.visualstudio.com/updates/v1_93#_terminal-shell-integration-api) 덕분에, Cline은 터미널에서 명령을 직접 실행하고 출력을 받을 수 있습니다. 이를 통해 패키지 설치나 빌드 스크립트 실행부터 애플리케이션 배포, 데이터베이스 관리, 테스트 실행까지 광범위한 작업을 수행할 수 있습니다. Cline은 개발 환경과 도구 체인에 맞추어 정확하게 작업을 실행합니다.
개발 서버와 같은 오래 실행되는 프로세스의 경우, "실행 중 계속"(Proceed While Running) 버튼을 사용하여 명령이 백그라운드에서 실행되는 동안 Cline이 작업을 계속할 수 있게 합니다. 작업이 진행되는 동안 Cline은 새로운 터미널 출력을 실시간으로 확인하여, 파일 편집 시 발생하는 컴파일 오류와 같은 문제에 즉시 대응할 수 있습니다.
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="right" width="400" src="https://github.com/user-attachments/assets/c5977833-d9b8-491e-90f9-05f9cd38c588">
### 파일 생성과 편집
Cline은 에디터 내에서 파일을 생성 및 편집하고 변경의 Diff 뷰로 표시합니다. Diff 뷰 에디터에서 Cline의 변경을 직접 편집하거나 되돌릴 수 있으며, 채팅에서 피드백을 제공하여 만족할 때까지 개선 요청할 수 있습니다. Cline은 린터/컴파일러 오류(누락된 임포트, 구문 오류 등)도 모니터링하고 발생한 문제를 자동으로 수정합니다.
Cline에 의한 모든 변경은 파일의 타임라인에 기록되어 필요할 때 변경을 추적하고 되돌릴 수 있는 간단한 방법을 제공합니다.
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="left" width="370" src="https://github.com/user-attachments/assets/bc2e85ba-dfeb-4fe6-9942-7cfc4703cbe5">
### 브라우저 사용
Claude 3.5 Sonnet의 새로운 [컴퓨터 사용](https://www.anthropic.com/news/3-5-models-and-computer-use) 기능으로 인해, Cline은 브라우저를 실행하고 요소를 클릭하고 텍스트를 입력하고 스크롤하며 각 단계에서 스크린샷과 콘솔 로그를 캡처할 수 있습니다. 이를 통해 인터랙티브한 디버깅, 엔드투엔드 테스트, 심지어 일반적인 웹 탐색까지 가능해집니다. 이로 인해 오류 로그를 수동으로 복사 & 붙여넣기 할 필요 없이 시각적 버그나 런타임 문제를 자율적으로 수정할 수 있습니다.
Cline에게 "앱을 테스트해줘"라고 요청하면, `npm run dev`와 같은 명령을 실행하고 로컬에서 실행 중인 개발 서버를 브라우저에서 실행하여 일련의 테스트를 수행하고 모든 것이 정상적으로 작동하는지 확인합니다. [데모는 여기를 참조하세요.](https://x.com/sdrzn/status/1850880547825823989)
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="right" width="350" src="https://github.com/user-attachments/assets/ac0efa14-5c1f-4c26-a42d-9d7c56f5fadd">
### "도구를 추가 해주세요."
Cline은 [Model Context Protocol](https://github.com/modelcontextprotocol)을 활용하여 커스텀 도구를 생성하고 기능을 확장할 수 있습니다. 기존의 [커뮤니티 서버](https://github.com/modelcontextprotocol/servers)를 사용할 수도 있지만, Cline은 사용자의 워크플로우에 최적화된 도구를 직접 제작하고 설치할 수도 있습니다. "~ 도구를 추가해주세요."라고 요청만 하면, Cline은 새로운 MCP 서버 생성부터 확장 프로그램 내 설치까지 모두 자동으로 처리합니다. 이러한 커스텀 도구는 Cline의 툴키트의 일부가 되어 향후 작업에서 사용할 수 있게 됩니다.
- "Jira 티켓을 가져오는 도구를 추가해주세요": 티켓 AC를 가져와 Cline에게 작업을 요청
- "AWS EC2를 관리하는 도구를 추가해주세요": 서버 메트릭을 확인하고 인스턴스를 확장 또는 축소
- "최신 PagerDuty 인시던트를 가져오는 도구를 추가해주세요": 최신 장애 정보를 가져와 Cline에게 버그 수정 요청
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="left" width="360" src="https://github.com/user-attachments/assets/7fdf41e6-281a-4b4b-ac19-020b838b6970">
### 컨텍스트 추가
**`@url`:** URL을 붙여넣으면 확장이 해당 페이지를 가져와 Markdown으로 변환합니다. 최신 문서를 Cline에게 제공할 때 유용합니다.
**`@problems`:** Cline이 수정할 워크스페이스 오류와 경고(Problems' panel)를 추가합니다.
**`@file`:** 파일의 내용을 추가하여, 파일을 읽는 데 API 요청을 허비하지 않고도 Cline이 접근할 수 있도록 합니다. (+ 파일 검색 가능)
**`@folder`:** 폴더 내 모든 파일을 한 번에 추가하여 워크플로우를 더욱 빠르게 진행할 수 있습니다.
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
<img align="right" width="350" src="https://github.com/user-attachments/assets/140c8606-d3bf-41b9-9a1f-4dbf0d4c90cb">
### 체크포인트: 비교 및 복원
Cline이 작업을 진행하는 동안 확장 프로그램은 각 단계에서 워크스페이스의 스냅샷을 저장합니다. “Compare” 버튼을 사용하여 스냅샷과 현재 워크스페이스의 차이를 확인하고, “Restore” 버튼을 사용하여 해당 시점으로 롤백할 수 있습니다.
예를 들어, 로컬 웹 서버에서 작업 중일 때 “Restore Workspace Only”을 사용하여 서로 다른 버전의 앱을 신속하게 테스트하고, “Restore Task and Workspace”을 사용하여 계속 진행할 버전을 찾을 수 있습니다. 이를 통해 진행 상황을 잃지 않고 안전하게 다양한 접근 방식을 실험할 수 있습니다.
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
## 기여
프로젝트에 기여하려면, [기여 가이드](CONTRIBUTING.md)에서 기본 사항을 익히세요. 또한, [Discord](https://discord.gg/cline)에 참여하여 `#contributors` 채널에서 다른 기여자들과 이야기할 수 있습니다. 풀타임 직업을 찾고 있다면, [채용 페이지](https://cline.bot/join-us)에서 열려있는 포지션을 확인하세요.
<details>
<summary>로컬 개발 방법</summary>
1. 리포지토리를 클론합니다 _(Requires [git-lfs](https://git-lfs.com/))_
```bash
git clone https://github.com/cline/cline.git
```
2. 프로젝트를 VSCode에서 엽니다:
```bash
code cline
```
3. 확장 프로그램과 webview-gui의 필요한 의존성을 설치합니다:
```bash
npm run install:all
```
4. `F5`를 눌러(또는 `Run`->`Start Debugging`), 확장 프로그램이 로드된 새로운 VSCode 창을 엽니다. (프로젝트 빌드에 문제가 있는 경우, [esbuild problem matchers extension](https://marketplace.visualstudio.com/items?itemName=connor4312.esbuild-problem-matchers)을 설치해야 할 수도 있습니다.)
</details>
<details>
<summary>Pull Request 생성 방법</summary>
1. PR을 만들기 전, 변경 사항을 기록하는 changeset 항목을 생성:
```bash
npm run changeset
```
이후 프롬프트에서 다음 정보를 입력하세요:
- 변경 유형 (major, minor, patch)
- `major` → 호환되지 않는 변경 (1.0.0 → 2.0.0)
- `minor` → 새로운 기능 추가 (1.0.0 → 1.1.0)
- `patch` → 버그 수정 (1.0.0 → 1.0.1)
- 변경 사항 설명 입력
2. 변경 사항과 생성된 `.changeset` 파일을 커밋 후 브랜치를 푸시하고 GitHub에서 PR을 생성하세요.
3. 브랜치를 푸시하고 GitHub에서 PR을 생성하세요. CI가 다음과 같은 작업을 수행합니다:
- 테스트 및 코드 검증 실행
- Changesetbot이 버전 변경 영향을 보여주는 코멘트를 생성
- 브랜치가 메인에 머지되면, Changesetbot이 버전 패키지 PR을 생성
- 버전 패키지 PR이 머지되면, 새로운 릴리즈가 게시됨
</details>
## 라이센스
[Apache 2.0 © 2025 Cline Bot Inc.](/LICENSE)
+1 -1
View File
@@ -158,4 +158,4 @@ Para contribuir com o projeto, comece com nosso [Guia de Contribuição](CONTRIB
## Licença
[Apache 2.0 © 2024 Cline Bot Inc.](./LICENSE)
[Apache 2.0 © 2025 Cline Bot Inc.](./LICENSE)
+1 -1
View File
@@ -158,5 +158,5 @@ Cline 所做的所有更改都会记录在你的文件时间轴中,提供了
## 许可证
[Apache 2.0 © 2024 Cline Bot Inc.](./LICENSE)
[Apache 2.0 © 2025 Cline Bot Inc.](./LICENSE)
+1 -1
View File
@@ -158,4 +158,4 @@ Cline 所做的所有更改都會記錄在你的文件時間軸中,提供了
## 許可證
[Apache 2.0 © 2024 Cline Bot Inc.](./LICENSE)
[Apache 2.0 © 2025 Cline Bot Inc.](./LICENSE)
+2692 -218
View File
File diff suppressed because it is too large Load Diff
+11 -4
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.6.1",
"version": "3.7.1",
"icon": "assets/icons/icon.png",
"galleryBanner": {
"color": "#617A91",
@@ -160,7 +160,7 @@
"cline.enableCheckpoints": {
"type": "boolean",
"default": true,
"description": "Enables extension to save checkpoints of workspace throughout the task."
"description": "Enables extension to save checkpoints of workspace throughout the task. Uses git under the hood which may not work well with large workspaces."
},
"cline.disableBrowserTool": {
"type": "boolean",
@@ -211,6 +211,11 @@
"type": "boolean",
"default": true,
"description": "Controls whether the MCP Marketplace is enabled."
},
"cline.editor.skipDiffAnimation": {
"type": "boolean",
"default": false,
"description": "Skip the animation when applying diffs in the editor. Useful for faster updates, especially with remote repositories."
}
}
}
@@ -267,16 +272,17 @@
"@anthropic-ai/bedrock-sdk": "^0.12.4",
"@anthropic-ai/sdk": "^0.37.0",
"@anthropic-ai/vertex-sdk": "^0.6.4",
"@aws-sdk/client-bedrock-runtime": "^3.758.0",
"@google-cloud/vertexai": "^1.9.3",
"@google/generative-ai": "^0.18.0",
"@mistralai/mistralai": "^1.5.0",
"@modelcontextprotocol/sdk": "^1.0.1",
"@modelcontextprotocol/sdk": "^1.7.0",
"@types/clone-deep": "^4.0.4",
"@types/get-folder-size": "^3.0.4",
"@types/pdf-parse": "^1.1.4",
"@types/turndown": "^5.0.5",
"@vscode/codicons": "^0.0.36",
"axios": "^1.7.4",
"axios": "^1.8.2",
"cheerio": "^1.0.0",
"chokidar": "^4.0.1",
"clone-deep": "^4.0.1",
@@ -296,6 +302,7 @@
"open-graph-scraper": "^6.9.0",
"openai": "^4.83.0",
"os-name": "^6.0.0",
"p-timeout": "^6.1.4",
"p-wait-for": "^5.0.2",
"pdf-parse": "^1.1.1",
"posthog-node": "^4.8.1",
+5 -1
View File
@@ -9,7 +9,7 @@ import { OllamaHandler } from "./providers/ollama"
import { LmStudioHandler } from "./providers/lmstudio"
import { GeminiHandler } from "./providers/gemini"
import { OpenAiNativeHandler } from "./providers/openai-native"
import { ApiStream } from "./transform/stream"
import { ApiStream, ApiStreamUsageChunk } from "./transform/stream"
import { DeepSeekHandler } from "./providers/deepseek"
import { RequestyHandler } from "./providers/requesty"
import { TogetherHandler } from "./providers/together"
@@ -20,10 +20,12 @@ import { ClineHandler } from "./providers/cline"
import { LiteLlmHandler } from "./providers/litellm"
import { AskSageHandler } from "./providers/asksage"
import { XAIHandler } from "./providers/xai"
import { SambanovaHandler } from "./providers/sambanova"
export interface ApiHandler {
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream
getModel(): { id: string; info: ModelInfo }
getApiStreamUsage?(): Promise<ApiStreamUsageChunk | undefined>
}
export interface SingleCompletionHandler {
@@ -71,6 +73,8 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
return new AskSageHandler(options)
case "xai":
return new XAIHandler(options)
case "sambanova":
return new SambanovaHandler(options)
default:
return new AnthropicHandler(options)
}
+273 -13
View File
@@ -2,9 +2,12 @@ import AnthropicBedrock from "@anthropic-ai/bedrock-sdk"
import { Anthropic } from "@anthropic-ai/sdk"
import { withRetry } from "../retry"
import { ApiHandler } from "../"
import { convertToR1Format } from "../transform/r1-format"
import { ApiHandlerOptions, bedrockDefaultModelId, BedrockModelId, bedrockModels, ModelInfo } from "../../shared/api"
import { calculateApiCostOpenAI } from "../../utils/cost"
import { ApiStream } from "../transform/stream"
import { fromNodeProviderChain } from "@aws-sdk/credential-providers"
import { BedrockRuntimeClient, InvokeModelWithResponseStreamCommand } from "@aws-sdk/client-bedrock-runtime"
// https://docs.anthropic.com/en/api/claude-on-amazon-bedrock
export class AwsBedrockHandler implements ApiHandler {
@@ -18,19 +21,25 @@ export class AwsBedrockHandler implements ApiHandler {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
// cross region inference requires prefixing the model id with the region
let modelId = await this.getModelId()
const model = this.getModel()
// Check if this is a Deepseek model
if (modelId.includes("deepseek")) {
yield* this.createDeepseekMessage(systemPrompt, messages, modelId, model)
return
}
let budget_tokens = this.options.thinkingBudgetTokens || 0
const reasoningOn = modelId.includes("3-7") && budget_tokens !== 0 ? true : false
// Get model info and message indices for caching
const model = this.getModel()
const userMsgIndices = messages.reduce((acc, msg, index) => (msg.role === "user" ? [...acc, index] : acc), [] as number[])
const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
// Create anthropic client, using sessions created or renewed after this handler's
// initialization, and allowing for session renewal if necessary as well
const client = await this.getClient()
const client = await this.getAnthropicClient()
const stream = await client.messages.create({
model: modelId,
@@ -99,6 +108,20 @@ export class AwsBedrockHandler implements ApiHandler {
break
case "content_block_start":
switch (chunk.content_block.type) {
case "thinking":
yield {
type: "reasoning",
reasoning: chunk.content_block.thinking || "",
}
break
case "redacted_thinking":
// Handle redacted thinking blocks - we still mark it as reasoning
// but note that the content is encrypted
yield {
type: "reasoning",
reasoning: "[Redacted thinking block]",
}
break
case "text":
if (chunk.index > 0) {
yield {
@@ -115,6 +138,12 @@ export class AwsBedrockHandler implements ApiHandler {
break
case "content_block_delta":
switch (chunk.delta.type) {
case "thinking_delta":
yield {
type: "reasoning",
reasoning: chunk.delta.thinking,
}
break
case "text_delta":
yield {
type: "text",
@@ -139,12 +168,21 @@ export class AwsBedrockHandler implements ApiHandler {
}
}
private async getClient(): Promise<AnthropicBedrock> {
// Create AWS credentials by executing a an AWS provider chain exactly as the
// Anthropic SDK does it, by wrapping the default chain into a temporary process
// environment.
// Default AWS region
private static readonly DEFAULT_REGION = "us-east-1"
/**
* Gets AWS credentials using the provider chain
* Centralizes credential retrieval logic for all AWS services
*/
private async getAwsCredentials(): Promise<{
accessKeyId: string
secretAccessKey: string
sessionToken?: string
}> {
// Create AWS credentials by executing an AWS provider chain
const providerChain = fromNodeProviderChain()
const credentials = await AwsBedrockHandler.withTempEnv(
return await AwsBedrockHandler.withTempEnv(
() => {
AwsBedrockHandler.setEnv("AWS_REGION", this.options.awsRegion)
AwsBedrockHandler.setEnv("AWS_ACCESS_KEY_ID", this.options.awsAccessKey)
@@ -154,23 +192,54 @@ export class AwsBedrockHandler implements ApiHandler {
},
() => providerChain(),
)
}
/**
* Gets the AWS region to use, with fallback to default
*/
private getRegion(): string {
return this.options.awsRegion || AwsBedrockHandler.DEFAULT_REGION
}
/**
* Creates a BedrockRuntimeClient with the appropriate credentials
*/
private async getBedrockClient(): Promise<BedrockRuntimeClient> {
const credentials = await this.getAwsCredentials()
return new BedrockRuntimeClient({
region: this.getRegion(),
credentials: {
accessKeyId: credentials.accessKeyId,
secretAccessKey: credentials.secretAccessKey,
sessionToken: credentials.sessionToken,
},
...(this.options.awsBedrockEndpoint && { endpoint: this.options.awsBedrockEndpoint }),
})
}
/**
* Creates an AnthropicBedrock client with the appropriate credentials
*/
private async getAnthropicClient(): Promise<AnthropicBedrock> {
const credentials = await this.getAwsCredentials()
// Return an AnthropicBedrock client with the resolved/assumed credentials.
//
// When AnthropicBedrock creates its AWS client, the chain will execute very
// fast as the access/secret keys will already be already provided, and have
// a higher precedence than the profiles.
return new AnthropicBedrock({
awsAccessKey: credentials.accessKeyId,
awsSecretKey: credentials.secretAccessKey,
awsSessionToken: credentials.sessionToken,
awsRegion: this.options.awsRegion || "us-east-1",
awsRegion: this.getRegion(),
...(this.options.awsBedrockEndpoint && { baseURL: this.options.awsBedrockEndpoint }),
})
}
/**
* Gets the appropriate model ID, accounting for cross-region inference if enabled
*/
async getModelId(): Promise<string> {
if (this.options.awsUseCrossRegionInference) {
let regionPrefix = (this.options.awsRegion || "").slice(0, 3)
let regionPrefix = this.getRegion().slice(0, 3)
switch (regionPrefix) {
case "us-":
return `us.${this.getModel().id}`
@@ -202,4 +271,195 @@ export class AwsBedrockHandler implements ApiHandler {
process.env[key] = value
}
}
/**
* Creates a message using the Deepseek R1 model through AWS Bedrock
*/
private async *createDeepseekMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
modelId: string,
model: { id: BedrockModelId; info: ModelInfo },
): ApiStream {
// Get Bedrock client with proper credentials
const client = await this.getBedrockClient()
// Format prompt for DeepSeek R1 according to documentation
const formattedPrompt = this.formatDeepseekR1Prompt(systemPrompt, messages)
// Prepare the request based on DeepSeek R1's expected format
const command = new InvokeModelWithResponseStreamCommand({
modelId: modelId,
contentType: "application/json",
accept: "application/json",
body: JSON.stringify({
prompt: formattedPrompt,
max_tokens: model.info.maxTokens || 8000,
temperature: 0,
}),
})
// Track token usage
const inputTokenEstimate = this.estimateInputTokens(systemPrompt, messages)
let outputTokens = 0
let isFirstChunk = true
let accumulatedTokens = 0
const TOKEN_REPORT_THRESHOLD = 100 // Report usage after accumulating this many tokens
// Execute the streaming request
const response = await client.send(command)
if (response.body) {
for await (const chunk of response.body) {
if (chunk.chunk?.bytes) {
try {
// Parse the response chunk
const decodedChunk = new TextDecoder().decode(chunk.chunk.bytes)
const parsedChunk = JSON.parse(decodedChunk)
// Report usage on first chunk
if (isFirstChunk) {
isFirstChunk = false
const totalCost = calculateApiCostOpenAI(model.info, inputTokenEstimate, 0, 0, 0)
yield {
type: "usage",
inputTokens: inputTokenEstimate,
outputTokens: 0,
totalCost: totalCost,
}
}
// Handle DeepSeek R1 response format
if (parsedChunk.choices && parsedChunk.choices.length > 0) {
// For non-streaming response (full response)
const text = parsedChunk.choices[0].text
if (text) {
const chunkTokens = this.estimateTokenCount(text)
outputTokens += chunkTokens
accumulatedTokens += chunkTokens
yield {
type: "text",
text: text,
}
if (accumulatedTokens >= TOKEN_REPORT_THRESHOLD) {
const totalCost = calculateApiCostOpenAI(model.info, 0, accumulatedTokens, 0, 0)
yield {
type: "usage",
inputTokens: 0,
outputTokens: accumulatedTokens,
totalCost: totalCost,
}
accumulatedTokens = 0
}
}
} else if (parsedChunk.delta?.text) {
// For streaming response (delta updates)
const text = parsedChunk.delta.text
const chunkTokens = this.estimateTokenCount(text)
outputTokens += chunkTokens
accumulatedTokens += chunkTokens
yield {
type: "text",
text: text,
}
// Report aggregated token usage only when threshold is reached
if (accumulatedTokens >= TOKEN_REPORT_THRESHOLD) {
const totalCost = calculateApiCostOpenAI(model.info, 0, accumulatedTokens, 0, 0)
yield {
type: "usage",
inputTokens: 0,
outputTokens: accumulatedTokens,
totalCost: totalCost,
}
accumulatedTokens = 0
}
}
} catch (error) {
console.error("Error parsing Deepseek response chunk:", error)
// Propagate the error by yielding a text response with error information
yield {
type: "text",
text: `[ERROR] Failed to parse Deepseek response: ${error instanceof Error ? error.message : String(error)}`,
}
}
}
}
// Report any remaining accumulated tokens at the end of the stream
if (accumulatedTokens > 0) {
const totalCost = calculateApiCostOpenAI(model.info, 0, accumulatedTokens, 0, 0)
yield {
type: "usage",
inputTokens: 0,
outputTokens: accumulatedTokens,
totalCost: totalCost,
}
}
// Add final total cost calculation that includes both input and output tokens
const finalTotalCost = calculateApiCostOpenAI(model.info, inputTokenEstimate, outputTokens, 0, 0)
yield {
type: "usage",
inputTokens: inputTokenEstimate,
outputTokens: outputTokens,
totalCost: finalTotalCost,
}
}
}
/**
* Formats prompt for DeepSeek R1 model according to documentation
* First uses convertToR1Format to merge consecutive messages with the same role,
* then converts to the string format that DeepSeek R1 expects
*/
private formatDeepseekR1Prompt(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): string {
// First use convertToR1Format to merge consecutive messages with the same role
const r1Messages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
// Then convert to the special string format expected by DeepSeek R1
let combinedContent = ""
for (const message of r1Messages) {
let content = ""
if (message.content) {
if (typeof message.content === "string") {
content = message.content
} else {
// Extract text content from message parts
content = message.content
.filter((part) => part.type === "text")
.map((part) => part.text)
.join("\n")
}
}
combinedContent += message.role === "user" ? "User: " + content + "\n" : "Assistant: " + content + "\n"
}
// Format according to DeepSeek R1's expected prompt format
return `<begin▁of▁sentence><User>${combinedContent}<Assistant><think>\n`
}
/**
* Estimates token count based on text length (approximate)
* Note: This is a rough estimation, as the actual token count depends on the tokenizer
*/
private estimateInputTokens(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): number {
// For Deepseek R1, we estimate the token count of the formatted prompt
// The formatted prompt includes special tokens and consistent formatting
const formattedPrompt = this.formatDeepseekR1Prompt(systemPrompt, messages)
return Math.ceil(formattedPrompt.length / 4)
}
/**
* Estimates token count for a text string
*/
private estimateTokenCount(text: string): number {
// Approximate 4 characters per token
return Math.ceil(text.length / 4)
}
}
+67 -23
View File
@@ -2,13 +2,15 @@ import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { ApiHandler } from "../"
import { ApiHandlerOptions, ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "../../shared/api"
import { streamOpenRouterFormatRequest } from "../transform/openrouter-stream"
import { ApiStream } from "../transform/stream"
import { createOpenRouterStream } from "../transform/openrouter-stream"
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
import axios from "axios"
import { OpenRouterErrorResponse } from "./types"
export class ClineHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
lastGenerationId?: string
constructor(options: ApiHandlerOptions) {
this.options = options
@@ -19,36 +21,78 @@ export class ClineHandler implements ApiHandler {
}
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const model = this.getModel()
const genId = yield* streamOpenRouterFormatRequest(
this.lastGenerationId = undefined
const stream = await createOpenRouterStream(
this.client,
systemPrompt,
messages,
model,
this.getModel(),
this.options.o3MiniReasoningEffort,
this.options.thinkingBudgetTokens,
)
try {
const response = await axios.get(`https://api.cline.bot/v1/generation?id=${genId}`, {
headers: {
Authorization: `Bearer ${this.options.clineApiKey}`,
},
timeout: 5_000, // this request hangs sometimes
})
const generation = response.data
console.log("cline generation details:", generation)
yield {
type: "usage",
inputTokens: generation?.native_tokens_prompt || 0,
outputTokens: generation?.native_tokens_completion || 0,
totalCost: generation?.total_cost || 0,
for await (const chunk of stream) {
// openrouter returns an error object instead of the openai sdk throwing an error
if ("error" in chunk) {
const error = chunk.error as OpenRouterErrorResponse["error"]
console.error(`Cline API Error: ${error?.code} - ${error?.message}`)
// Include metadata in the error message if available
const metadataStr = error.metadata ? `\nMetadata: ${JSON.stringify(error.metadata, null, 2)}` : ""
throw new Error(`Cline API Error ${error.code}: ${error.message}${metadataStr}`)
}
if (!this.lastGenerationId && chunk.id) {
this.lastGenerationId = chunk.id
}
const delta = chunk.choices[0]?.delta
if (delta?.content) {
yield {
type: "text",
text: delta.content,
}
}
// Reasoning tokens are returned separately from the content
if ("reasoning" in delta && delta.reasoning) {
yield {
type: "reasoning",
// @ts-ignore-next-line
reasoning: delta.reasoning,
}
}
} catch (error) {
// ignore if fails
console.error("Error fetching cline generation details:", error)
}
const apiStreamUsage = await this.getApiStreamUsage()
if (apiStreamUsage) {
yield apiStreamUsage
}
}
async getApiStreamUsage(): Promise<ApiStreamUsageChunk | undefined> {
if (this.lastGenerationId) {
try {
const response = await axios.get(`https://api.cline.bot/v1/generation?id=${this.lastGenerationId}`, {
headers: {
Authorization: `Bearer ${this.options.clineApiKey}`,
},
timeout: 15_000, // this request hangs sometimes
})
const generation = response.data
return {
type: "usage",
inputTokens: generation?.native_tokens_prompt || 0,
outputTokens: generation?.native_tokens_completion || 0,
totalCost: generation?.total_cost || 0,
}
} catch (error) {
// ignore if fails
console.error("Error fetching cline generation details:", error)
}
}
return undefined
}
getModel(): { id: string; info: ModelInfo } {
+8 -1
View File
@@ -23,11 +23,18 @@ export class LiteLlmHandler implements ApiHandler {
role: "system",
content: systemPrompt,
}
const modelId = this.options.liteLlmModelId || liteLlmDefaultModelId
const isOminiModel = modelId.includes("o1-mini") || modelId.includes("o3-mini")
let temperature: number | undefined = 0
if (isOminiModel) {
temperature = undefined // does not support temperature
}
const stream = await this.client.chat.completions.create({
model: this.options.liteLlmModelId || liteLlmDefaultModelId,
messages: [systemMessage, ...formattedMessages],
temperature: 0,
temperature,
stream: true,
stream_options: { include_usage: true },
})
+3 -2
View File
@@ -15,7 +15,8 @@ export class OpenAiHandler implements ApiHandler {
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
if (this.options.openAiBaseUrl?.toLowerCase().includes("azure.com")) {
// 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.client = new AzureOpenAI({
baseURL: this.options.openAiBaseUrl,
apiKey: this.options.openAiApiKey,
@@ -39,7 +40,7 @@ export class OpenAiHandler implements ApiHandler {
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
let temperature: number | undefined = 0
let temperature: number | undefined = this.options.openAiModelInfo?.temperature ?? openAiModelInfoSaneDefaults.temperature
let reasoningEffort: ChatCompletionReasoningEffort | undefined = undefined
if (isDeepseekReasoner) {
+52 -11
View File
@@ -2,17 +2,17 @@ import { Anthropic } from "@anthropic-ai/sdk"
import axios from "axios"
import delay from "delay"
import OpenAI from "openai"
import { withRetry } from "../retry"
import { ApiHandler } from "../"
import { ApiHandlerOptions, ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "../../shared/api"
import { streamOpenRouterFormatRequest } from "../transform/openrouter-stream"
import { ApiStream } from "../transform/stream"
import { convertToR1Format } from "../transform/r1-format"
import { withRetry } from "../retry"
import { createOpenRouterStream } from "../transform/openrouter-stream"
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
import { OpenRouterErrorResponse } from "./types"
export class OpenRouterHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
lastGenerationId?: string
constructor(options: ApiHandlerOptions) {
this.options = options
@@ -28,23 +28,63 @@ export class OpenRouterHandler implements ApiHandler {
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const model = this.getModel()
const genId = yield* streamOpenRouterFormatRequest(
this.lastGenerationId = undefined
const stream = await createOpenRouterStream(
this.client,
systemPrompt,
messages,
model,
this.getModel(),
this.options.o3MiniReasoningEffort,
this.options.thinkingBudgetTokens,
)
if (genId) {
for await (const chunk of stream) {
// openrouter returns an error object instead of the openai sdk throwing an error
if ("error" in chunk) {
const error = chunk.error as OpenRouterErrorResponse["error"]
console.error(`OpenRouter API Error: ${error?.code} - ${error?.message}`)
// Include metadata in the error message if available
const metadataStr = error.metadata ? `\nMetadata: ${JSON.stringify(error.metadata, null, 2)}` : ""
throw new Error(`OpenRouter API Error ${error.code}: ${error.message}${metadataStr}`)
}
if (!this.lastGenerationId && chunk.id) {
this.lastGenerationId = chunk.id
}
const delta = chunk.choices[0]?.delta
if (delta?.content) {
yield {
type: "text",
text: delta.content,
}
}
// Reasoning tokens are returned separately from the content
if ("reasoning" in delta && delta.reasoning) {
yield {
type: "reasoning",
// @ts-ignore-next-line
reasoning: delta.reasoning,
}
}
}
const apiStreamUsage = await this.getApiStreamUsage()
if (apiStreamUsage) {
yield apiStreamUsage
}
}
async getApiStreamUsage(): Promise<ApiStreamUsageChunk | undefined> {
if (this.lastGenerationId) {
await delay(500) // FIXME: necessary delay to ensure generation endpoint is ready
try {
const generationIterator = this.fetchGenerationDetails(genId)
const generationIterator = this.fetchGenerationDetails(this.lastGenerationId)
const generation = (await generationIterator.next()).value
// console.log("OpenRouter generation details:", generation)
yield {
return {
type: "usage",
// cacheWriteTokens: 0,
// cacheReadTokens: 0,
@@ -58,6 +98,7 @@ export class OpenRouterHandler implements ApiHandler {
console.error("Error fetching OpenRouter generation details:", error)
}
}
return undefined
}
@withRetry({ maxRetries: 4, baseDelay: 250, maxDelay: 1000, retryAllErrors: true })
@@ -68,7 +109,7 @@ export class OpenRouterHandler implements ApiHandler {
headers: {
Authorization: `Bearer ${this.options.openRouterApiKey}`,
},
timeout: 5_000, // this request hangs sometimes
timeout: 15_000, // this request hangs sometimes
})
yield response.data?.data
} catch (error) {
+24 -9
View File
@@ -1,7 +1,16 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { ApiHandler } from "../"
import { ApiHandlerOptions, QwenModelId, ModelInfo, qwenDefaultModelId, qwenModels } from "../../shared/api"
import {
ApiHandlerOptions,
ModelInfo,
mainlandQwenModels,
internationalQwenModels,
mainlandQwenDefaultModelId,
internationalQwenDefaultModelId,
MainlandQwenModelId,
InternationalQwenModelId,
} from "../../shared/api"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { convertToR1Format } from "../transform/r1-format"
@@ -21,15 +30,21 @@ export class QwenHandler implements ApiHandler {
})
}
getModel(): { id: QwenModelId; info: ModelInfo } {
getModel(): { id: MainlandQwenModelId | InternationalQwenModelId; info: ModelInfo } {
const modelId = this.options.apiModelId
if (modelId && modelId in qwenModels) {
const id = modelId as QwenModelId
return { id, info: qwenModels[id] }
}
return {
id: qwenDefaultModelId,
info: qwenModels[qwenDefaultModelId],
// Branch based on API line to let poor typescript know what to do
if (this.options.qwenApiLine === "china") {
return {
id: (modelId as MainlandQwenModelId) ?? mainlandQwenDefaultModelId,
info: mainlandQwenModels[modelId as MainlandQwenModelId] ?? mainlandQwenModels[mainlandQwenDefaultModelId],
}
} else {
return {
id: (modelId as InternationalQwenModelId) ?? internationalQwenDefaultModelId,
info:
internationalQwenModels[modelId as InternationalQwenModelId] ??
internationalQwenModels[internationalQwenDefaultModelId],
}
}
}
+75
View File
@@ -0,0 +1,75 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { withRetry } from "../retry"
import { ApiHandlerOptions, ModelInfo, SambanovaModelId, sambanovaDefaultModelId, sambanovaModels } from "../../shared/api"
import { ApiHandler } from "../index"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { convertToR1Format } from "../transform/r1-format"
export class SambanovaHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://api.sambanova.ai/v1",
apiKey: this.options.sambanovaApiKey,
})
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const model = this.getModel()
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
const modelId = model.id.toLowerCase()
if (modelId.includes("deepseek") || modelId.includes("qwen") || modelId.includes("qwq")) {
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
}
const stream = await this.client.chat.completions.create({
model: this.getModel().id,
messages: openAiMessages,
temperature: 0,
stream: true,
stream_options: { include_usage: true },
})
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
if (delta?.content) {
yield {
type: "text",
text: delta.content,
}
}
if (chunk.usage) {
yield {
type: "usage",
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
}
}
}
}
getModel(): { id: string; info: ModelInfo } {
const modelId = this.options.apiModelId
if (modelId && modelId in sambanovaModels) {
const id = modelId as SambanovaModelId
return { id, info: sambanovaModels[id] }
}
return {
id: sambanovaDefaultModelId,
info: sambanovaModels[sambanovaDefaultModelId],
}
}
}
+21
View File
@@ -162,6 +162,21 @@ export class VertexHandler implements ApiHandler {
break
case "content_block_start":
switch (chunk.content_block.type) {
case "thinking":
yield {
type: "reasoning",
reasoning: chunk.content_block.thinking || "",
}
break
case "redacted_thinking":
// Handle redacted thinking blocks - we still mark it as reasoning
// but note that the content is encrypted
yield {
type: "reasoning",
reasoning: "[Redacted thinking block]",
}
break
case "text":
if (chunk.index > 0) {
yield {
@@ -178,6 +193,12 @@ export class VertexHandler implements ApiHandler {
break
case "content_block_delta":
switch (chunk.delta.type) {
case "thinking_delta":
yield {
type: "reasoning",
reasoning: chunk.delta.thinking,
}
break
case "text_delta":
yield {
type: "text",
+9 -38
View File
@@ -6,14 +6,14 @@ import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { OpenRouterErrorResponse } from "../providers/types"
export async function* streamOpenRouterFormatRequest(
export async function createOpenRouterStream(
client: OpenAI,
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
model: { id: string; info: ModelInfo },
o3MiniReasoningEffort?: string,
thinkingBudgetTokens?: number,
): AsyncGenerator<ApiStreamChunk, string | undefined, unknown> {
) {
// Convert Anthropic messages to OpenAI format
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
@@ -98,7 +98,12 @@ export async function* streamOpenRouterFormatRequest(
let temperature: number | undefined = 0
let topP: number | undefined = undefined
if (model.id.startsWith("deepseek/deepseek-r1") || model.id === "perplexity/sonar-reasoning") {
if (
model.id.startsWith("deepseek/deepseek-r1") ||
model.id === "perplexity/sonar-reasoning" ||
model.id === "qwen/qwq-32b:free" ||
model.id === "qwen/qwq-32b"
) {
// Recommended values from DeepSeek
temperature = 0.7
topP = 0.95
@@ -142,39 +147,5 @@ export async function* streamOpenRouterFormatRequest(
...(reasoning ? { reasoning } : {}),
})
let genId: string | undefined
for await (const chunk of stream) {
// openrouter returns an error object instead of the openai sdk throwing an error
if ("error" in chunk) {
const error = chunk.error as OpenRouterErrorResponse["error"]
console.error(`OpenRouter API Error: ${error?.code} - ${error?.message}`)
// Include metadata in the error message if available
const metadataStr = error.metadata ? `\nMetadata: ${JSON.stringify(error.metadata, null, 2)}` : ""
throw new Error(`OpenRouter API Error ${error.code}: ${error.message}${metadataStr}`)
}
if (!genId && chunk.id) {
genId = chunk.id
}
const delta = chunk.choices[0]?.delta
if (delta?.content) {
yield {
type: "text",
text: delta.content,
}
}
// Reasoning tokens are returned separately from the content
if ("reasoning" in delta && delta.reasoning) {
yield {
type: "reasoning",
// @ts-ignore-next-line
reasoning: delta.reasoning,
}
}
}
return genId
return stream
}
+150 -44
View File
@@ -22,7 +22,7 @@ import { listFiles } from "../services/glob/list-files"
import { regexSearchFiles } from "../services/ripgrep"
import { parseSourceCodeForDefinitionsTopLevel } from "../services/tree-sitter"
import { ApiConfiguration } from "../shared/api"
import { findLast, findLastIndex } from "../shared/array"
import { findLast, findLastIndex, parsePartialArrayString } from "../shared/array"
import { AutoApprovalSettings } from "../shared/AutoApprovalSettings"
import { BrowserSettings } from "../shared/BrowserSettings"
import { ChatSettings } from "../shared/ChatSettings"
@@ -35,8 +35,10 @@ import {
ClineApiReqCancelReason,
ClineApiReqInfo,
ClineAsk,
ClineAskQuestion,
ClineAskUseMcpServer,
ClineMessage,
ClinePlanModeResponse,
ClineSay,
ClineSayBrowserAction,
ClineSayTool,
@@ -46,7 +48,7 @@ import { getApiMetrics } from "../shared/getApiMetrics"
import { HistoryItem } from "../shared/HistoryItem"
import { ClineAskResponse, ClineCheckpointRestore } from "../shared/WebviewMessage"
import { calculateApiCostAnthropic } from "../utils/cost"
import { fileExistsAtPath } from "../utils/fs"
import { fileExistsAtPath, isDirectory } from "../utils/fs"
import { arePathsEqual, getReadablePath } from "../utils/path"
import { fixModelHtmlEscaping, removeInvalidChars } from "../utils/string"
import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from "./assistant-message"
@@ -62,6 +64,7 @@ import { ClineHandler } from "../api/providers/cline"
import { ClineProvider, GlobalFileNames } from "./webview/ClineProvider"
import { DEFAULT_LANGUAGE_SETTINGS, getLanguageKey, LanguageDisplay, LanguageKey } from "../shared/Languages"
import { telemetryService } from "../services/telemetry/TelemetryService"
import pTimeout from "p-timeout"
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
@@ -294,13 +297,12 @@ export class Cline {
break
case "taskAndWorkspace":
case "workspace":
if (!this.checkpointTracker) {
if (!this.checkpointTracker && !this.checkpointTrackerErrorMessage) {
try {
this.checkpointTracker = await CheckpointTracker.create(
this.taskId,
this.providerRef.deref()?.context.globalStorageUri.fsPath,
)
this.checkpointTrackerErrorMessage = undefined
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error"
console.error("Failed to initialize checkpoint tracker:", errorMessage)
@@ -410,13 +412,12 @@ export class Cline {
}
// TODO: handle if this is called from outside original workspace, in which case we need to show user error message we cant show diff outside of workspace?
if (!this.checkpointTracker) {
if (!this.checkpointTracker && !this.checkpointTrackerErrorMessage) {
try {
this.checkpointTracker = await CheckpointTracker.create(
this.taskId,
this.providerRef.deref()?.context.globalStorageUri.fsPath,
)
this.checkpointTrackerErrorMessage = undefined
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error"
console.error("Failed to initialize checkpoint tracker:", errorMessage)
@@ -440,21 +441,30 @@ export class Cline {
try {
if (seeNewChangesSinceLastTaskCompletion) {
// Get last task completed
const lastTaskCompletedMessage = findLast(
const lastTaskCompletedMessageCheckpointHash = findLast(
this.clineMessages.slice(0, messageIndex),
(m) => m.say === "completion_result",
) // ask is only used to relinquish control, its the last say we care about
)?.lastCheckpointHash // ask is only used to relinquish control, its the last say we care about
// if undefined, then we get diff from beginning of git
// if (!lastTaskCompletedMessage) {
// console.error("No previous task completion message found")
// return
// }
// This value *should* always exist
const firstCheckpointMessageCheckpointHash = this.clineMessages.find(
(m) => m.say === "checkpoint_created",
)?.lastCheckpointHash
const previousCheckpointHash = lastTaskCompletedMessageCheckpointHash || firstCheckpointMessageCheckpointHash // either use the diff between the first checkpoint and the task completion, or the diff between the latest two task completions
if (!previousCheckpointHash) {
vscode.window.showErrorMessage("Unexpected error: No checkpoint hash found")
relinquishButton()
return
}
// Get changed files between current state and commit
changedFiles = await this.checkpointTracker?.getDiffSet(
lastTaskCompletedMessage?.lastCheckpointHash, // if undefined, then we get diff from beginning of git history, AKA when the task was started
hash,
)
changedFiles = await this.checkpointTracker?.getDiffSet(previousCheckpointHash, hash)
if (!changedFiles?.length) {
vscode.window.showInformationMessage("No changes found")
relinquishButton()
@@ -517,13 +527,12 @@ export class Cline {
return false
}
if (!this.checkpointTracker) {
if (!this.checkpointTracker && !this.checkpointTrackerErrorMessage) {
try {
this.checkpointTracker = await CheckpointTracker.create(
this.taskId,
this.providerRef.deref()?.context.globalStorageUri.fsPath,
)
this.checkpointTrackerErrorMessage = undefined
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error"
console.error("Failed to initialize checkpoint tracker:", errorMessage)
@@ -535,12 +544,26 @@ export class Cline {
const lastTaskCompletedMessage = findLast(this.clineMessages.slice(0, messageIndex), (m) => m.say === "completion_result")
try {
// Get changed files between current state and commit
const changedFiles = await this.checkpointTracker?.getDiffSet(
lastTaskCompletedMessage?.lastCheckpointHash, // if undefined, then we get diff from beginning of git history, AKA when the task was started
hash,
)
const changedFilesCount = changedFiles?.length || 0
// Get last task completed
const lastTaskCompletedMessageCheckpointHash = lastTaskCompletedMessage?.lastCheckpointHash // ask is only used to relinquish control, its the last say we care about
// if undefined, then we get diff from beginning of git
// if (!lastTaskCompletedMessage) {
// console.error("No previous task completion message found")
// return
// }
// This value *should* always exist
const firstCheckpointMessageCheckpointHash = this.clineMessages.find(
(m) => m.say === "checkpoint_created",
)?.lastCheckpointHash
const previousCheckpointHash = lastTaskCompletedMessageCheckpointHash || firstCheckpointMessageCheckpointHash // either use the diff between the first checkpoint and the task completion, or the diff between the latest two task completions
if (!previousCheckpointHash) {
return false
}
// Get count of changed files between current state and commit
const changedFilesCount = (await this.checkpointTracker?.getDiffCount(previousCheckpointHash, hash)) || 0
if (changedFilesCount > 0) {
return true
}
@@ -1057,6 +1080,7 @@ export class Cline {
this.urlContentFetcher.closeBrowser()
this.browserSession.closeBrowser()
this.clineIgnoreController.dispose()
this.diffViewProvider.disposeAll()
await this.diffViewProvider.revertChanges() // need to await for when we want to make sure directories/files are reverted before re-starting the task from a checkpoint
}
@@ -1272,13 +1296,33 @@ export class Cline {
const clineRulesFilePath = path.resolve(cwd, GlobalFileNames.clineRules)
let clineRulesFileInstructions: string | undefined
if (await fileExistsAtPath(clineRulesFilePath)) {
try {
const ruleFileContent = (await fs.readFile(clineRulesFilePath, "utf8")).trim()
if (ruleFileContent) {
clineRulesFileInstructions = `# .clinerules\n\nThe following is provided by a root-level .clinerules file where the user has specified instructions for this working directory (${cwd.toPosix()})\n\n${ruleFileContent}`
if (await isDirectory(clineRulesFilePath)) {
try {
// Read all files in the .clinerules/ directory.
const ruleFiles = await fs
.readdir(clineRulesFilePath, { withFileTypes: true, recursive: true })
.then((files) => files.filter((file) => file.isFile()))
.then((files) => files.map((file) => path.resolve(file.parentPath, file.name)))
const ruleFileContent = await Promise.all(
ruleFiles.map(async (file) => {
const ruleFilePath = path.resolve(clineRulesFilePath, file)
const ruleFilePathRelative = path.relative(cwd, ruleFilePath)
return `${ruleFilePathRelative}\n` + (await fs.readFile(ruleFilePath, "utf8")).trim()
}),
).then((contents) => contents.join("\n\n"))
clineRulesFileInstructions = `# .clinerules/\n\nThe following is provided by a root-level .clinerules/ directory where the user has specified instructions for this working directory (${cwd.toPosix()})\n\n${ruleFileContent}`
} catch {
console.error(`Failed to read .clinerules directory at ${clineRulesFilePath}`)
}
} else {
try {
const ruleFileContent = (await fs.readFile(clineRulesFilePath, "utf8")).trim()
if (ruleFileContent) {
clineRulesFileInstructions = `# .clinerules\n\nThe following is provided by a root-level .clinerules file where the user has specified instructions for this working directory (${cwd.toPosix()})\n\n${ruleFileContent}`
}
} catch {
console.error(`Failed to read .clinerules file at ${clineRulesFilePath}`)
}
} catch {
console.error(`Failed to read .clinerules file at ${clineRulesFilePath}`)
}
}
@@ -1703,7 +1747,7 @@ export class Cline {
`This is likely because the SEARCH block content doesn't match exactly with what's in the file, or if you used multiple SEARCH/REPLACE blocks they may not have been in the order they appear in the file.\n\n` +
`The file was reverted to its original state:\n\n` +
`<file_content path="${relPath.toPosix()}">\n${this.diffViewProvider.originalContent}\n</file_content>\n\n` +
`Try again with a more precise SEARCH block.\n(If you keep running into this error, you may use the write_to_file tool as a workaround.)`,
`Try again with fewer/more precise SEARCH blocks.\n(If you run into this error two times in a row, you may use the write_to_file tool as a fallback.)`,
),
)
await this.diffViewProvider.revertChanges()
@@ -2677,9 +2721,14 @@ export class Cline {
}
case "ask_followup_question": {
const question: string | undefined = block.params.question
const optionsRaw: string | undefined = block.params.options
const sharedMessage = {
question: removeClosingTag("question", question),
options: parsePartialArrayString(removeClosingTag("options", optionsRaw)),
} satisfies ClineAskQuestion
try {
if (block.partial) {
await this.ask("followup", removeClosingTag("question", question), block.partial).catch(() => {})
await this.ask("followup", JSON.stringify(sharedMessage), block.partial).catch(() => {})
break
} else {
if (!question) {
@@ -2697,8 +2746,25 @@ export class Cline {
})
}
const { text, images } = await this.ask("followup", question, false)
await this.say("user_feedback", text ?? "", images)
const { text, images } = await this.ask("followup", JSON.stringify(sharedMessage), false)
// Check if options contains the text response
if (optionsRaw && text && parsePartialArrayString(optionsRaw).includes(text)) {
// Valid option selected, don't show user message in UI
// Update last followup message with selected option
const lastFollowupMessage = findLast(this.clineMessages, (m) => m.ask === "followup")
if (lastFollowupMessage) {
lastFollowupMessage.text = JSON.stringify({
...sharedMessage,
selected: text,
} satisfies ClineAskQuestion)
await this.saveClineMessages()
}
} else {
// Option not selected, send user feedback
await this.say("user_feedback", text ?? "", images)
}
pushToolResult(formatResponse.toolResult(`<answer>\n${text}\n</answer>`, images))
break
@@ -2711,11 +2777,14 @@ export class Cline {
}
case "plan_mode_response": {
const response: string | undefined = block.params.response
const optionsRaw: string | undefined = block.params.options
const sharedMessage = {
response: removeClosingTag("response", response),
options: parsePartialArrayString(removeClosingTag("options", optionsRaw)),
} satisfies ClinePlanModeResponse
try {
if (block.partial) {
await this.ask("plan_mode_response", removeClosingTag("response", response), block.partial).catch(
() => {},
)
await this.ask("plan_mode_response", JSON.stringify(sharedMessage), block.partial).catch(() => {})
break
} else {
if (!response) {
@@ -2734,7 +2803,7 @@ export class Cline {
// }
this.isAwaitingPlanResponse = true
let { text, images } = await this.ask("plan_mode_response", response, false)
let { text, images } = await this.ask("plan_mode_response", JSON.stringify(sharedMessage), false)
this.isAwaitingPlanResponse = false
// webview invoke sendMessage will send this marker in order to put webview into the proper state (responding to an ask) and as a flag to extension that the user switched to ACT mode.
@@ -2742,6 +2811,25 @@ export class Cline {
text = ""
}
// Check if options contains the text response
if (optionsRaw && text && parsePartialArrayString(optionsRaw).includes(text)) {
// Valid option selected, don't show user message in UI
// Update last followup message with selected option
const lastPlanMessage = findLast(this.clineMessages, (m) => m.ask === "plan_mode_response")
if (lastPlanMessage) {
lastPlanMessage.text = JSON.stringify({
...sharedMessage,
selected: text,
} satisfies ClinePlanModeResponse)
await this.saveClineMessages()
}
} else {
// Option not selected, send user feedback
if (text || images?.length) {
await this.say("user_feedback", text ?? "", images)
}
}
if (this.didRespondToPlanAskBySwitchingMode) {
pushToolResult(
formatResponse.toolResult(
@@ -2757,10 +2845,6 @@ export class Cline {
pushToolResult(formatResponse.toolResult(`<user_message>\n${text}\n</user_message>`, images))
}
if (text || images?.length) {
await this.say("user_feedback", text ?? "", images)
}
//
break
}
@@ -3040,13 +3124,16 @@ export class Cline {
// use this opportunity to initialize the checkpoint tracker (can be expensive to initialize in the constructor)
// FIXME: right now we're letting users init checkpoints for old tasks, but this could be a problem if opening a task in the wrong workspace
// isNewTask &&
if (!this.checkpointTracker) {
if (!this.checkpointTracker && !this.checkpointTrackerErrorMessage) {
try {
this.checkpointTracker = await CheckpointTracker.create(
this.taskId,
this.providerRef.deref()?.context.globalStorageUri.fsPath,
this.checkpointTracker = await pTimeout(
CheckpointTracker.create(this.taskId, this.providerRef.deref()?.context.globalStorageUri.fsPath),
{
milliseconds: 15_000,
message:
"Checkpoints taking too long to initialize. Consider re-opening Cline in a project that uses git, or disabling checkpoints.",
},
)
this.checkpointTrackerErrorMessage = undefined
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error"
console.error("Failed to initialize checkpoint tracker:", errorMessage)
@@ -3174,6 +3261,7 @@ export class Cline {
let assistantMessage = ""
let reasoningMessage = ""
this.isStreaming = true
let didReceiveUsageChunk = false
try {
for await (const chunk of stream) {
if (!chunk) {
@@ -3181,6 +3269,7 @@ export class Cline {
}
switch (chunk.type) {
case "usage":
didReceiveUsageChunk = true
inputTokens += chunk.inputTokens
outputTokens += chunk.outputTokens
cacheWriteTokens += chunk.cacheWriteTokens ?? 0
@@ -3250,6 +3339,23 @@ export class Cline {
this.isStreaming = false
}
// OpenRouter/Cline may not return token usage as part of the stream (since it may abort early), so we fetch after the stream is finished
// (updateApiReq below will update the api_req_started message with the usage details. we do this async so it updates the api_req_started message in the background)
if (!didReceiveUsageChunk) {
this.api.getApiStreamUsage?.().then(async (apiStreamUsage) => {
if (apiStreamUsage) {
inputTokens += apiStreamUsage.inputTokens
outputTokens += apiStreamUsage.outputTokens
cacheWriteTokens += apiStreamUsage.cacheWriteTokens ?? 0
cacheReadTokens += apiStreamUsage.cacheReadTokens ?? 0
totalCost = apiStreamUsage.totalCost
}
updateApiReqMsg()
await this.saveClineMessages()
await this.providerRef.deref()?.postStateToWebview()
})
}
// need to call here in case the stream was aborted
if (this.abort) {
throw new Error("Cline instance aborted")
@@ -3520,7 +3626,7 @@ export class Cline {
details +=
"\nIn this mode you should focus on information gathering, asking questions, and architecting a solution. Once you have a plan, use the plan_mode_response tool to engage in a conversational back and forth with the user. Do not use the plan_mode_response tool until you've gathered all the information you need e.g. with read_file or ask_followup_question."
details +=
'\n(Remember: If it seems the user wants you to use tools only available in Act Mode, you should ask the user to "toggle to Act mode" (use those words) - they will have to manually do this themselves with the Plan/Act toggle button below. You do not have the ability to switch to Act Mode yourself, and must wait for the user to do it themselves once they are satisfied with the plan.)'
'\n(Remember: If it seems the user wants you to use tools only available in Act Mode, you should ask the user to "toggle to Act mode" (use those words) - they will have to manually do this themselves with the Plan/Act toggle button below. You do not have the ability to switch to Act Mode yourself, and must wait for the user to do it themselves once they are satisfied with the plan. You also cannot present an option to toggle to Act mode, as this will be something you need to direct the user to do manually themselves.)'
} else {
details += "\nACT MODE"
}
+1 -61
View File
@@ -45,6 +45,7 @@ export const toolParamNames = [
"arguments",
"uri",
"question",
"options",
"response",
"result",
] as const
@@ -58,64 +59,3 @@ export interface ToolUse {
params: Partial<Record<ToolParamName, string>>
partial: boolean
}
export interface ExecuteCommandToolUse extends ToolUse {
name: "execute_command"
// Pick<Record<ToolParamName, string>, "command"> makes "command" required, but Partial<> makes it optional
params: Partial<Pick<Record<ToolParamName, string>, "command" | "requires_approval">>
}
export interface ReadFileToolUse extends ToolUse {
name: "read_file"
params: Partial<Pick<Record<ToolParamName, string>, "path">>
}
export interface WriteToFileToolUse extends ToolUse {
name: "write_to_file"
params: Partial<Pick<Record<ToolParamName, string>, "path" | "content">>
}
export interface ReplaceInFileToolUse extends ToolUse {
name: "replace_in_file"
params: Partial<Pick<Record<ToolParamName, string>, "path" | "diff">>
}
export interface SearchFilesToolUse extends ToolUse {
name: "search_files"
params: Partial<Pick<Record<ToolParamName, string>, "path" | "regex" | "file_pattern">>
}
export interface ListFilesToolUse extends ToolUse {
name: "list_files"
params: Partial<Pick<Record<ToolParamName, string>, "path" | "recursive">>
}
export interface ListCodeDefinitionNamesToolUse extends ToolUse {
name: "list_code_definition_names"
params: Partial<Pick<Record<ToolParamName, string>, "path">>
}
export interface BrowserActionToolUse extends ToolUse {
name: "browser_action"
params: Partial<Pick<Record<ToolParamName, string>, "action" | "url" | "coordinate" | "text">>
}
export interface UseMcpToolToolUse extends ToolUse {
name: "use_mcp_tool"
params: Partial<Pick<Record<ToolParamName, string>, "server_name" | "tool_name" | "arguments">>
}
export interface AccessMcpResourceToolUse extends ToolUse {
name: "access_mcp_resource"
params: Partial<Pick<Record<ToolParamName, string>, "server_name" | "uri">>
}
export interface AskFollowupQuestionToolUse extends ToolUse {
name: "ask_followup_question"
params: Partial<Pick<Record<ToolParamName, string>, "question">>
}
export interface AttemptCompletionToolUse extends ToolUse {
name: "attempt_completion"
params: Partial<Pick<Record<ToolParamName, string>, "result" | "command">>
}
+11 -3
View File
@@ -216,9 +216,13 @@ Usage:
Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth.
Parameters:
- question: (required) The question to ask the user. This should be a clear, specific question that addresses the information you need.
- options: (optional) An array of 2-5 options for the user to choose from. Each option should be a string describing a possible answer. You may not always need to provide options, but it may be helpful in many cases where it can save the user from having to type out a response manually.
Usage:
<ask_followup_question>
<question>Your question here</question>
<options>
Array of options here (optional), e.g. ["Option 1", "Option 2", "Option 3"]
</options>
</ask_followup_question>
## attempt_completion
@@ -239,9 +243,13 @@ Your final result description here
Description: Respond to the user's inquiry in an effort to plan a solution to the user's task. This tool should be used when you need to provide a response to a question or statement from the user about how you plan to accomplish the task. This tool is only available in PLAN MODE. The environment_details will specify the current mode, if it is not PLAN MODE then you should not use this tool. Depending on the user's message, you may ask questions to get clarification about the user's request, architect a solution to the task, and to brainstorm ideas with the user. For example, if the user's task is to create a website, you may start by asking some clarifying questions, then present a detailed plan for how you will accomplish the task given the context, and perhaps engage in a back and forth to finalize the details before the user switches you to ACT MODE to implement the solution.
Parameters:
- response: (required) The response to provide to the user. Do not try to use tools in this parameter, this is simply a chat response.
- options: (optional) An array of 2-5 options for the user to choose from. Each option should be a string describing a possible choice or path forward in the planning process. This can help guide the discussion and make it easier for the user to provide input on key decisions. You may not always need to provide options, but it may be helpful in many cases where it can save the user from having to type out a response manually. Do NOT present an option to toggle to Act mode, as this will be something you need to direct the user to do manually themselves.
Usage:
<plan_mode_response>
<response>Your response here</response>
<options>
Array of options here (optional), e.g. ["Option 1", "Option 2", "Option 3"]
</options>
</plan_mode_response>
# Tool Use Examples
@@ -813,7 +821,7 @@ You have access to two tools for working with files: **write_to_file** and **rep
## Important Considerations
- Using write_to_file requires providing the files complete final content.
- Using write_to_file requires providing the file's complete final content.
- If you only need to make small changes to an existing file, consider using replace_in_file instead to avoid unnecessarily rewriting the entire file.
- While write_to_file should not be your default choice, don't hesitate to use it when the situation truly calls for it.
@@ -826,12 +834,12 @@ You have access to two tools for working with files: **write_to_file** and **rep
## When to Use
- Small, localized changes like updating a few lines, function implementations, changing variable names, modifying a section of text, etc.
- Targeted improvements where only specific portions of the files content needs to be altered.
- Targeted improvements where only specific portions of the file's content needs to be altered.
- Especially useful for long files where much of the file will remain unchanged.
## Advantages
- More efficient for minor edits, since you dont need to supply the entire file content.
- More efficient for minor edits, since you don't need to supply the entire file content.
- Reduces the chance of errors that can occur when overwriting large files.
# Choosing the Appropriate Tool
+327 -195
View File
@@ -16,7 +16,7 @@ import { getTheme } from "../../integrations/theme/getTheme"
import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
import { McpHub } from "../../services/mcp/McpHub"
import { UserInfo } from "../../shared/UserInfo"
import { ApiProvider, ModelInfo } from "../../shared/api"
import { ApiConfiguration, ApiProvider, ModelInfo } from "../../shared/api"
import { findLast } from "../../shared/array"
import { AutoApprovalSettings, DEFAULT_AUTO_APPROVAL_SETTINGS } from "../../shared/AutoApprovalSettings"
import { BrowserSettings, DEFAULT_BROWSER_SETTINGS } from "../../shared/BrowserSettings"
@@ -36,6 +36,7 @@ import { telemetryService } from "../../services/telemetry/TelemetryService"
import { TelemetrySetting } from "../../shared/TelemetrySetting"
import { cleanupLegacyCheckpoints } from "../../integrations/checkpoints/CheckpointMigration"
import CheckpointTracker from "../../integrations/checkpoints/CheckpointTracker"
import { getTotalTasksSize } from "../../utils/storage"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -62,12 +63,14 @@ type SecretKey =
| "authNonce"
| "asksageApiKey"
| "xaiApiKey"
| "sambanovaApiKey"
type GlobalStateKey =
| "apiProvider"
| "apiModelId"
| "awsRegion"
| "awsUseCrossRegionInference"
| "awsBedrockUsePromptCache"
| "awsBedrockEndpoint"
| "awsProfile"
| "awsUseProfile"
| "vertexProjectId"
@@ -105,6 +108,7 @@ type GlobalStateKey =
| "telemetrySetting"
| "asksageApiUrl"
| "thinkingBudgetTokens"
| "planActSeparateModelsSetting"
export const GlobalFileNames = {
apiConversationHistory: "api_conversation_history.json",
@@ -544,109 +548,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
break
case "apiConfiguration":
if (message.apiConfiguration) {
const {
apiProvider,
apiModelId,
apiKey,
openRouterApiKey,
awsAccessKey,
awsSecretKey,
awsSessionToken,
awsRegion,
awsUseCrossRegionInference,
awsBedrockUsePromptCache,
awsProfile,
awsUseProfile,
vertexProjectId,
vertexRegion,
openAiBaseUrl,
openAiApiKey,
openAiModelId,
openAiModelInfo,
ollamaModelId,
ollamaBaseUrl,
ollamaApiOptionsCtxNum,
lmStudioModelId,
lmStudioBaseUrl,
anthropicBaseUrl,
geminiApiKey,
openAiNativeApiKey,
deepSeekApiKey,
requestyApiKey,
requestyModelId,
togetherApiKey,
togetherModelId,
qwenApiKey,
mistralApiKey,
azureApiVersion,
openRouterModelId,
openRouterModelInfo,
vsCodeLmModelSelector,
liteLlmBaseUrl,
liteLlmModelId,
liteLlmApiKey,
qwenApiLine,
asksageApiKey,
asksageApiUrl,
xaiApiKey,
thinkingBudgetTokens,
clineApiKey,
} = message.apiConfiguration
await this.updateGlobalState("apiProvider", apiProvider)
await this.updateGlobalState("apiModelId", apiModelId)
await this.storeSecret("apiKey", apiKey)
await this.storeSecret("openRouterApiKey", openRouterApiKey)
await this.storeSecret("awsAccessKey", awsAccessKey)
await this.storeSecret("awsSecretKey", awsSecretKey)
await this.storeSecret("awsSessionToken", awsSessionToken)
await this.updateGlobalState("awsRegion", awsRegion)
await this.updateGlobalState("awsUseCrossRegionInference", awsUseCrossRegionInference)
await this.updateGlobalState("awsBedrockUsePromptCache", awsBedrockUsePromptCache)
await this.updateGlobalState("awsProfile", awsProfile)
await this.updateGlobalState("awsUseProfile", awsUseProfile)
await this.updateGlobalState("vertexProjectId", vertexProjectId)
await this.updateGlobalState("vertexRegion", vertexRegion)
await this.updateGlobalState("openAiBaseUrl", openAiBaseUrl)
await this.storeSecret("openAiApiKey", openAiApiKey)
await this.updateGlobalState("openAiModelId", openAiModelId)
await this.updateGlobalState("openAiModelInfo", openAiModelInfo)
await this.updateGlobalState("ollamaModelId", ollamaModelId)
await this.updateGlobalState("ollamaBaseUrl", ollamaBaseUrl)
await this.updateGlobalState("ollamaApiOptionsCtxNum", ollamaApiOptionsCtxNum)
await this.updateGlobalState("lmStudioModelId", lmStudioModelId)
await this.updateGlobalState("lmStudioBaseUrl", lmStudioBaseUrl)
await this.updateGlobalState("anthropicBaseUrl", anthropicBaseUrl)
await this.storeSecret("geminiApiKey", geminiApiKey)
await this.storeSecret("openAiNativeApiKey", openAiNativeApiKey)
await this.storeSecret("deepSeekApiKey", deepSeekApiKey)
await this.storeSecret("requestyApiKey", requestyApiKey)
await this.storeSecret("togetherApiKey", togetherApiKey)
await this.storeSecret("qwenApiKey", qwenApiKey)
await this.storeSecret("mistralApiKey", mistralApiKey)
await this.storeSecret("liteLlmApiKey", liteLlmApiKey)
await this.storeSecret("xaiApiKey", xaiApiKey)
await this.updateGlobalState("azureApiVersion", azureApiVersion)
await this.updateGlobalState("openRouterModelId", openRouterModelId)
await this.updateGlobalState("openRouterModelInfo", openRouterModelInfo)
await this.updateGlobalState("vsCodeLmModelSelector", vsCodeLmModelSelector)
await this.updateGlobalState("liteLlmBaseUrl", liteLlmBaseUrl)
await this.updateGlobalState("liteLlmModelId", liteLlmModelId)
await this.updateGlobalState("qwenApiLine", qwenApiLine)
await this.updateGlobalState("requestyModelId", requestyModelId)
await this.updateGlobalState("togetherModelId", togetherModelId)
await this.storeSecret("asksageApiKey", asksageApiKey)
await this.updateGlobalState("asksageApiUrl", asksageApiUrl)
await this.updateGlobalState("thinkingBudgetTokens", thinkingBudgetTokens)
await this.storeSecret("clineApiKey", clineApiKey)
if (this.cline) {
this.cline.api = buildApiHandler(message.apiConfiguration)
}
await this.updateApiConfiguration(message.apiConfiguration)
}
await this.postStateToWebview()
break
case "customInstructions":
await this.updateCustomInstructions(message.text)
break
case "autoApprovalSettings":
if (message.autoApprovalSettings) {
await this.updateGlobalState("autoApprovalSettings", message.autoApprovalSettings)
@@ -670,6 +575,13 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.togglePlanActModeWithChatSettings(message.chatSettings, message.chatContent)
}
break
case "optionsResponse":
await this.postMessageToWebview({
type: "invoke",
invoke: "sendMessage",
text: message.text,
})
break
// case "relaunchChromeDebugMode":
// if (this.cline) {
// this.cline.browserSession.relaunchChromeDebugMode()
@@ -905,6 +817,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
break
}
case "requestTotalTasksSize": {
this.refreshTotalTasksSize()
break
}
case "restartMcpServer": {
try {
await this.mcpHub?.restartConnection(message.text!)
@@ -974,13 +890,42 @@ export class ClineProvider implements vscode.WebviewViewProvider {
break
}
case "telemetrySetting": {
const telemetrySetting = message.text as TelemetrySetting
await this.updateGlobalState("telemetrySetting", telemetrySetting)
const isOptedIn = telemetrySetting === "enabled"
telemetryService.updateTelemetryState(isOptedIn)
if (message.telemetrySetting) {
await this.updateTelemetrySetting(message.telemetrySetting)
}
await this.postStateToWebview()
break
}
case "updateSettings": {
// api config
if (message.apiConfiguration) {
await this.updateApiConfiguration(message.apiConfiguration)
}
// custom instructions
await this.updateCustomInstructions(message.customInstructionsSetting)
// telemetry setting
if (message.telemetrySetting) {
await this.updateTelemetrySetting(message.telemetrySetting)
}
// plan act setting
await this.updateGlobalState("planActSeparateModelsSetting", message.planActSeparateModelsSetting)
// after settings are updated, post state to webview
await this.postStateToWebview()
await this.postMessageToWebview({ type: "didUpdateSettings" })
break
}
case "clearAllTaskHistory": {
await this.deleteAllTaskHistory()
await this.postStateToWebview()
this.refreshTotalTasksSize()
this.postMessageToWebview({ type: "relinquishControl" })
break
}
// Add more switch case statements here as more webview message commands
// are created within the webview context (i.e. inside media/main.js)
}
@@ -990,6 +935,12 @@ export class ClineProvider implements vscode.WebviewViewProvider {
)
}
async updateTelemetrySetting(telemetrySetting: TelemetrySetting) {
await this.updateGlobalState("telemetrySetting", telemetrySetting)
const isOptedIn = telemetrySetting === "enabled"
telemetryService.updateTelemetryState(isOptedIn)
}
async togglePlanActModeWithChatSettings(chatSettings: ChatSettings, chatContent?: ChatContent) {
const didSwitchToActMode = chatSettings.mode === "act"
@@ -1003,86 +954,97 @@ export class ClineProvider implements vscode.WebviewViewProvider {
previousModeModelId: newModelId,
previousModeModelInfo: newModelInfo,
previousModeThinkingBudgetTokens: newThinkingBudgetTokens,
planActSeparateModelsSetting,
} = await this.getState()
// Save the last model used in this mode
await this.updateGlobalState("previousModeApiProvider", apiConfiguration.apiProvider)
await this.updateGlobalState("previousModeThinkingBudgetTokens", apiConfiguration.thinkingBudgetTokens)
switch (apiConfiguration.apiProvider) {
case "anthropic":
case "bedrock":
case "vertex":
case "gemini":
case "asksage":
await this.updateGlobalState("previousModeModelId", apiConfiguration.apiModelId)
break
case "openrouter":
case "cline":
await this.updateGlobalState("previousModeModelId", apiConfiguration.openRouterModelId)
await this.updateGlobalState("previousModeModelInfo", apiConfiguration.openRouterModelInfo)
break
case "vscode-lm":
await this.updateGlobalState("previousModeModelId", apiConfiguration.vsCodeLmModelSelector)
break
case "openai":
await this.updateGlobalState("previousModeModelId", apiConfiguration.openAiModelId)
await this.updateGlobalState("previousModeModelInfo", apiConfiguration.openAiModelInfo)
break
case "ollama":
await this.updateGlobalState("previousModeModelId", apiConfiguration.ollamaModelId)
break
case "lmstudio":
await this.updateGlobalState("previousModeModelId", apiConfiguration.lmStudioModelId)
break
case "litellm":
await this.updateGlobalState("previousModeModelId", apiConfiguration.liteLlmModelId)
break
case "requesty":
await this.updateGlobalState("previousModeModelId", apiConfiguration.requestyModelId)
break
}
const shouldSwitchModel = planActSeparateModelsSetting === true
// Restore the model used in previous mode
if (newApiProvider && newModelId) {
await this.updateGlobalState("apiProvider", newApiProvider)
await this.updateGlobalState("thinkingBudgetTokens", newThinkingBudgetTokens)
switch (newApiProvider) {
if (shouldSwitchModel) {
// Save the last model used in this mode
await this.updateGlobalState("previousModeApiProvider", apiConfiguration.apiProvider)
await this.updateGlobalState("previousModeThinkingBudgetTokens", apiConfiguration.thinkingBudgetTokens)
switch (apiConfiguration.apiProvider) {
case "anthropic":
case "bedrock":
case "vertex":
case "gemini":
case "asksage":
await this.updateGlobalState("apiModelId", newModelId)
case "openai-native":
case "qwen":
case "deepseek":
await this.updateGlobalState("previousModeModelId", apiConfiguration.apiModelId)
break
case "openrouter":
case "cline":
await this.updateGlobalState("openRouterModelId", newModelId)
await this.updateGlobalState("openRouterModelInfo", newModelInfo)
await this.updateGlobalState("previousModeModelId", apiConfiguration.openRouterModelId)
await this.updateGlobalState("previousModeModelInfo", apiConfiguration.openRouterModelInfo)
break
case "vscode-lm":
await this.updateGlobalState("vsCodeLmModelSelector", newModelId)
await this.updateGlobalState("previousModeModelId", apiConfiguration.vsCodeLmModelSelector)
break
case "openai":
await this.updateGlobalState("openAiModelId", newModelId)
await this.updateGlobalState("openAiModelInfo", newModelInfo)
await this.updateGlobalState("previousModeModelId", apiConfiguration.openAiModelId)
await this.updateGlobalState("previousModeModelInfo", apiConfiguration.openAiModelInfo)
break
case "ollama":
await this.updateGlobalState("ollamaModelId", newModelId)
await this.updateGlobalState("previousModeModelId", apiConfiguration.ollamaModelId)
break
case "lmstudio":
await this.updateGlobalState("lmStudioModelId", newModelId)
await this.updateGlobalState("previousModeModelId", apiConfiguration.lmStudioModelId)
break
case "litellm":
await this.updateGlobalState("liteLlmModelId", newModelId)
await this.updateGlobalState("previousModeModelId", apiConfiguration.liteLlmModelId)
break
case "requesty":
await this.updateGlobalState("requestyModelId", newModelId)
await this.updateGlobalState("previousModeModelId", apiConfiguration.requestyModelId)
break
}
if (this.cline) {
const { apiConfiguration: updatedApiConfiguration } = await this.getState()
this.cline.api = buildApiHandler(updatedApiConfiguration)
// Restore the model used in previous mode
if (newApiProvider || newModelId || newThinkingBudgetTokens !== undefined) {
await this.updateGlobalState("apiProvider", newApiProvider)
await this.updateGlobalState("thinkingBudgetTokens", newThinkingBudgetTokens)
switch (newApiProvider) {
case "anthropic":
case "bedrock":
case "vertex":
case "gemini":
case "asksage":
case "openai-native":
case "qwen":
case "deepseek":
await this.updateGlobalState("apiModelId", newModelId)
break
case "openrouter":
case "cline":
await this.updateGlobalState("openRouterModelId", newModelId)
await this.updateGlobalState("openRouterModelInfo", newModelInfo)
break
case "vscode-lm":
await this.updateGlobalState("vsCodeLmModelSelector", newModelId)
break
case "openai":
await this.updateGlobalState("openAiModelId", newModelId)
await this.updateGlobalState("openAiModelInfo", newModelInfo)
break
case "ollama":
await this.updateGlobalState("ollamaModelId", newModelId)
break
case "lmstudio":
await this.updateGlobalState("lmStudioModelId", newModelId)
break
case "litellm":
await this.updateGlobalState("liteLlmModelId", newModelId)
break
case "requesty":
await this.updateGlobalState("requestyModelId", newModelId)
break
}
if (this.cline) {
const { apiConfiguration: updatedApiConfiguration } = await this.getState()
this.cline.api = buildApiHandler(updatedApiConfiguration)
}
}
}
@@ -1141,7 +1103,110 @@ export class ClineProvider implements vscode.WebviewViewProvider {
if (this.cline) {
this.cline.customInstructions = instructions || undefined
}
await this.postStateToWebview()
}
async updateApiConfiguration(apiConfiguration: ApiConfiguration) {
const {
apiProvider,
apiModelId,
apiKey,
openRouterApiKey,
awsAccessKey,
awsSecretKey,
awsSessionToken,
awsRegion,
awsUseCrossRegionInference,
awsBedrockUsePromptCache,
awsBedrockEndpoint,
awsProfile,
awsUseProfile,
vertexProjectId,
vertexRegion,
openAiBaseUrl,
openAiApiKey,
openAiModelId,
openAiModelInfo,
ollamaModelId,
ollamaBaseUrl,
ollamaApiOptionsCtxNum,
lmStudioModelId,
lmStudioBaseUrl,
anthropicBaseUrl,
geminiApiKey,
openAiNativeApiKey,
deepSeekApiKey,
requestyApiKey,
requestyModelId,
togetherApiKey,
togetherModelId,
qwenApiKey,
mistralApiKey,
azureApiVersion,
openRouterModelId,
openRouterModelInfo,
vsCodeLmModelSelector,
liteLlmBaseUrl,
liteLlmModelId,
liteLlmApiKey,
qwenApiLine,
asksageApiKey,
asksageApiUrl,
xaiApiKey,
thinkingBudgetTokens,
clineApiKey,
sambanovaApiKey,
} = apiConfiguration
await this.updateGlobalState("apiProvider", apiProvider)
await this.updateGlobalState("apiModelId", apiModelId)
await this.storeSecret("apiKey", apiKey)
await this.storeSecret("openRouterApiKey", openRouterApiKey)
await this.storeSecret("awsAccessKey", awsAccessKey)
await this.storeSecret("awsSecretKey", awsSecretKey)
await this.storeSecret("awsSessionToken", awsSessionToken)
await this.updateGlobalState("awsRegion", awsRegion)
await this.updateGlobalState("awsUseCrossRegionInference", awsUseCrossRegionInference)
await this.updateGlobalState("awsBedrockUsePromptCache", awsBedrockUsePromptCache)
await this.updateGlobalState("awsBedrockEndpoint", awsBedrockEndpoint)
await this.updateGlobalState("awsProfile", awsProfile)
await this.updateGlobalState("awsUseProfile", awsUseProfile)
await this.updateGlobalState("vertexProjectId", vertexProjectId)
await this.updateGlobalState("vertexRegion", vertexRegion)
await this.updateGlobalState("openAiBaseUrl", openAiBaseUrl)
await this.storeSecret("openAiApiKey", openAiApiKey)
await this.updateGlobalState("openAiModelId", openAiModelId)
await this.updateGlobalState("openAiModelInfo", openAiModelInfo)
await this.updateGlobalState("ollamaModelId", ollamaModelId)
await this.updateGlobalState("ollamaBaseUrl", ollamaBaseUrl)
await this.updateGlobalState("ollamaApiOptionsCtxNum", ollamaApiOptionsCtxNum)
await this.updateGlobalState("lmStudioModelId", lmStudioModelId)
await this.updateGlobalState("lmStudioBaseUrl", lmStudioBaseUrl)
await this.updateGlobalState("anthropicBaseUrl", anthropicBaseUrl)
await this.storeSecret("geminiApiKey", geminiApiKey)
await this.storeSecret("openAiNativeApiKey", openAiNativeApiKey)
await this.storeSecret("deepSeekApiKey", deepSeekApiKey)
await this.storeSecret("requestyApiKey", requestyApiKey)
await this.storeSecret("togetherApiKey", togetherApiKey)
await this.storeSecret("qwenApiKey", qwenApiKey)
await this.storeSecret("mistralApiKey", mistralApiKey)
await this.storeSecret("liteLlmApiKey", liteLlmApiKey)
await this.storeSecret("xaiApiKey", xaiApiKey)
await this.updateGlobalState("azureApiVersion", azureApiVersion)
await this.updateGlobalState("openRouterModelId", openRouterModelId)
await this.updateGlobalState("openRouterModelInfo", openRouterModelInfo)
await this.updateGlobalState("vsCodeLmModelSelector", vsCodeLmModelSelector)
await this.updateGlobalState("liteLlmBaseUrl", liteLlmBaseUrl)
await this.updateGlobalState("liteLlmModelId", liteLlmModelId)
await this.updateGlobalState("qwenApiLine", qwenApiLine)
await this.updateGlobalState("requestyModelId", requestyModelId)
await this.updateGlobalState("togetherModelId", togetherModelId)
await this.storeSecret("asksageApiKey", asksageApiKey)
await this.updateGlobalState("asksageApiUrl", asksageApiUrl)
await this.updateGlobalState("thinkingBudgetTokens", thinkingBudgetTokens)
await this.storeSecret("clineApiKey", clineApiKey)
await this.storeSecret("sambanovaApiKey", sambanovaApiKey)
if (this.cline) {
this.cline.api = buildApiHandler(apiConfiguration)
}
}
// MCP
@@ -1707,46 +1772,78 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
await downloadTask(historyItem.ts, apiConversationHistory)
}
async deleteAllTaskHistory() {
await this.clearTask()
await this.updateGlobalState("taskHistory", undefined)
try {
// Remove all contents of tasks directory
const taskDirPath = path.join(this.context.globalStorageUri.fsPath, "tasks")
if (await fileExistsAtPath(taskDirPath)) {
await fs.rm(taskDirPath, { recursive: true, force: true })
}
// Remove checkpoints directory contents
const checkpointsDirPath = path.join(this.context.globalStorageUri.fsPath, "checkpoints")
if (await fileExistsAtPath(checkpointsDirPath)) {
await fs.rm(checkpointsDirPath, { recursive: true, force: true })
}
} catch (error) {
vscode.window.showErrorMessage(
`Encountered error while deleting task history, there may be some files left behind. Error: ${error instanceof Error ? error.message : String(error)}`,
)
}
// await this.postStateToWebview()
}
async refreshTotalTasksSize() {
getTotalTasksSize(this.context.globalStorageUri.fsPath)
.then((newTotalSize) => {
this.postMessageToWebview({
type: "totalTasksSize",
totalTasksSize: newTotalSize,
})
})
.catch((error) => {
console.error("Error calculating total tasks size:", error)
})
}
async deleteTaskWithId(id: string) {
console.info("deleteTaskWithId: ", id)
if (id === this.cline?.taskId) {
await this.clearTask()
console.debug("cleared task")
}
const { taskDirPath, apiConversationHistoryFilePath, uiMessagesFilePath } = await this.getTaskWithId(id)
// Delete checkpoints
console.info("deleting checkpoints")
const taskHistory = ((await this.getGlobalState("taskHistory")) as HistoryItem[] | undefined) || []
const historyItem = taskHistory.find((item) => item.id === id)
//console.log("historyItem: ", historyItem)
if (historyItem) {
try {
await CheckpointTracker.deleteCheckpoints(id, historyItem, this.context.globalStorageUri.fsPath)
} catch (error) {
console.error(`Failed to delete checkpoints for task ${id}:`, error)
try {
if (id === this.cline?.taskId) {
await this.clearTask()
console.debug("cleared task")
}
const { taskDirPath, apiConversationHistoryFilePath, uiMessagesFilePath } = await this.getTaskWithId(id)
const updatedTaskHistory = await this.deleteTaskFromState(id)
// Delete the task files
const apiConversationHistoryFileExists = await fileExistsAtPath(apiConversationHistoryFilePath)
if (apiConversationHistoryFileExists) {
await fs.unlink(apiConversationHistoryFilePath)
}
const uiMessagesFileExists = await fileExistsAtPath(uiMessagesFilePath)
if (uiMessagesFileExists) {
await fs.unlink(uiMessagesFilePath)
}
const legacyMessagesFilePath = path.join(taskDirPath, "claude_messages.json")
if (await fileExistsAtPath(legacyMessagesFilePath)) {
await fs.unlink(legacyMessagesFilePath)
}
await fs.rmdir(taskDirPath) // succeeds if the dir is empty
if (updatedTaskHistory.length === 0) {
await this.deleteAllTaskHistory()
}
} catch (error) {
console.debug(`Error deleting task:`, error)
}
await this.deleteTaskFromState(id)
// Delete the task files
const apiConversationHistoryFileExists = await fileExistsAtPath(apiConversationHistoryFilePath)
if (apiConversationHistoryFileExists) {
await fs.unlink(apiConversationHistoryFilePath)
}
const uiMessagesFileExists = await fileExistsAtPath(uiMessagesFilePath)
if (uiMessagesFileExists) {
await fs.unlink(uiMessagesFilePath)
}
const legacyMessagesFilePath = path.join(taskDirPath, "claude_messages.json")
if (await fileExistsAtPath(legacyMessagesFilePath)) {
await fs.unlink(legacyMessagesFilePath)
}
await fs.rmdir(taskDirPath) // succeeds if the dir is empty
this.refreshTotalTasksSize()
}
async deleteTaskFromState(id: string) {
@@ -1757,6 +1854,8 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
// Notify the webview that the task has been deleted
await this.postStateToWebview()
return updatedTaskHistory
}
async postStateToWebview() {
@@ -1776,6 +1875,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
userInfo,
mcpMarketplaceEnabled,
telemetrySetting,
planActSeparateModelsSetting,
} = await this.getState()
return {
@@ -1786,7 +1886,10 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
currentTaskItem: this.cline?.taskId ? (taskHistory || []).find((item) => item.id === this.cline?.taskId) : undefined,
checkpointTrackerErrorMessage: this.cline?.checkpointTrackerErrorMessage,
clineMessages: this.cline?.clineMessages || [],
taskHistory: (taskHistory || []).filter((item) => item.ts && item.task).sort((a, b) => b.ts - a.ts),
taskHistory: (taskHistory || [])
.filter((item) => item.ts && item.task)
.sort((a, b) => b.ts - a.ts)
.slice(0, 100), // for now we're only getting the latest 100 tasks, but a better solution here is to only pass in 3 for recent task history, and then get the full task history on demand when going to the task history view (maybe with pagination?)
shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId,
platform: process.platform as Platform,
autoApprovalSettings,
@@ -1795,6 +1898,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
userInfo,
mcpMarketplaceEnabled,
telemetrySetting,
planActSeparateModelsSetting,
vscMachineId: vscode.env.machineId,
}
}
@@ -1863,6 +1967,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
awsRegion,
awsUseCrossRegionInference,
awsBedrockUsePromptCache,
awsBedrockEndpoint,
awsProfile,
awsUseProfile,
vertexProjectId,
@@ -1910,6 +2015,8 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
asksageApiUrl,
xaiApiKey,
thinkingBudgetTokens,
sambanovaApiKey,
planActSeparateModelsSettingRaw,
] = await Promise.all([
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
this.getGlobalState("apiModelId") as Promise<string | undefined>,
@@ -1922,6 +2029,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
this.getGlobalState("awsRegion") as Promise<string | undefined>,
this.getGlobalState("awsUseCrossRegionInference") as Promise<boolean | undefined>,
this.getGlobalState("awsBedrockUsePromptCache") as Promise<boolean | undefined>,
this.getGlobalState("awsBedrockEndpoint") as Promise<string | undefined>,
this.getGlobalState("awsProfile") as Promise<string | undefined>,
this.getGlobalState("awsUseProfile") as Promise<boolean | undefined>,
this.getGlobalState("vertexProjectId") as Promise<string | undefined>,
@@ -1969,6 +2077,8 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
this.getGlobalState("asksageApiUrl") as Promise<string | undefined>,
this.getSecret("xaiApiKey") as Promise<string | undefined>,
this.getGlobalState("thinkingBudgetTokens") as Promise<number | undefined>,
this.getSecret("sambanovaApiKey") as Promise<string | undefined>,
this.getGlobalState("planActSeparateModelsSetting") as Promise<boolean | undefined>,
])
let apiProvider: ApiProvider
@@ -1991,6 +2101,24 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
const mcpMarketplaceEnabled = vscode.workspace.getConfiguration("cline").get<boolean>("mcpMarketplace.enabled", true)
// Plan/Act separate models setting is a boolean indicating whether the user wants to use different models for plan and act. Existing users expect this to be enabled, while we want new users to opt in to this being disabled by default.
// On win11 state sometimes initializes as empty string instead of undefined
let planActSeparateModelsSetting: boolean | undefined = undefined
if (planActSeparateModelsSettingRaw === true || planActSeparateModelsSettingRaw === false) {
planActSeparateModelsSetting = planActSeparateModelsSettingRaw
} else {
// default to true for existing users
if (storedApiProvider) {
planActSeparateModelsSetting = true
} else {
// default to false for new users
planActSeparateModelsSetting = false
}
// this is a special case where it's a new state, but we want it to default to different values for existing and new users.
// persist so next time state is retrieved it's set to the correct value.
await this.updateGlobalState("planActSeparateModelsSetting", planActSeparateModelsSetting)
}
return {
apiConfiguration: {
apiProvider,
@@ -2004,6 +2132,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
awsRegion,
awsUseCrossRegionInference,
awsBedrockUsePromptCache,
awsBedrockEndpoint,
awsProfile,
awsUseProfile,
vertexProjectId,
@@ -2040,6 +2169,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
asksageApiKey,
asksageApiUrl,
xaiApiKey,
sambanovaApiKey,
},
lastShownAnnouncementId,
customInstructions,
@@ -2054,6 +2184,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
previousModeThinkingBudgetTokens,
mcpMarketplaceEnabled,
telemetrySetting: telemetrySetting || "unset",
planActSeparateModelsSetting,
}
}
@@ -2185,6 +2316,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
"liteLlmApiKey",
"asksageApiKey",
"xaiApiKey",
"sambanovaApiKey",
]
for (const key of secretKeys) {
await this.storeSecret(key, undefined)
@@ -2,17 +2,9 @@ import fs from "fs/promises"
import { globby } from "globby"
import * as path from "path"
import simpleGit, { SimpleGit } from "simple-git"
import { HistoryItem } from "../../shared/HistoryItem"
import { telemetryService } from "../../services/telemetry/TelemetryService"
import { fileExistsAtPath } from "../../utils/fs"
import { getLfsPatterns, writeExcludesFile } from "./CheckpointExclusions"
import { getWorkingDirectory, hashWorkingDir } from "./CheckpointUtils"
interface StorageProvider {
context: {
globalStorageUri: { fsPath: string }
}
}
import { telemetryService } from "../../services/telemetry/TelemetryService"
interface CheckpointAddResult {
success: boolean
@@ -27,7 +19,6 @@ interface CheckpointAddResult {
* - Git repository initialization and configuration
* - Git settings management (user, LFS, etc.)
* - Worktree configuration and management
* - Task-specific branch management (creation, switching, deletion)
* - Managing nested git repositories during checkpoint operations
* - File staging and checkpoint creation
* - Shadow git repository maintenance and cleanup
@@ -63,7 +54,7 @@ export class GitOperations {
* - Unable to create initial commit
* - LFS pattern setup fails
*/
public async initShadowGit(gitPath: string, cwd: string): Promise<string> {
public async initShadowGit(gitPath: string, cwd: string, taskId: string): Promise<string> {
console.info(`Initializing shadow git`)
// If repo exists, just verify worktree
@@ -74,10 +65,15 @@ export class GitOperations {
throw new Error("Checkpoints can only be used in the original workspace: " + worktree.value)
}
console.warn(`Using existing shadow git at ${gitPath}`)
// shadow git repo already exists, but update the excludes just in case
await writeExcludesFile(gitPath, await getLfsPatterns(this.cwd))
return gitPath
}
// Initialize new repo
const startTime = performance.now()
const checkpointsDir = path.dirname(gitPath)
console.warn(`Creating new shadow git in ${checkpointsDir}`)
@@ -94,12 +90,14 @@ export class GitOperations {
const lfsPatterns = await getLfsPatterns(cwd)
await writeExcludesFile(gitPath, lfsPatterns)
// Stage all files for initial commit (important so main branch is created with all files in initial commit, and branches created from it will take up less space)
await this.addCheckpointFiles(git)
// Initial commit only on first repo creation
await git.commit("initial commit", { "--allow-empty": null })
const durationMs = Math.round(performance.now() - startTime)
telemetryService.captureCheckpointUsage(taskId, "shadow_git_initialized", durationMs)
console.warn(`Shadow git initialization completed`)
return gitPath
@@ -125,174 +123,6 @@ export class GitOperations {
}
}
/**
* Checks if a shadow Git repository exists for the current workspace.
* (checkpoints/{workspaceHash}/.git).
*
* @param provider - The ClineProvider instance for accessing VS Code functionality
* @returns Promise<boolean> True if a branch-per-task shadow git exists, false otherwise
*/
// public static async doesShadowGitExist(provider?: StorageProvider): Promise<boolean> {
// const globalStoragePath = provider?.context.globalStorageUri.fsPath
// if (!globalStoragePath) {
// return false
// }
// // Check branch-per-task path for newer tasks
// const workingDir = await getWorkingDirectory()
// const cwdHash = hashWorkingDir(workingDir)
// const gitPath = path.join(globalStoragePath, "checkpoints", cwdHash, ".git")
// const exists = await fileExistsAtPath(gitPath)
// if (exists) {
// console.info("Found existing shadow git")
// }
// return exists
// }
/**
* Deletes a branch in the git repository, handling cases where the branch is currently checked out.
* If the branch to be deleted is currently checked out, the method will:
* 1. Save the current worktree configuration
* 2. Temporarily unset the worktree to prevent workspace modifications
* 3. Force switch to master/main branch
* 4. Delete the target branch
* 5. Restore the worktree configuration
*
* @param git - SimpleGit instance to use for operations
* @param branchName - Name of the branch to delete
* @param checkpointsDir - Directory containing the git repository
* @throws Error if:
* - Branch deletion fails
* - Unable to switch to master/main branch after 3 retries
* - Git operations fail during the process
*/
public static async deleteBranchForGit(git: SimpleGit, branchName: string): Promise<void> {
// Check if branch exists
const branches = await git.branchLocal()
if (!branches.all.includes(branchName)) {
console.error(`Task branch ${branchName} does not exist, nothing to delete`)
return // Branch doesn't exist, nothing to delete
}
// First, if we're on the branch to be deleted, switch to master/main
const currentBranch = await git.revparse(["--abbrev-ref", "HEAD"])
console.info(`Current branch: ${currentBranch}, target branch to delete: ${branchName}`)
if (currentBranch === branchName) {
console.debug("Currently on branch to be deleted, switching to master/main first")
// Save the current worktree config
const worktree = await git.getConfig("core.worktree")
console.debug(`Saved current worktree config: ${worktree.value}`)
try {
await git.raw(["config", "--unset", "core.worktree"])
// Force discard all changes before we delete the branch
await git.reset(["--hard"])
await git.clean("f", ["-d"]) // Clean mode 'f' for force, -d for directories
// Determine default branch (master or main)
const defaultBranch = branches.all.includes("main") ? "main" : "master"
console.debug(`Using ${defaultBranch} as default branch`)
// Switch to default branch and delete target branch
console.debug(`Attempting to force switch to ${defaultBranch} branch`)
await git.checkout([defaultBranch, "--force"])
// Verify the switch completed, sometimes this takes a second
let retries = 3
while (retries > 0) {
const newBranch = await git.revparse(["--abbrev-ref", "HEAD"])
if (newBranch === defaultBranch) {
console.debug(`Successfully switched to ${defaultBranch} branch`)
break
}
retries--
if (retries === 0) {
throw new Error(`Failed to switch to ${defaultBranch} branch`)
}
}
console.info(`Deleting branch: ${branchName}`)
await git.raw(["branch", "-D", branchName])
console.debug(`Successfully deleted branch: ${branchName}`)
} finally {
// Restore the worktree config
if (worktree.value) {
await git.addConfig("core.worktree", worktree.value)
}
}
} else {
// If we're not on the branch, we can safely delete it
console.info(`Directly deleting branch ${branchName}`)
await git.raw(["branch", "-D", branchName])
console.debug(`Successfully deleted branch: ${branchName}`)
}
}
/**
* Static method to delete a task's branch using stored workspace path.
* 1. First attempts to delete branch-per-task checkpoint if it exists
*
* @param taskId - The ID of the task whose branch should be deleted
* @param historyItem - The history item containing the shadow git config
* @param globalStoragePath - Path to VS Code's global storage
* @throws Error if:
* - Global storage path is invalid
* - Branch deletion fails
*/
public static async deleteTaskBranchStatic(
taskId: string,
historyItem: HistoryItem,
globalStoragePath: string,
): Promise<void> {
try {
console.debug("Starting static task branch deletion process...")
if (!globalStoragePath) {
throw new Error("Global storage uri is invalid")
}
// Handle both active and inactive tasks
let workingDir: string
if (historyItem.shadowGitConfigWorkTree) {
workingDir = historyItem.shadowGitConfigWorkTree
} else {
workingDir = await getWorkingDirectory()
}
const gitPath = path.join(globalStoragePath, "checkpoints", hashWorkingDir(workingDir), ".git")
if (await fileExistsAtPath(gitPath)) {
console.debug(`Found branch-per-task git repository at ${gitPath}`)
const git = simpleGit(path.dirname(gitPath))
const branchName = `task-${taskId}`
// Check if the branch exists
const branches = await git.branchLocal()
if (branches.all.includes(branchName)) {
console.info(`Found branch ${branchName} to delete`)
await GitOperations.deleteBranchForGit(git, branchName)
// Determine if the task is active based on whether we had to use the stored worktree path
const isTaskActive = !historyItem.shadowGitConfigWorkTree
telemetryService.captureCheckpointUsage(
taskId,
isTaskActive ? "branch_deleted_active" : "branch_deleted_inactive",
)
return
}
console.warn(`Branch ${branchName} not found in branch-per-task repository`)
}
console.info("No checkpoints found to delete")
} catch (error) {
console.error("Failed to delete task branch:", error)
throw new Error(`Failed to delete task branch: ${error instanceof Error ? error.message : String(error)}`)
}
}
/**
* Since we use git to track checkpoints, we need to temporarily disable nested git repos to work around git's
* requirement of using submodules for nested repos.
@@ -333,48 +163,6 @@ export class GitOperations {
}
}
/**
* Switches to or creates a task-specific branch in the shadow Git repository.
* For branch-per-task checkpoints, this ensures we're on the correct task branch before operations.
*
* The method performs the following:
* 1. Gets the shadow git path and initializes simple-git
* 2. Constructs the branch name using the task ID
* 3. Checks if the branch exists:
* - If not, creates a new branch
* - If yes, switches to the existing branch
* 4. Verifies the branch switch completed successfully
*
* Branch naming convention:
* task-{taskId}
*
* @param taskId - The ID of the task whose branch to switch to
* @param gitPath - Path to the .git directory
* @returns Promise<void>
* @throws Error if branch operations fail or git commands error
*/
public async switchToTaskBranch(taskId: string, gitPath: string): Promise<void> {
const git = simpleGit(path.dirname(gitPath))
const branchName = `task-${taskId}`
// Update excludes when creating a new branch for a new task
await writeExcludesFile(gitPath, await getLfsPatterns(this.cwd))
// Create new task-specific branch, or switch to one if it already exists.
const branches = await git.branchLocal()
if (!branches.all.includes(branchName)) {
console.info(`Creating new task branch: ${branchName}`)
await git.checkoutLocalBranch(branchName)
telemetryService.captureCheckpointUsage(taskId, "branch_created")
} else {
console.info(`Switching to existing task branch: ${branchName}`)
await git.checkout(branchName)
}
// const currentBranch = await git.revparse(["--abbrev-ref", "HEAD"])
console.info(`Current Checkpoint branch after switch: ${branchName}`)
}
/**
* Adds files to the shadow git repository while handling nested git repos.
* Uses git commands to list files and stages them for commit.
@@ -396,6 +184,7 @@ export class GitOperations {
* - Nested git repo handling fails
*/
public async addCheckpointFiles(git: SimpleGit): Promise<CheckpointAddResult> {
const startTime = performance.now()
try {
// Update exclude patterns before each commit
await this.renameNestedGitRepos(true)
@@ -403,6 +192,8 @@ export class GitOperations {
try {
await git.add(".")
const durationMs = Math.round(performance.now() - startTime)
console.debug(`Checkpoint add operation completed in ${durationMs}ms`)
return { success: true }
} catch (error) {
console.error("Checkpoint add operation failed:", error)
@@ -2,10 +2,9 @@ import fs from "fs/promises"
import * as path from "path"
import simpleGit from "simple-git"
import * as vscode from "vscode"
import { HistoryItem } from "../../shared/HistoryItem"
import { telemetryService } from "../../services/telemetry/TelemetryService"
import { GitOperations } from "./CheckpointGitOperations"
import { getShadowGitPath, hashWorkingDir, getWorkingDirectory } from "./CheckpointUtils"
import { getShadowGitPath, getWorkingDirectory, hashWorkingDir } from "./CheckpointUtils"
/**
* CheckpointTracker Module
@@ -34,9 +33,9 @@ import { getShadowGitPath, hashWorkingDir, getWorkingDirectory } from "./Checkpo
* - Handles cleanup and resource disposal
*
* Checkpoint Architecture:
* - Uses a branch-per-task model to consolidate shadow git repositories
* - Each task gets its own branch within a single shadow git per workspace
* - Automatically cleans up by deleting task branches when tasks are removed
* - Unique shadow git repository for each workspace
* - Workspaces are identified by name, and hashed to a unique number
* - All commits for a workspace are stored in one shadow git, under a single branch
*/
class CheckpointTracker {
@@ -65,7 +64,7 @@ class CheckpointTracker {
/**
* Creates a new CheckpointTracker instance for tracking changes in a task.
* Handles initialization of the shadow git repository and branch setup.
* Handles initialization of the shadow git repository.
*
* @param taskId - Unique identifier for the task to track
* @param globalStoragePath - the globalStorage path
@@ -79,11 +78,9 @@ class CheckpointTracker {
* Key operations:
* - Validates git installation and settings
* - Creates/initializes shadow git repository
* - Sets up task-specific branch for new checkpoints
*
* Configuration:
* - Respects 'cline.enableCheckpoints' VS Code setting
* - Uses branch-per-task architecture for new checkpoints
*/
public static async create(taskId: string, globalStoragePath: string | undefined): Promise<CheckpointTracker | undefined> {
if (!globalStoragePath) {
@@ -91,6 +88,7 @@ class CheckpointTracker {
}
try {
console.info(`Creating new CheckpointTracker for task ${taskId}`)
const startTime = performance.now()
// Check if checkpoints are disabled in VS Code settings
const enableCheckpoints = vscode.workspace.getConfiguration("cline").get<boolean>("enableCheckpoints") ?? true
@@ -111,13 +109,11 @@ class CheckpointTracker {
const newTracker = new CheckpointTracker(globalStoragePath, taskId, workingDir, cwdHash)
// Branch-per-task structure
const gitPath = await getShadowGitPath(newTracker.globalStoragePath, newTracker.taskId, newTracker.cwdHash)
await newTracker.gitOperations.initShadowGit(gitPath, workingDir)
await newTracker.gitOperations.initShadowGit(gitPath, workingDir, taskId)
telemetryService.captureCheckpointUsage(taskId, "shadow_git_initialized")
await newTracker.gitOperations.switchToTaskBranch(newTracker.taskId, gitPath)
const durationMs = Math.round(performance.now() - startTime)
telemetryService.captureCheckpointUsage(taskId, "shadow_git_initialized", durationMs)
return newTracker
} catch (error) {
@@ -131,33 +127,31 @@ class CheckpointTracker {
*
* Key behaviors:
* - Creates commit with checkpoint files in shadow git repo
* - For new tasks, switches to task-specific branch first
* - Caches the created commit hash
*
* Commit structure:
* - Branch-per-task: "checkpoint-{cwdHash}-{taskId}"
* - Commit message: "checkpoint-{cwdHash}-{taskId}"
* - Always allows empty commits
*
* Dependencies:
* - Requires initialized shadow git (getShadowGitPath)
* - For new checkpoints, requires task branch setup
* - Uses addCheckpointFiles to stage changes using 'git add .'
* - Relies on git's native exclusion handling via the exclude file
*
* @returns Promise<string | undefined> The created commit hash, or undefined if:
* - Shadow git access fails
* - Branch switch fails
* - Staging files fails
* - Commit creation fails
* @throws Error if unable to:
* - Access shadow git path
* - Initialize simple-git
* - Switch branches
* - Stage or commit files
*/
public async commit(): Promise<string | undefined> {
try {
console.info(`Creating new checkpoint commit for task ${this.taskId}`)
const startTime = performance.now()
const gitPath = await getShadowGitPath(this.globalStoragePath, this.taskId, this.cwdHash)
const git = simpleGit(path.dirname(gitPath))
@@ -170,10 +164,14 @@ class CheckpointTracker {
console.info(`Creating checkpoint commit with message: ${commitMessage}`)
const result = await git.commit(commitMessage, {
"--allow-empty": null,
"--no-verify": null,
})
const commitHash = result.commit || ""
console.warn(`Checkpoint commit created.`)
telemetryService.captureCheckpointUsage(this.taskId, "commit_created")
const durationMs = Math.round(performance.now() - startTime)
telemetryService.captureCheckpointUsage(this.taskId, "commit_created", durationMs)
return commitHash
} catch (error) {
console.error("Failed to create checkpoint:", {
@@ -228,7 +226,6 @@ class CheckpointTracker {
*
* Dependencies:
* - Requires initialized shadow git (getShadowGitPath)
* - For new checkpoints, requires task branch setup
* - Must be called with a valid commit hash from this task's history
*
* @param commitHash - The hash of the checkpoint commit to reset to
@@ -236,18 +233,20 @@ class CheckpointTracker {
* @throws Error if unable to:
* - Access shadow git path
* - Initialize simple-git
* - Switch to task branch
* - Reset to target commit
*/
public async resetHead(commitHash: string): Promise<void> {
console.info(`Resetting to checkpoint: ${commitHash}`)
const startTime = performance.now()
const gitPath = await getShadowGitPath(this.globalStoragePath, this.taskId, this.cwdHash)
const git = simpleGit(path.dirname(gitPath))
console.debug(`Using shadow git at: ${gitPath}`)
await this.gitOperations.switchToTaskBranch(this.taskId, gitPath)
await git.reset(["--hard", commitHash]) // Hard reset to target commit
console.debug(`Successfully reset to checkpoint: ${commitHash}`)
telemetryService.captureCheckpointUsage(this.taskId, "restored")
const durationMs = Math.round(performance.now() - startTime)
telemetryService.captureCheckpointUsage(this.taskId, "restored", durationMs)
}
/**
@@ -264,7 +263,7 @@ class CheckpointTracker {
* @returns Array of file changes with before/after content
*/
public async getDiffSet(
lhsHash?: string,
lhsHash: string,
rhsHash?: string,
): Promise<
Array<{
@@ -274,40 +273,17 @@ class CheckpointTracker {
after: string
}>
> {
const startTime = performance.now()
const gitPath = await getShadowGitPath(this.globalStoragePath, this.taskId, this.cwdHash)
const git = simpleGit(path.dirname(gitPath))
console.info(`Getting diff between commits: ${lhsHash || "initial"} -> ${rhsHash || "working directory"}`)
// If lhsHash is missing, iteratively check up to 5 commits to find the first one with tracked files
let baseHash = lhsHash
if (!baseHash) {
// Ensure we're on the correct task branch before getting commits
await this.gitOperations.switchToTaskBranch(this.taskId, gitPath)
// Verify which branch we're on after switching
const currentBranch = await git.revparse(["--abbrev-ref", "HEAD"])
console.info(`Getting commits from branch: ${currentBranch}`)
try {
// Get all commits that match the checkpoint pattern for this specific task
const commitPattern = `checkpoint-${this.cwdHash}-${this.taskId}`
const branchCommits = await git.log(["--grep", commitPattern, "--reverse"])
if (!branchCommits.all.length) {
throw new Error("No commits found in the branch.")
}
// Get the first commit that matches our task's checkpoint pattern
baseHash = branchCommits.all[0].hash
} catch (error) {
console.error("Failed to get branch commits:", error)
throw new Error("Failed to determine branch history")
}
}
// Stage all changes so that untracked files appear in diff summary
await this.gitOperations.addCheckpointFiles(git)
const diffRange = rhsHash ? `${baseHash}..${rhsHash}` : baseHash
const diffRange = rhsHash ? `${lhsHash}..${rhsHash}` : lhsHash
console.info(`Diff range: ${diffRange}`)
const diffSummary = await git.diffSummary([diffRange])
@@ -318,7 +294,7 @@ class CheckpointTracker {
let beforeContent = ""
try {
beforeContent = await git.show([`${baseHash}:${filePath}`])
beforeContent = await git.show([`${lhsHash}:${filePath}`])
} catch (_) {
// file didn't exist in older commit => remains empty
}
@@ -346,22 +322,38 @@ class CheckpointTracker {
})
}
const durationMs = Math.round(performance.now() - startTime)
telemetryService.captureCheckpointUsage(this.taskId, "diff_generated", durationMs)
return result
}
/**
* Deletes all checkpoint data for a given task.
* Returns the number of files changed between two commits.
*
* @param taskId - The ID of the task whose checkpoints should be deleted
* @param historyItem - The history item containing the shadow git config for this task
* @param globalStoragePath - the globalStorage path
* @throws Error if deletion fails
* @param lhsHash - The commit to compare from (older commit)
* @param rhsHash - The commit to compare to (newer commit).
* If omitted, we compare to the working directory.
* @returns The number of files changed between the commits
*/
public static async deleteCheckpoints(taskId: string, historyItem: HistoryItem, globalStoragePath: string): Promise<void> {
if (!globalStoragePath) {
throw new Error("Global storage uri is invalid")
}
await GitOperations.deleteTaskBranchStatic(taskId, historyItem, globalStoragePath)
public async getDiffCount(lhsHash: string, rhsHash?: string): Promise<number> {
const startTime = performance.now()
const gitPath = await getShadowGitPath(this.globalStoragePath, this.taskId, this.cwdHash)
const git = simpleGit(path.dirname(gitPath))
console.info(`Getting diff count between commits: ${lhsHash || "initial"} -> ${rhsHash || "working directory"}`)
// Stage all changes so that untracked files appear in diff summary
await this.gitOperations.addCheckpointFiles(git)
const diffRange = rhsHash ? `${lhsHash}..${rhsHash}` : lhsHash
const diffSummary = await git.diffSummary([diffRange])
const durationMs = Math.round(performance.now() - startTime)
telemetryService.captureCheckpointUsage(this.taskId, "diff_generated", durationMs)
return diffSummary.files.length
}
}
@@ -5,9 +5,8 @@ import os from "os"
/**
* Gets the path to the shadow Git repository in globalStorage.
* For new checkpoints, uses the consolidated branch-per-task structure.
*
* Branch-per-task path structure:
* Checkpoints path structure:
* globalStorage/
* checkpoints/
* {cwdHash}/
+21 -1
View File
@@ -21,10 +21,22 @@ export class DiffViewProvider {
private activeDiffEditor?: vscode.TextEditor
private fadedOverlayController?: DecorationController
private activeLineController?: DecorationController
private skipAnimation: boolean
private streamedLines: string[] = []
private preDiagnostics: [vscode.Uri, vscode.Diagnostic[]][] = []
private configWatcher: vscode.Disposable
constructor(private cwd: string) {}
constructor(private cwd: string) {
// Initialize skipAnimation from configuration
this.skipAnimation = vscode.workspace.getConfiguration("cline.editor").get("skipDiffAnimation", false)
// Add configuration watcher
this.configWatcher = vscode.workspace.onDidChangeConfiguration((e) => {
if (e.affectsConfiguration("cline.editor.skipDiffAnimation")) {
this.skipAnimation = vscode.workspace.getConfiguration("cline.editor").get("skipDiffAnimation", false)
}
})
}
async open(relPath: string): Promise<void> {
this.relPath = relPath
@@ -96,6 +108,10 @@ export class DiffViewProvider {
const beginningOfDocument = new vscode.Position(0, 0)
diffEditor.selection = new vscode.Selection(beginningOfDocument, beginningOfDocument)
// Stub for skip Animation
if (this.skipAnimation) {
}
for (let i = 0; i < diffLines.length; i++) {
const currentLine = this.streamedLines.length + i
// Replace all content up to the current line with accumulated lines
@@ -374,4 +390,8 @@ export class DiffViewProvider {
this.streamedLines = []
this.preDiagnostics = []
}
disposeAll() {
this.configWatcher.dispose()
}
}
+6
View File
@@ -4,6 +4,7 @@ import pdf from "pdf-parse/lib/pdf-parse"
import mammoth from "mammoth"
import fs from "fs/promises"
import { isBinaryFile } from "isbinaryfile"
import { getFileSizeInKB } from "../../utils/fs"
export async function extractTextFromFile(filePath: string): Promise<string> {
try {
@@ -22,6 +23,11 @@ export async function extractTextFromFile(filePath: string): Promise<string> {
default:
const isBinary = await isBinaryFile(filePath).catch(() => false)
if (!isBinary) {
// If file is over 300KB, throw an error
const fileSizeInKB = await getFileSizeInKB(filePath)
if (fileSizeInKB > 300) {
throw new Error(`File is too large to read into context.`)
}
return await fs.readFile(filePath, "utf8")
} else {
throw new Error(`Cannot read text for file type: ${fileExtension}`)
+13 -8
View File
@@ -1,5 +1,6 @@
import { PostHog } from "posthog-node"
import * as vscode from "vscode"
import { version as extensionVersion } from "../../../package.json"
/**
* PostHogClient handles telemetry event tracking for the Cline extension
@@ -65,6 +66,8 @@ class PostHogClient {
private distinctId: string = vscode.env.machineId
/** Whether telemetry is currently enabled based on user and VSCode settings */
private telemetryEnabled: boolean = false
/** Current version of the extension */
private readonly version: string = extensionVersion
/**
* Private constructor to enforce singleton pattern
@@ -120,7 +123,12 @@ class PostHogClient {
public capture(event: { event: string; properties?: any }): void {
// Only send events if telemetry is enabled
if (this.telemetryEnabled) {
this.client.capture({ distinctId: this.distinctId, event: event.event, properties: event.properties })
// Include extension version in all event properties
const propertiesWithVersion = {
...event.properties,
extension_version: this.version,
}
this.client.capture({ distinctId: this.distinctId, event: event.event, properties: propertiesWithVersion })
}
}
@@ -250,22 +258,19 @@ class PostHogClient {
* Records interactions with the git-based checkpoint system
* @param taskId Unique identifier for the task
* @param action The type of checkpoint action
* @param durationMs Optional duration of the operation in milliseconds
*/
public captureCheckpointUsage(
taskId: string,
action:
| "shadow_git_initialized"
| "commit_created"
| "branch_created"
| "branch_deleted_active"
| "branch_deleted_inactive"
| "restored",
action: "shadow_git_initialized" | "commit_created" | "restored" | "diff_generated",
durationMs?: number,
) {
this.capture({
event: PostHogClient.EVENTS.TASK.CHECKPOINT_USED,
properties: {
taskId,
action,
durationMs,
},
})
}
+2
View File
@@ -84,6 +84,8 @@ function separateFiles(allFiles: string[]): {
"java",
"php",
"swift",
// Kotlin
"kt",
].map((e) => `.${e}`)
const filesToParse = allFiles.filter((file) => extensions.includes(path.extname(file))).slice(0, 50) // 50 files max
const remainingFiles = allFiles.filter((file) => !filesToParse.includes(file))
@@ -13,6 +13,7 @@ import {
javaQuery,
phpQuery,
swiftQuery,
kotlinQuery,
} from "./queries"
export interface LanguageParser {
@@ -120,6 +121,10 @@ export async function loadRequiredLanguageParsers(filesToParse: string[]): Promi
language = await loadLanguage("swift")
query = language.query(swiftQuery)
break
case "kt":
language = await loadLanguage("kotlin")
query = language.query(kotlinQuery)
break
default:
throw new Error(`Unsupported language: ${ext}`)
}
@@ -10,3 +10,4 @@ export { default as cQuery } from "./c"
export { default as csharpQuery } from "./c-sharp"
export { default as goQuery } from "./go"
export { default as swiftQuery } from "./swift"
export { default as kotlinQuery } from "./kotlin"
@@ -0,0 +1,22 @@
export default `
(class_declaration
name: (simple_identifier) @name.definition.class) @definition.class
(function_declaration
name: (simple_identifier) @name.definition.function) @definition.function
(interface_declaration
name: (simple_identifier) @name.definition.interface) @definition.interface
(object_declaration
name: (simple_identifier) @name.definition.object) @definition.object
(property_declaration
name: (simple_identifier) @name.definition.property) @definition.property
(enum_declaration
name: (simple_identifier) @name.definition.enum) @definition.enum
(typealias_declaration
name: (simple_identifier) @name.definition.typealias) @definition.typealias
)`
+26 -10
View File
@@ -33,6 +33,8 @@ export interface ExtensionMessage {
| "commitSearchResults"
| "openGraphData"
| "isImageUrlResult"
| "didUpdateSettings"
| "totalTasksSize"
text?: string
action?:
| "chatButtonClicked"
@@ -68,6 +70,7 @@ export interface ExtensionMessage {
}
url?: string
isImage?: boolean
totalTasksSize?: number | null
}
export type Invoke = "sendMessage" | "primaryButtonClick" | "secondaryButtonClick"
@@ -77,26 +80,27 @@ export type Platform = "aix" | "darwin" | "freebsd" | "linux" | "openbsd" | "sun
export const DEFAULT_PLATFORM = "unknown"
export interface ExtensionState {
version: string
apiConfiguration?: ApiConfiguration
customInstructions?: string
uriScheme?: string
currentTaskItem?: HistoryItem
checkpointTrackerErrorMessage?: string
clineMessages: ClineMessage[]
taskHistory: HistoryItem[]
shouldShowAnnouncement: boolean
autoApprovalSettings: AutoApprovalSettings
browserSettings: BrowserSettings
chatSettings: ChatSettings
checkpointTrackerErrorMessage?: string
clineMessages: ClineMessage[]
currentTaskItem?: HistoryItem
customInstructions?: string
mcpMarketplaceEnabled?: boolean
planActSeparateModelsSetting: boolean
platform: Platform
shouldShowAnnouncement: boolean
taskHistory: HistoryItem[]
telemetrySetting: TelemetrySetting
uriScheme?: string
userInfo?: {
displayName: string | null
email: string | null
photoURL: string | null
}
mcpMarketplaceEnabled?: boolean
telemetrySetting: TelemetrySetting
version: string
vscMachineId: string
}
@@ -197,6 +201,18 @@ export interface ClineAskUseMcpServer {
uri?: string
}
export interface ClinePlanModeResponse {
response: string
options?: string[]
selected?: string
}
export interface ClineAskQuestion {
question: string
options?: string[]
selected?: string
}
export interface ClineApiReqInfo {
request?: string
tokensIn?: number
+8 -1
View File
@@ -4,11 +4,11 @@ import { BrowserSettings } from "./BrowserSettings"
import { ChatSettings } from "./ChatSettings"
import { UserInfo } from "./UserInfo"
import { ChatContent } from "./ChatContent"
import { TelemetrySetting } from "./TelemetrySetting"
export interface WebviewMessage {
type:
| "apiConfiguration"
| "customInstructions"
| "webviewDidLaunch"
| "newTask"
| "askResponse"
@@ -59,6 +59,10 @@ export interface WebviewMessage {
| "fetchOpenGraphData"
| "checkIsImageUrl"
| "invoke"
| "updateSettings"
| "clearAllTaskHistory"
| "optionsResponse"
| "requestTotalTasksSize"
// | "relaunchChromeDebugMode"
text?: string
disabled?: boolean
@@ -83,6 +87,9 @@ export interface WebviewMessage {
customToken?: string
// For openInBrowser
url?: string
planActSeparateModelsSetting?: boolean
telemetrySetting?: TelemetrySetting
customInstructionsSetting?: string
}
export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse"
+341 -5
View File
@@ -18,6 +18,7 @@ export type ApiProvider =
| "litellm"
| "asksage"
| "xai"
| "sambanova"
export interface ApiHandlerOptions {
apiModelId?: string
@@ -38,12 +39,13 @@ export interface ApiHandlerOptions {
awsBedrockUsePromptCache?: boolean
awsUseProfile?: boolean
awsProfile?: string
awsBedrockEndpoint?: string
vertexProjectId?: string
vertexRegion?: string
openAiBaseUrl?: string
openAiApiKey?: string
openAiModelId?: string
openAiModelInfo?: ModelInfo
openAiModelInfo?: OpenAiCompatibleModelInfo
ollamaModelId?: string
ollamaBaseUrl?: string
ollamaApiOptionsCtxNum?: string
@@ -66,6 +68,7 @@ export interface ApiHandlerOptions {
asksageApiKey?: string
xaiApiKey?: string
thinkingBudgetTokens?: number
sambanovaApiKey?: string
}
export type ApiConfiguration = ApiHandlerOptions & {
@@ -87,6 +90,10 @@ export interface ModelInfo {
description?: string
}
export interface OpenAiCompatibleModelInfo extends ModelInfo {
temperature?: number
}
// Anthropic
// https://docs.anthropic.com/en/docs/about-claude/models // prices updated 2025-01-02
export type AnthropicModelId = keyof typeof anthropicModels
@@ -215,6 +222,14 @@ export const bedrockModels = {
inputPrice: 0.25,
outputPrice: 1.25,
},
"deepseek.r1-v1:0": {
maxTokens: 8_000,
contextWindow: 64_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 1.35,
outputPrice: 5.4,
},
} as const satisfies Record<string, ModelInfo>
// OpenRouter
@@ -389,13 +404,14 @@ export const vertexModels = {
},
} as const satisfies Record<string, ModelInfo>
export const openAiModelInfoSaneDefaults: ModelInfo = {
export const openAiModelInfoSaneDefaults: OpenAiCompatibleModelInfo = {
maxTokens: -1,
contextWindow: 128_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
temperature: 0,
}
// Gemini
@@ -605,9 +621,11 @@ export const deepSeekModels = {
// Qwen
// https://bailian.console.aliyun.com/
export type QwenModelId = keyof typeof qwenModels
export const qwenDefaultModelId: QwenModelId = "qwen-coder-plus-latest"
export const qwenModels = {
export type MainlandQwenModelId = keyof typeof mainlandQwenModels
export type InternationalQwenModelId = keyof typeof internationalQwenModels
export const internationalQwenDefaultModelId: InternationalQwenModelId = "qwen-coder-plus-latest"
export const mainlandQwenDefaultModelId: MainlandQwenModelId = "qwen-coder-plus-latest"
export const internationalQwenModels = {
"qwen2.5-coder-32b-instruct": {
maxTokens: 8_192,
contextWindow: 131_072,
@@ -810,6 +828,229 @@ export const qwenModels = {
},
} as const satisfies Record<string, ModelInfo>
export const mainlandQwenModels = {
"qwen2.5-coder-32b-instruct": {
maxTokens: 8_192,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.002,
outputPrice: 0.006,
cacheWritesPrice: 0.002,
cacheReadsPrice: 0.006,
},
"qwen2.5-coder-14b-instruct": {
maxTokens: 8_192,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.002,
outputPrice: 0.006,
cacheWritesPrice: 0.002,
cacheReadsPrice: 0.006,
},
"qwen2.5-coder-7b-instruct": {
maxTokens: 8_192,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.001,
outputPrice: 0.002,
cacheWritesPrice: 0.001,
cacheReadsPrice: 0.002,
},
"qwen2.5-coder-3b-instruct": {
maxTokens: 8_192,
contextWindow: 32_768,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.0,
outputPrice: 0.0,
cacheWritesPrice: 0.0,
cacheReadsPrice: 0.0,
},
"qwen2.5-coder-1.5b-instruct": {
maxTokens: 8_192,
contextWindow: 32_768,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.0,
outputPrice: 0.0,
cacheWritesPrice: 0.0,
cacheReadsPrice: 0.0,
},
"qwen2.5-coder-0.5b-instruct": {
maxTokens: 8_192,
contextWindow: 32_768,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.0,
outputPrice: 0.0,
cacheWritesPrice: 0.0,
cacheReadsPrice: 0.0,
},
"qwen-coder-plus-latest": {
maxTokens: 129_024,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 3.5,
outputPrice: 7,
cacheWritesPrice: 3.5,
cacheReadsPrice: 7,
},
"qwen-plus-latest": {
maxTokens: 129_024,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.8,
outputPrice: 2,
cacheWritesPrice: 0.8,
cacheReadsPrice: 0.2,
},
"qwen-turbo-latest": {
maxTokens: 1_000_000,
contextWindow: 1_000_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.8,
outputPrice: 2,
cacheWritesPrice: 0.8,
cacheReadsPrice: 2,
},
"qwen-max-latest": {
maxTokens: 30_720,
contextWindow: 32_768,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 2.4,
outputPrice: 9.6,
cacheWritesPrice: 2.4,
cacheReadsPrice: 9.6,
},
"qwq-plus-latest": {
maxTokens: 8_192,
contextWindow: 131_071,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.0,
outputPrice: 0.0,
cacheWritesPrice: 0.0,
cacheReadsPrice: 0.0,
},
"qwq-plus": {
maxTokens: 8_192,
contextWindow: 131_071,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.0,
outputPrice: 0.0,
cacheWritesPrice: 0.0,
cacheReadsPrice: 0.0,
},
"qwen-coder-plus": {
maxTokens: 129_024,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 3.5,
outputPrice: 7,
cacheWritesPrice: 3.5,
cacheReadsPrice: 7,
},
"qwen-plus": {
maxTokens: 129_024,
contextWindow: 131_072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.8,
outputPrice: 2,
cacheWritesPrice: 0.8,
cacheReadsPrice: 0.2,
},
"qwen-turbo": {
maxTokens: 1_000_000,
contextWindow: 1_000_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.3,
outputPrice: 0.6,
cacheWritesPrice: 0.3,
cacheReadsPrice: 0.6,
},
"qwen-max": {
maxTokens: 30_720,
contextWindow: 32_768,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 2.4,
outputPrice: 9.6,
cacheWritesPrice: 2.4,
cacheReadsPrice: 9.6,
},
"deepseek-v3": {
maxTokens: 8_000,
contextWindow: 64_000,
supportsImages: false,
supportsPromptCache: true,
inputPrice: 0,
outputPrice: 0.28,
cacheWritesPrice: 0.14,
cacheReadsPrice: 0.014,
},
"deepseek-r1": {
maxTokens: 8_000,
contextWindow: 64_000,
supportsImages: false,
supportsPromptCache: true,
inputPrice: 0,
outputPrice: 2.19,
cacheWritesPrice: 0.55,
cacheReadsPrice: 0.14,
},
"qwen-vl-max": {
maxTokens: 30_720,
contextWindow: 32_768,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 3,
outputPrice: 9,
cacheWritesPrice: 3,
cacheReadsPrice: 9,
},
"qwen-vl-max-latest": {
maxTokens: 129_024,
contextWindow: 131_072,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 3,
outputPrice: 9,
cacheWritesPrice: 3,
cacheReadsPrice: 9,
},
"qwen-vl-plus": {
maxTokens: 6_000,
contextWindow: 8_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 1.5,
outputPrice: 4.5,
cacheWritesPrice: 1.5,
cacheReadsPrice: 4.5,
},
"qwen-vl-plus-latest": {
maxTokens: 129_024,
contextWindow: 131_072,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 1.5,
outputPrice: 4.5,
cacheWritesPrice: 1.5,
cacheReadsPrice: 4.5,
},
} as const satisfies Record<string, ModelInfo>
// Mistral
// https://docs.mistral.ai/getting-started/models/models_overview/
export type MistralModelId = keyof typeof mistralModels
@@ -847,6 +1088,14 @@ export const mistralModels = {
inputPrice: 0.1,
outputPrice: 0.1,
},
"mistral-small-latest": {
maxTokens: 131_000,
contextWindow: 131_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0.1,
outputPrice: 0.3,
},
"mistral-small-2501": {
maxTokens: 32_000,
contextWindow: 32_000,
@@ -1028,3 +1277,90 @@ export const xaiModels = {
description: "X AI's Grok Beta model (legacy) with 131K context window",
},
} as const satisfies Record<string, ModelInfo>
// SambaNova
// https://docs.sambanova.ai/cloud/docs/get-started/supported-models
export type SambanovaModelId = keyof typeof sambanovaModels
export const sambanovaDefaultModelId: SambanovaModelId = "Meta-Llama-3.3-70B-Instruct"
export const sambanovaModels = {
"Meta-Llama-3.3-70B-Instruct": {
maxTokens: 4096,
contextWindow: 128_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"DeepSeek-R1-Distill-Llama-70B": {
maxTokens: 4096,
contextWindow: 32_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"Llama-3.1-Swallow-70B-Instruct-v0.3": {
maxTokens: 4096,
contextWindow: 16_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"Llama-3.1-Swallow-8B-Instruct-v0.3": {
maxTokens: 4096,
contextWindow: 16_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"Meta-Llama-3.1-405B-Instruct": {
maxTokens: 4096,
contextWindow: 16_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"Meta-Llama-3.1-8B-Instruct": {
maxTokens: 4096,
contextWindow: 16_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"Meta-Llama-3.2-1B-Instruct": {
maxTokens: 4096,
contextWindow: 16_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"Qwen2.5-72B-Instruct": {
maxTokens: 4096,
contextWindow: 16_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"Qwen2.5-Coder-32B-Instruct": {
maxTokens: 4096,
contextWindow: 16_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
"QwQ-32B-Preview": {
maxTokens: 4096,
contextWindow: 16_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
},
} as const satisfies Record<string, ModelInfo>
+34
View File
@@ -20,3 +20,37 @@ export function findLast<T>(array: Array<T>, predicate: (value: T, index: number
const index = findLastIndex(array, predicate)
return index === -1 ? undefined : array[index]
}
/**
* Converts a partial or complete stringified array into an actual array.
* Handles both complete JSON strings and incomplete array strings.
* Splits on the specific tokens: [" ", " "]
* @param arrayString A string representation of an array, which may be incomplete
* @returns Array of strings parsed from the input
*/
export function parsePartialArrayString(arrayString: string): string[] {
try {
// Try parsing as complete JSON first
return JSON.parse(arrayString)
} catch {
// If JSON parsing fails, handle as partial string
const trimmed = arrayString.trim()
if (!trimmed.startsWith('["')) {
return []
}
// Remove leading ["
let content = trimmed.slice(2)
// Remove trailing "] if it exists
content = content.replace(/"]$/, "")
if (!content) {
return []
}
// Split on ", " token and handle the parts
return content
.split('", "')
.map((item) => item.trim())
.filter(Boolean)
}
}
+21 -1
View File
@@ -3,7 +3,7 @@ import { after, describe, it } from "mocha"
import * as os from "os"
import * as path from "path"
import "should"
import { createDirectoriesForFile, fileExistsAtPath } from "./fs"
import { createDirectoriesForFile, fileExistsAtPath, isDirectory } from "./fs"
describe("Filesystem Utilities", () => {
const tmpDir = path.join(os.tmpdir(), "cline-test-" + Math.random().toString(36).slice(2))
@@ -68,4 +68,24 @@ describe("Filesystem Utilities", () => {
exists.should.be.true()
})
})
describe("isDirectory", () => {
it("should return true for directories", async () => {
await fs.mkdir(tmpDir, { recursive: true })
const isDir = await isDirectory(tmpDir)
isDir.should.be.true()
})
it("should return false for files", async () => {
const testFile = path.join(tmpDir, "test.txt")
await fs.writeFile(testFile, "test")
const isDir = await isDirectory(testFile)
isDir.should.be.false()
})
it("should return false for non-existent paths", async () => {
const nonExistentPath = path.join(tmpDir, "does-not-exist")
const isDir = await isDirectory(nonExistentPath)
isDir.should.be.false()
})
})
})
+29
View File
@@ -45,3 +45,32 @@ export async function fileExistsAtPath(filePath: string): Promise<boolean> {
return false
}
}
/**
* Checks if the path is a directory
* @param filePath - The path to check.
* @returns A promise that resolves to true if the path is a directory, false otherwise.
*/
export async function isDirectory(filePath: string): Promise<boolean> {
try {
const stats = await fs.stat(filePath)
return stats.isDirectory()
} catch {
return false
}
}
/**
* Gets the size of a file in kilobytes
* @param filePath - Path to the file to check
* @returns Promise<number> - Size of the file in KB, or 0 if file doesn't exist
*/
export async function getFileSizeInKB(filePath: string): Promise<number> {
try {
const stats = await fs.stat(filePath)
const fileSizeInKB = stats.size / 1000 // Convert bytes to KB (decimal) - matches OS file size display
return fileSizeInKB
} catch {
return 0
}
}
+21
View File
@@ -0,0 +1,21 @@
import path from "path"
import getFolderSize from "get-folder-size"
/**
* Gets the total size of tasks and checkpoints directories
* @param storagePath The base storage path (typically globalStorageUri.fsPath)
* @returns The total size in bytes, or null if calculation fails
*/
export async function getTotalTasksSize(storagePath: string): Promise<number | null> {
const tasksDir = path.join(storagePath, "tasks")
const checkpointsDir = path.join(storagePath, "checkpoints")
try {
const tasksSize = await getFolderSize.loose(tasksDir)
const checkpointsSize = await getFolderSize.loose(checkpointsDir)
return tasksSize + checkpointsSize
} catch (error) {
console.error("Failed to calculate total task size:", error)
return null
}
}
+527 -9
View File
@@ -31,6 +31,7 @@
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@tailwindcss/vite": "^4.0.12",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^14.6.1",
@@ -45,6 +46,7 @@
"eslint-plugin-react-refresh": "^0.4.16",
"globals": "^15.14.0",
"jsdom": "^26.0.0",
"tailwindcss": "^4.0.12",
"typescript": "^5.7.3",
"typescript-eslint": "^8.18.2",
"vite": "^6.1.1",
@@ -125,9 +127,9 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz",
"integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==",
"version": "7.26.10",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz",
"integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==",
"license": "MIT",
"dependencies": {
"regenerator-runtime": "^0.14.0"
@@ -2438,6 +2440,244 @@
"@swc/counter": "^0.1.3"
}
},
"node_modules/@tailwindcss/node": {
"version": "4.0.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.0.12.tgz",
"integrity": "sha512-a6J11K1Ztdln9OrGfoM75/hChYPcHYGNYimqciMrvKXRmmPaS8XZTHhdvb5a3glz4Kd4ZxE1MnuFE2c0fGGmtg==",
"dev": true,
"license": "MIT",
"dependencies": {
"enhanced-resolve": "^5.18.1",
"jiti": "^2.4.2",
"tailwindcss": "4.0.12"
}
},
"node_modules/@tailwindcss/oxide": {
"version": "4.0.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.0.12.tgz",
"integrity": "sha512-DWb+myvJB9xJwelwT9GHaMc1qJj6MDXRDR0CS+T8IdkejAtu8ctJAgV4r1drQJLPeS7mNwq2UHW2GWrudTf63A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 10"
},
"optionalDependencies": {
"@tailwindcss/oxide-android-arm64": "4.0.12",
"@tailwindcss/oxide-darwin-arm64": "4.0.12",
"@tailwindcss/oxide-darwin-x64": "4.0.12",
"@tailwindcss/oxide-freebsd-x64": "4.0.12",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.12",
"@tailwindcss/oxide-linux-arm64-gnu": "4.0.12",
"@tailwindcss/oxide-linux-arm64-musl": "4.0.12",
"@tailwindcss/oxide-linux-x64-gnu": "4.0.12",
"@tailwindcss/oxide-linux-x64-musl": "4.0.12",
"@tailwindcss/oxide-win32-arm64-msvc": "4.0.12",
"@tailwindcss/oxide-win32-x64-msvc": "4.0.12"
}
},
"node_modules/@tailwindcss/oxide-android-arm64": {
"version": "4.0.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.0.12.tgz",
"integrity": "sha512-dAXCaemu3mHLXcA5GwGlQynX8n7tTdvn5i1zAxRvZ5iC9fWLl5bGnjZnzrQqT7ttxCvRwdVf3IHUnMVdDBO/kQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
"version": "4.0.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.0.12.tgz",
"integrity": "sha512-vPNI+TpJQ7sizselDXIJdYkx9Cu6JBdtmRWujw9pVIxW8uz3O2PjgGGzL/7A0sXI8XDjSyRChrUnEW9rQygmJQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-darwin-x64": {
"version": "4.0.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.0.12.tgz",
"integrity": "sha512-RL/9jM41Fdq4Efr35C5wgLx98BirnrfwuD+zgMFK6Ir68HeOSqBhW9jsEeC7Y/JcGyPd3MEoJVIU4fAb7YLg7A==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-freebsd-x64": {
"version": "4.0.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.0.12.tgz",
"integrity": "sha512-7WzWiax+LguJcMEimY0Q4sBLlFXu1tYxVka3+G2M9KmU/3m84J3jAIV4KZWnockbHsbb2XgrEjtlJKVwHQCoRA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
"version": "4.0.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.0.12.tgz",
"integrity": "sha512-X9LRC7jjE1QlfIaBbXjY0PGeQP87lz5mEfLSVs2J1yRc9PSg1tEPS9NBqY4BU9v5toZgJgzKeaNltORyTs22TQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
"version": "4.0.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.0.12.tgz",
"integrity": "sha512-i24IFNq2402zfDdoWKypXz0ZNS2G4NKaA82tgBlE2OhHIE+4mg2JDb5wVfyP6R+MCm5grgXvurcIcKWvo44QiQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
"version": "4.0.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.0.12.tgz",
"integrity": "sha512-LmOdshJBfAGIBG0DdBWhI0n5LTMurnGGJCHcsm9F//ISfsHtCnnYIKgYQui5oOz1SUCkqsMGfkAzWyNKZqbGNw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
"version": "4.0.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.0.12.tgz",
"integrity": "sha512-OSK667qZRH30ep8RiHbZDQfqkXjnzKxdn0oRwWzgCO8CoTxV+MvIkd0BWdQbYtYuM1wrakARV/Hwp0eA/qzdbw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
"version": "4.0.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.0.12.tgz",
"integrity": "sha512-uylhWq6OWQ8krV8Jk+v0H/3AZKJW6xYMgNMyNnUbbYXWi7hIVdxRKNUB5UvrlC3RxtgsK5EAV2i1CWTRsNcAnA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.0.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.0.12.tgz",
"integrity": "sha512-XDLnhMoXZEEOir1LK43/gHHwK84V1GlV8+pAncUAIN2wloeD+nNciI9WRIY/BeFTqES22DhTIGoilSO39xDb2g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
"version": "4.0.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.0.12.tgz",
"integrity": "sha512-I/BbjCLpKDQucvtn6rFuYLst1nfFwSMYyPzkx/095RE+tuzk5+fwXuzQh7T3fIBTcbn82qH/sFka7yPGA50tLw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/vite": {
"version": "4.0.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.0.12.tgz",
"integrity": "sha512-JM3gp601UJiryIZ9R2bSqalzcOy15RCybQ1Q+BJqDEwVyo4LkWKeqQAcrpHapWXY31OJFTuOUVBFDWMhzHm2Bg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@tailwindcss/node": "4.0.12",
"@tailwindcss/oxide": "4.0.12",
"lightningcss": "^1.29.1",
"tailwindcss": "4.0.12"
},
"peerDependencies": {
"vite": "^5.2.0 || ^6"
}
},
"node_modules/@testing-library/dom": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
@@ -4585,6 +4825,16 @@
"node": ">=6"
}
},
"node_modules/detect-libc": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/devlop": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
@@ -4620,6 +4870,20 @@
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/enhanced-resolve": {
"version": "5.18.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
"integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
"tapable": "^2.2.0"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/error-stack-parser": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
@@ -5582,15 +5846,13 @@
"license": "ISC"
},
"node_modules/jiti": {
"version": "1.21.7",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
"integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"bin": {
"jiti": "bin/jiti.js"
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/js-cookie": {
@@ -5749,6 +6011,245 @@
"node": ">= 0.8.0"
}
},
"node_modules/lightningcss": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz",
"integrity": "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==",
"dev": true,
"license": "MPL-2.0",
"dependencies": {
"detect-libc": "^2.0.3"
},
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"lightningcss-darwin-arm64": "1.29.2",
"lightningcss-darwin-x64": "1.29.2",
"lightningcss-freebsd-x64": "1.29.2",
"lightningcss-linux-arm-gnueabihf": "1.29.2",
"lightningcss-linux-arm64-gnu": "1.29.2",
"lightningcss-linux-arm64-musl": "1.29.2",
"lightningcss-linux-x64-gnu": "1.29.2",
"lightningcss-linux-x64-musl": "1.29.2",
"lightningcss-win32-arm64-msvc": "1.29.2",
"lightningcss-win32-x64-msvc": "1.29.2"
}
},
"node_modules/lightningcss-darwin-arm64": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.2.tgz",
"integrity": "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-darwin-x64": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.2.tgz",
"integrity": "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-freebsd-x64": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.2.tgz",
"integrity": "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm-gnueabihf": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.2.tgz",
"integrity": "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-gnu": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.2.tgz",
"integrity": "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-musl": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.2.tgz",
"integrity": "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-gnu": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.2.tgz",
"integrity": "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-musl": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.2.tgz",
"integrity": "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-arm64-msvc": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.2.tgz",
"integrity": "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-x64-msvc": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.2.tgz",
"integrity": "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/local-pkg": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.0.0.tgz",
@@ -7298,6 +7799,23 @@
"integrity": "sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA==",
"license": "MIT"
},
"node_modules/tailwindcss": {
"version": "4.0.12",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.12.tgz",
"integrity": "sha512-bT0hJo91FtncsAMSsMzUkoo/iEU0Xs5xgFgVC9XmdM9bw5MhZuQFjPNl6wxAE0SiQF/YTZJa+PndGWYSDtuxAg==",
"dev": true,
"license": "MIT"
},
"node_modules/tapable": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/terser": {
"version": "5.37.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz",
+2
View File
@@ -35,6 +35,7 @@
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@tailwindcss/vite": "^4.0.12",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^14.6.1",
@@ -49,6 +50,7 @@
"eslint-plugin-react-refresh": "^0.4.16",
"globals": "^15.14.0",
"jsdom": "^26.0.0",
"tailwindcss": "^4.0.12",
"typescript": "^5.7.3",
"typescript-eslint": "^8.18.2",
"vite": "^6.1.1",
+77 -24
View File
@@ -5,8 +5,10 @@ import { useEvent, useSize } from "react-use"
import styled from "styled-components"
import {
ClineApiReqInfo,
ClineAskQuestion,
ClineAskUseMcpServer,
ClineMessage,
ClinePlanModeResponse,
ClineSayTool,
COMPLETION_RESULT_CHANGES_FLAG,
ExtensionMessage,
@@ -15,18 +17,18 @@ import { COMMAND_OUTPUT_STRING, COMMAND_REQ_APP_STRING } from "../../../../src/s
import { useExtensionState } from "../../context/ExtensionStateContext"
import { findMatchingResourceOrTemplate, getMcpServerDisplayName } from "../../utils/mcp"
import { vscode } from "../../utils/vscode"
import { CheckmarkControl } from "../common/CheckmarkControl"
import { CheckpointControls, CheckpointOverlay } from "../common/CheckpointControls"
import CodeAccordian, { cleanPathPrefix } from "../common/CodeAccordian"
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
import MarkdownBlock from "../common/MarkdownBlock"
import SuccessButton from "../common/SuccessButton"
import Thumbnails from "../common/Thumbnails"
import McpResourceRow from "../mcp/McpResourceRow"
import McpToolRow from "../mcp/McpToolRow"
import { highlightMentions } from "./TaskHeader"
import CreditLimitError from "./CreditLimitError"
import { CheckmarkControl } from "../common/CheckmarkControl"
import McpResponseDisplay from "../mcp/McpResponseDisplay"
import { OptionsButtons } from "./OptionsButtons"
import { highlightMentions } from "./TaskHeader"
import SuccessButton from "../common/SuccessButton"
const ChatRowContainer = styled.div`
padding: 10px 6px 10px 15px;
@@ -790,8 +792,30 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
)
case "api_req_finished":
return null // we should never see this message type
// case "mcp_server_response":
// return <McpResponseDisplay responseText={message.text || ""} />
case "mcp_server_response":
return <McpResponseDisplay responseText={message.text || ""} />
return (
<>
<div style={{ paddingTop: 0 }}>
<div
style={{
marginBottom: "4px",
opacity: 0.8,
fontSize: "12px",
textTransform: "uppercase",
}}>
Response
</div>
<CodeAccordian
code={message.text}
language="json"
isExpanded={true}
onToggleExpand={onToggleExpand}
/>
</div>
</>
)
case "text":
return (
<div>
@@ -912,10 +936,12 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
style={{
display: "flex",
flexDirection: "column",
backgroundColor: "rgba(255, 191, 0, 0.1)",
backgroundColor: "var(--vscode-textBlockQuote-background)",
padding: 8,
borderRadius: 3,
fontSize: 12,
color: "var(--vscode-foreground)",
opacity: 0.8,
}}>
<div
style={{
@@ -924,24 +950,15 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
marginBottom: 4,
}}>
<i
className="codicon codicon-error"
className="codicon codicon-warning"
style={{
marginRight: 8,
fontSize: 18,
color: "#FFA500",
fontSize: 14,
color: "var(--vscode-descriptionForeground)",
}}></i>
<span
style={{
fontWeight: 500,
color: "#FFA500",
}}>
Diff Edit Failed
</span>
</div>
<div>
This usually happens when the model uses search patterns that don't match anything in the
file. Retrying...
<span style={{ fontWeight: 500 }}>Diff Edit Mismatch</span>
</div>
<div>The model used search patterns that don't match anything in the file. Retrying...</div>
</div>
</>
)
@@ -1024,8 +1041,8 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
})
}}
style={{
width: "100%",
cursor: seeNewChangesDisabled ? "wait" : "pointer",
width: "100%",
}}>
<i className="codicon codicon-new-file" style={{ marginRight: 6 }} />
See new changes
@@ -1183,6 +1200,19 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
return null // Don't render anything when we get a completion_result ask without text
}
case "followup":
let question: string | undefined
let options: string[] | undefined
let selected: string | undefined
try {
const parsedMessage = JSON.parse(message.text || "{}") as ClineAskQuestion
question = parsedMessage.question
options = parsedMessage.options
selected = parsedMessage.selected
} catch (e) {
// legacy messages would pass question directly
question = message.text
}
return (
<>
{title && (
@@ -1192,16 +1222,39 @@ export const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifi
</div>
)}
<div style={{ paddingTop: 10 }}>
<Markdown markdown={message.text} />
<Markdown markdown={question} />
<OptionsButtons
options={options}
selected={selected}
isActive={isLast && lastModifiedMessage?.ask === "followup"}
/>
</div>
</>
)
case "plan_mode_response":
case "plan_mode_response": {
let response: string | undefined
let options: string[] | undefined
let selected: string | undefined
try {
const parsedMessage = JSON.parse(message.text || "{}") as ClinePlanModeResponse
response = parsedMessage.response
options = parsedMessage.options
selected = parsedMessage.selected
} catch (e) {
// legacy messages would pass response directly
response = message.text
}
return (
<div style={{}}>
<Markdown markdown={message.text} />
<Markdown markdown={response} />
<OptionsButtons
options={options}
selected={selected}
isActive={isLast && lastModifiedMessage?.ask === "plan_mode_response"}
/>
</div>
)
}
default:
return null
}
@@ -1132,6 +1132,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
</ModelContainer>
</ButtonGroup>
<Tooltip
style={{ zIndex: 1000 }}
visible={shownTooltipMode !== null}
tipText={`In ${shownTooltipMode === "act" ? "Act" : "Plan"} mode, Cline will ${shownTooltipMode === "act" ? "complete the task immediately" : "gather information to architect a plan"}`}
hintText={`Toggle w/ ${metaKeyChar}+Shift+A`}>
@@ -0,0 +1,69 @@
import styled from "styled-components"
import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
import { vscode } from "../../utils/vscode"
const OptionButton = styled.button<{ isSelected?: boolean; isNotSelectable?: boolean }>`
padding: 8px 12px;
background: ${(props) => (props.isSelected ? "var(--vscode-focusBorder)" : CODE_BLOCK_BG_COLOR)};
color: ${(props) => (props.isSelected ? "white" : "var(--vscode-input-foreground)")};
border: 1px solid var(--vscode-editorGroup-border);
border-radius: 2px;
cursor: ${(props) => (props.isNotSelectable ? "default" : "pointer")};
text-align: left;
font-size: 12px;
${(props) =>
!props.isNotSelectable &&
`
&:hover {
background: var(--vscode-focusBorder);
color: white;
}
`}
`
export const OptionsButtons = ({
options,
selected,
isActive,
}: {
options?: string[]
selected?: string
isActive?: boolean
}) => {
if (!options?.length) return null
const hasSelected = selected !== undefined && options.includes(selected)
return (
<div
style={{
display: "flex",
flexDirection: "column",
gap: "8px",
paddingTop: 15,
// marginTop: "22px",
}}>
{/* <div style={{ color: "var(--vscode-descriptionForeground)", fontSize: "11px", textTransform: "uppercase" }}>
SELECT ONE:
</div> */}
{options.map((option, index) => (
<OptionButton
key={index}
isSelected={option === selected}
isNotSelectable={hasSelected || !isActive}
onClick={() => {
if (hasSelected || !isActive) {
return
}
vscode.postMessage({
type: "optionsResponse",
text: option,
})
}}>
{option}
</OptionButton>
))}
</div>
)
}
+72 -41
View File
@@ -34,7 +34,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
onClose,
}) => {
const { apiConfiguration, currentTaskItem, checkpointTrackerErrorMessage } = useExtensionState()
const [isTaskExpanded, setIsTaskExpanded] = useState(true)
const [isTaskExpanded, setIsTaskExpanded] = useState(false)
const [isTextExpanded, setIsTextExpanded] = useState(false)
const [showSeeMore, setShowSeeMore] = useState(false)
const textContainerRef = useRef<HTMLDivElement>(null)
@@ -43,6 +43,15 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
const { selectedModelInfo } = useMemo(() => normalizeApiConfiguration(apiConfiguration), [apiConfiguration])
const contextWindow = selectedModelInfo?.contextWindow
// Open task header when checkpoint tracker error message is set
const prevErrorMessageRef = useRef(checkpointTrackerErrorMessage)
useEffect(() => {
if (checkpointTrackerErrorMessage !== prevErrorMessageRef.current) {
setIsTaskExpanded(true)
prevErrorMessageRef.current = checkpointTrackerErrorMessage
}
}, [checkpointTrackerErrorMessage])
/*
When dealing with event listeners in React components that depend on state variables, we face a challenge. We want our listener to always use the most up-to-date version of a callback function that relies on current state, but we don't want to constantly add and remove event listeners as that function updates. This scenario often arises with resize listeners or other window events. Simply adding the listener in a useEffect with an empty dependency array risks using stale state, while including the callback in the dependencies can lead to unnecessary re-registrations of the listener. There are react hook libraries that provide a elegant solution to this problem by utilizing the useRef hook to maintain a reference to the latest callback function without triggering re-renders or effect re-runs. This approach ensures that our event listener always has access to the most current state while minimizing performance overhead and potential memory leaks from multiple listener registrations.
Sources
@@ -85,19 +94,20 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
}, [isTextExpanded, windowHeight])
useEffect(() => {
if (textRef.current && textContainerRef.current) {
if (isTaskExpanded && textRef.current && textContainerRef.current) {
let textContainerHeight = textContainerRef.current.clientHeight
if (!textContainerHeight) {
textContainerHeight = textContainerRef.current.getBoundingClientRect().height
}
const isOverflowing = textRef.current.scrollHeight > textContainerHeight
// necessary to show see more button again if user resizes window to expand and then back to collapse
if (!isOverflowing) {
setIsTextExpanded(false)
}
setShowSeeMore(isOverflowing)
}
}, [task.text, windowWidth])
}, [task.text, windowWidth, isTaskExpanded])
const isCostAvailable = useMemo(() => {
const openAiCompatHasPricing =
@@ -386,49 +396,52 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
)}
</div>
{shouldShowPromptCacheInfo && (cacheReads !== undefined || cacheWrites !== undefined) && (
<div
style={{
display: "flex",
alignItems: "center",
gap: "4px",
flexWrap: "wrap",
}}>
<span style={{ fontWeight: "bold" }}>Cache:</span>
<span
{shouldShowPromptCacheInfo &&
(cacheReads !== undefined ||
cacheWrites !== undefined ||
apiConfiguration?.apiProvider === "anthropic") && (
<div
style={{
display: "flex",
alignItems: "center",
gap: "3px",
gap: "4px",
flexWrap: "wrap",
}}>
<i
className="codicon codicon-database"
<span style={{ fontWeight: "bold" }}>Cache:</span>
<span
style={{
fontSize: "12px",
fontWeight: "bold",
marginBottom: "-1px",
}}
/>
+{formatLargeNumber(cacheWrites || 0)}
</span>
<span
style={{
display: "flex",
alignItems: "center",
gap: "3px",
}}>
<i
className="codicon codicon-arrow-right"
display: "flex",
alignItems: "center",
gap: "3px",
}}>
<i
className="codicon codicon-database"
style={{
fontSize: "12px",
fontWeight: "bold",
marginBottom: "-1px",
}}
/>
+{formatLargeNumber(cacheWrites || 0)}
</span>
<span
style={{
fontSize: "12px",
fontWeight: "bold",
marginBottom: 0,
}}
/>
{formatLargeNumber(cacheReads || 0)}
</span>
</div>
)}
display: "flex",
alignItems: "center",
gap: "3px",
}}>
<i
className="codicon codicon-arrow-right"
style={{
fontSize: "12px",
fontWeight: "bold",
marginBottom: 0,
}}
/>
{formatLargeNumber(cacheReads || 0)}
</span>
</div>
)}
{ContextWindowComponent}
{isCostAvailable && (
<div
@@ -461,7 +474,25 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
}}>
<i className="codicon codicon-warning" />
<span>
{checkpointTrackerErrorMessage}
{checkpointTrackerErrorMessage.replace(/disabling checkpoints\.$/, "")}
{checkpointTrackerErrorMessage.endsWith("disabling checkpoints.") && (
<>
<a
onClick={() => {
vscode.postMessage({
type: "openExtensionSettings",
text: "enableCheckpoints",
})
}}
style={{
color: "inherit",
textDecoration: "underline",
cursor: "pointer",
}}>
disabling checkpoints.
</a>
</>
)}
{checkpointTrackerErrorMessage.includes("Git must be installed to use checkpoints.") && (
<>
{" "}
@@ -1,30 +1,25 @@
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import styled from "styled-components"
const StyledButton = styled(VSCodeButton)`
--success-button-bg: #176f2c;
--success-button-hover: #197f31;
--success-button-active: #156528;
interface SuccessButtonTWProps extends React.ComponentProps<typeof VSCodeButton> {}
background-color: var(--success-button-bg) !important;
border-color: var(--success-button-bg) !important;
color: #ffffff !important;
&:hover {
background-color: var(--success-button-hover) !important;
border-color: var(--success-button-hover) !important;
}
&:active {
background-color: var(--success-button-active) !important;
border-color: var(--success-button-active) !important;
}
`
interface SuccessButtonProps extends React.ComponentProps<typeof VSCodeButton> {}
const SuccessButton: React.FC<SuccessButtonProps> = (props) => {
return <StyledButton {...props} />
const SuccessButtonTW: React.FC<SuccessButtonTWProps> = (props) => {
return (
<VSCodeButton
{...props}
className={`
!bg-[#176f2c]
!border-[#176f2c]
!text-white
hover:!bg-[#197f31]
hover:!border-[#197f31]
active:!bg-[#156528]
active:!border-[#156528]
${props.className || ""}
`
.replace(/\s+/g, " ")
.trim()}
/>
)
}
export default SuccessButton
export default SuccessButtonTW
@@ -29,12 +29,12 @@ const TelemetryBanner = () => {
const handleAllow = () => {
setHasChosen(true)
vscode.postMessage({ type: "telemetrySetting", text: "enabled" satisfies TelemetrySetting })
vscode.postMessage({ type: "telemetrySetting", telemetrySetting: "enabled" satisfies TelemetrySetting })
}
const handleDeny = () => {
setHasChosen(true)
vscode.postMessage({ type: "telemetrySetting", text: "disabled" satisfies TelemetrySetting })
vscode.postMessage({ type: "telemetrySetting", telemetrySetting: "disabled" satisfies TelemetrySetting })
}
const handleOpenSettings = () => {
+5 -4
View File
@@ -13,10 +13,11 @@ interface TooltipProps {
hintText: string
tipText: string
children: React.ReactNode
style?: React.CSSProperties
}
// add styled component for tooltip
const TooltipBody = styled.div`
const TooltipBody = styled.div<Pick<TooltipProps, "style">>`
position: absolute;
background-color: ${getAsVar(VSC_SIDEBAR_BACKGROUND)};
color: ${getAsVar(VSC_DESCRIPTION_FOREGROUND)};
@@ -24,7 +25,7 @@ const TooltipBody = styled.div`
border-radius: 5px;
bottom: 100%;
left: -180%;
z-index: 10;
z-index: ${(props) => props.style?.zIndex ?? 10};
white-space: wrap;
max-width: 200px;
border: 1px solid ${getAsVar(VSC_INPUT_BORDER)};
@@ -39,12 +40,12 @@ const Hint = styled.div`
margin-top: 2px;
`
const Tooltip: React.FC<TooltipProps> = ({ visible, tipText, hintText, children }) => {
const Tooltip: React.FC<TooltipProps> = ({ visible, tipText, hintText, children, style }) => {
return (
<div style={{ position: "relative", display: "inline-block" }}>
{children}
{visible && (
<TooltipBody>
<TooltipBody style={style}>
{tipText}
{hintText && <Hint>{hintText}</Hint>}
</TooltipBody>
@@ -2,10 +2,13 @@ import { VSCodeButton, VSCodeTextField, VSCodeRadioGroup, VSCodeRadio } from "@v
import { useExtensionState } from "../../context/ExtensionStateContext"
import { vscode } from "../../utils/vscode"
import { Virtuoso } from "react-virtuoso"
import { memo, useMemo, useState, useEffect } from "react"
import { memo, useMemo, useState, useEffect, useCallback } from "react"
import Fuse, { FuseResult } from "fuse.js"
import { formatLargeNumber } from "../../utils/format"
import { formatSize } from "../../utils/size"
import DangerButton from "../common/DangerButton"
import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
import { useEvent } from "react-use"
type HistoryViewProps = {
onDone: () => void
@@ -14,10 +17,23 @@ type HistoryViewProps = {
type SortOption = "newest" | "oldest" | "mostExpensive" | "mostTokens" | "mostRelevant"
const HistoryView = ({ onDone }: HistoryViewProps) => {
const { taskHistory } = useExtensionState()
const { taskHistory, totalTasksSize } = useExtensionState()
const [searchQuery, setSearchQuery] = useState("")
const [sortOption, setSortOption] = useState<SortOption>("newest")
const [lastNonRelevantSort, setLastNonRelevantSort] = useState<SortOption | null>("newest")
const [deleteAllDisabled, setDeleteAllDisabled] = useState(false)
const handleMessage = useCallback((event: MessageEvent<ExtensionMessage>) => {
if (event.data.type === "relinquishControl") {
setDeleteAllDisabled(false)
}
}, [])
useEvent("message", handleMessage)
// Request total tasks size when component mounts
useEffect(() => {
vscode.postMessage({ type: "requestTotalTasksSize" })
}, [])
useEffect(() => {
if (searchQuery && sortOption !== "mostRelevant" && !lastNonRelevantSort) {
@@ -447,6 +463,21 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
)}
/>
</div>
<div
style={{
padding: "10px 10px",
borderTop: "1px solid var(--vscode-panel-border)",
}}>
<DangerButton
style={{ width: "100%" }}
disabled={deleteAllDisabled || taskHistory.length === 0}
onClick={() => {
setDeleteAllDisabled(true)
vscode.postMessage({ type: "clearAllTaskHistory" })
}}>
Delete All History{totalTasksSize !== null ? ` (${formatSize(totalTasksSize)})` : ""}
</DangerButton>
</div>
</div>
</>
)
+9 -1
View File
@@ -188,7 +188,15 @@ const StyledTabButton = styled.button<{ isActive: boolean }>`
}
`
const TabButton = ({ children, isActive, onClick }: { children: React.ReactNode; isActive: boolean; onClick: () => void }) => (
export const TabButton = ({
children,
isActive,
onClick,
}: {
children: React.ReactNode
isActive: boolean
onClick: () => void
}) => (
<StyledTabButton isActive={isActive} onClick={onClick}>
{children}
</StyledTabButton>
@@ -6,7 +6,6 @@ import {
VSCodeRadio,
VSCodeRadioGroup,
VSCodeTextField,
VSCodeButton,
} from "@vscode/webview-ui-toolkit/react"
import { Fragment, memo, useCallback, useEffect, useMemo, useState } from "react"
import ThinkingBudgetSlider from "./ThinkingBudgetSlider"
@@ -33,8 +32,10 @@ import {
openAiNativeModels,
openRouterDefaultModelId,
openRouterDefaultModelInfo,
qwenDefaultModelId,
qwenModels,
mainlandQwenModels,
internationalQwenModels,
mainlandQwenDefaultModelId,
internationalQwenDefaultModelId,
vertexDefaultModelId,
vertexModels,
askSageModels,
@@ -42,6 +43,8 @@ import {
askSageDefaultURL,
xaiDefaultModelId,
xaiModels,
sambanovaModels,
sambanovaDefaultModelId,
} from "../../../../src/shared/api"
import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
import { useExtensionState } from "../../context/ExtensionStateContext"
@@ -89,6 +92,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
const [vsCodeLmModels, setVsCodeLmModels] = useState<vscodemodels.LanguageModelChatSelector[]>([])
const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl)
const [azureApiVersionSelected, setAzureApiVersionSelected] = useState(!!apiConfiguration?.azureApiVersion)
const [awsEndpointSelected, setAwsEndpointSelected] = useState(!!apiConfiguration?.awsBedrockEndpoint)
const [modelConfigurationSelected, setModelConfigurationSelected] = useState(false)
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
@@ -206,6 +210,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
<VSCodeOption value="litellm">LiteLLM</VSCodeOption>
<VSCodeOption value="asksage">AskSage</VSCodeOption>
<VSCodeOption value="xai">X AI</VSCodeOption>
<VSCodeOption value="sambanova">SambaNova</VSCodeOption>
</VSCodeDropdown>
</DropdownContainer>
@@ -579,7 +584,33 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
{/* <VSCodeOption value="us-gov-east-1">us-gov-east-1</VSCodeOption> */}
</VSCodeDropdown>
</DropdownContainer>
<div style={{ display: "flex", flexDirection: "column" }}>
<VSCodeCheckbox
checked={awsEndpointSelected}
onChange={(e: any) => {
const isChecked = e.target.checked === true
setAwsEndpointSelected(isChecked)
if (!isChecked) {
setApiConfiguration({
...apiConfiguration,
awsBedrockEndpoint: "",
})
}
}}>
Use custom VPC endpoint
</VSCodeCheckbox>
{awsEndpointSelected && (
<VSCodeTextField
value={apiConfiguration?.awsBedrockEndpoint || ""}
style={{ width: "100%", marginTop: 3, marginBottom: 5 }}
type="url"
onInput={handleInputChange("awsBedrockEndpoint")}
placeholder="Enter VPC Endpoint URL (optional)"
/>
)}
<VSCodeCheckbox
checked={apiConfiguration?.awsUseCrossRegionInference || false}
onChange={(e: any) => {
@@ -799,6 +830,21 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
}}>
Supports Images
</VSCodeCheckbox>
<VSCodeCheckbox
checked={!!apiConfiguration?.openAiModelInfo?.supportsComputerUse}
onChange={(e: any) => {
const isChecked = e.target.checked === true
let modelInfo = apiConfiguration?.openAiModelInfo
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
modelInfo = { ...modelInfo, supportsComputerUse: isChecked }
setApiConfiguration({
...apiConfiguration,
openAiModelInfo: modelInfo,
})
}}>
Supports Computer Use
</VSCodeCheckbox>
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
<VSCodeTextField
value={
@@ -879,6 +925,38 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
<span style={{ fontWeight: 500 }}>Output Price / 1M tokens</span>
</VSCodeTextField>
</div>
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
<VSCodeTextField
value={
apiConfiguration?.openAiModelInfo?.temperature
? apiConfiguration.openAiModelInfo.temperature.toString()
: openAiModelInfoSaneDefaults.temperature?.toString()
}
onInput={(input: any) => {
let modelInfo = apiConfiguration?.openAiModelInfo
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
// Check if the input ends with a decimal point or has trailing zeros after decimal
const value = input.target.value
const shouldPreserveFormat =
value.endsWith(".") || (value.includes(".") && value.endsWith("0"))
modelInfo.temperature =
value === ""
? openAiModelInfoSaneDefaults.temperature
: shouldPreserveFormat
? value // Keep as string to preserve decimal format
: parseFloat(value)
setApiConfiguration({
...apiConfiguration,
openAiModelInfo: modelInfo,
})
}}>
<span style={{ fontWeight: 500 }}>Temperature</span>
</VSCodeTextField>
</div>
</>
)}
<p
@@ -1233,6 +1311,37 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
</div>
)}
{selectedProvider === "sambanova" && (
<div>
<VSCodeTextField
value={apiConfiguration?.sambanovaApiKey || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("sambanovaApiKey")}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>SambaNova API Key</span>
</VSCodeTextField>
<p
style={{
fontSize: "12px",
marginTop: 3,
color: "var(--vscode-descriptionForeground)",
}}>
This key is stored locally and only used to make API requests from this extension.
{!apiConfiguration?.sambanovaApiKey && (
<VSCodeLink
href="https://docs.sambanova.ai/cloud/docs/get-started/overview"
style={{
display: "inline",
fontSize: "inherit",
}}>
You can get a SambaNova API key by signing up here.
</VSCodeLink>
)}
</p>
</div>
)}
{apiErrorMessage && (
<p
style={{
@@ -1264,10 +1373,14 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
{selectedProvider === "gemini" && createDropdown(geminiModels)}
{selectedProvider === "openai-native" && createDropdown(openAiNativeModels)}
{selectedProvider === "deepseek" && createDropdown(deepSeekModels)}
{selectedProvider === "qwen" && createDropdown(qwenModels)}
{selectedProvider === "qwen" &&
createDropdown(
apiConfiguration?.qwenApiLine === "china" ? mainlandQwenModels : internationalQwenModels,
)}
{selectedProvider === "mistral" && createDropdown(mistralModels)}
{selectedProvider === "asksage" && createDropdown(askSageModels)}
{selectedProvider === "xai" && createDropdown(xaiModels)}
{selectedProvider === "sambanova" && createDropdown(sambanovaModels)}
</DropdownContainer>
{((selectedProvider === "anthropic" && selectedModelId === "claude-3-7-sonnet-20250219") ||
@@ -1483,7 +1596,10 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration):
case "deepseek":
return getProviderData(deepSeekModels, deepSeekDefaultModelId)
case "qwen":
return getProviderData(qwenModels, qwenDefaultModelId)
const qwenModels = apiConfiguration?.qwenApiLine === "china" ? mainlandQwenModels : internationalQwenModels
const qwenDefaultId =
apiConfiguration?.qwenApiLine === "china" ? mainlandQwenDefaultModelId : internationalQwenDefaultModelId
return getProviderData(qwenModels, qwenDefaultId)
case "mistral":
return getProviderData(mistralModels, mistralDefaultModelId)
case "asksage":
@@ -1504,7 +1620,7 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration):
return {
selectedProvider: provider,
selectedModelId: apiConfiguration?.openAiModelId || "",
selectedModelInfo: openAiModelInfoSaneDefaults,
selectedModelInfo: apiConfiguration?.openAiModelInfo || openAiModelInfoSaneDefaults,
}
case "ollama":
return {
@@ -1537,6 +1653,8 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration):
}
case "xai":
return getProviderData(xaiModels, xaiDefaultModelId)
case "sambanova":
return getProviderData(sambanovaModels, sambanovaDefaultModelId)
default:
return getProviderData(anthropicModels, anthropicDefaultModelId)
}
@@ -117,7 +117,11 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }
}
const hasInfo = useMemo(() => {
return modelIds.some((id) => id.toLowerCase() === searchTerm.toLowerCase())
try {
return modelIds.some((id) => id.toLowerCase() === searchTerm.toLowerCase())
} catch {
return false
}
}, [modelIds, searchTerm])
useEffect(() => {
@@ -1,10 +1,21 @@
import { VSCodeButton, VSCodeCheckbox, VSCodeLink, VSCodeTextArea } from "@vscode/webview-ui-toolkit/react"
import { memo, useEffect, useState } from "react"
import {
VSCodeButton,
VSCodeCheckbox,
VSCodeLink,
VSCodeTextArea,
VSCodePanels,
VSCodePanelTab,
VSCodePanelView,
} from "@vscode/webview-ui-toolkit/react"
import { memo, useCallback, useEffect, useState } from "react"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { validateApiConfiguration, validateModelId } from "../../utils/validate"
import { vscode } from "../../utils/vscode"
import SettingsButton from "../common/SettingsButton"
import ApiOptions from "./ApiOptions"
import { TabButton } from "../mcp/McpView"
import { useEvent } from "react-use"
import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
const { IS_DEV } = process.env
type SettingsViewProps = {
@@ -20,27 +31,51 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
openRouterModels,
telemetrySetting,
setTelemetrySetting,
chatSettings,
planActSeparateModelsSetting,
setPlanActSeparateModelsSetting,
} = useExtensionState()
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
const [pendingTabChange, setPendingTabChange] = useState<"plan" | "act" | null>(null)
const handleSubmit = () => {
const handleSubmit = (withoutDone: boolean = false) => {
const apiValidationResult = validateApiConfiguration(apiConfiguration)
const modelIdValidationResult = validateModelId(apiConfiguration, openRouterModels)
setApiErrorMessage(apiValidationResult)
setModelIdErrorMessage(modelIdValidationResult)
// setApiErrorMessage(apiValidationResult)
// setModelIdErrorMessage(modelIdValidationResult)
let apiConfigurationToSubmit = apiConfiguration
if (!apiValidationResult && !modelIdValidationResult) {
vscode.postMessage({ type: "apiConfiguration", apiConfiguration })
vscode.postMessage({
type: "customInstructions",
text: customInstructions,
})
vscode.postMessage({
type: "telemetrySetting",
text: telemetrySetting,
})
// vscode.postMessage({ type: "apiConfiguration", apiConfiguration })
// vscode.postMessage({
// type: "customInstructions",
// text: customInstructions,
// })
// vscode.postMessage({
// type: "telemetrySetting",
// text: telemetrySetting,
// })
// console.log("handleSubmit", withoutDone)
// vscode.postMessage({
// type: "separateModeSetting",
// text: separateModeSetting,
// })
} else {
// if the api configuration is invalid, we don't save it
apiConfigurationToSubmit = undefined
}
vscode.postMessage({
type: "updateSettings",
planActSeparateModelsSetting,
customInstructionsSetting: customInstructions,
telemetrySetting,
apiConfiguration: apiConfigurationToSubmit,
})
if (!withoutDone) {
onDone()
}
}
@@ -52,20 +87,51 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
// validate as soon as the component is mounted
/*
useEffect will use stale values of variables if they are not included in the dependency array. so trying to use useEffect with a dependency array of only one value for example will use any other variables' old values. In most cases you don't want this, and should opt to use react-use hooks.
useEffect(() => {
// uses someVar and anotherVar
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [someVar])
useEffect will use stale values of variables if they are not included in the dependency array.
so trying to use useEffect with a dependency array of only one value for example will use any
other variables' old values. In most cases you don't want this, and should opt to use react-use
hooks.
// uses someVar and anotherVar
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [someVar])
If we only want to run code once on mount we can use react-use's useEffectOnce or useMount
*/
*/
const handleMessage = useCallback(
(event: MessageEvent) => {
const message: ExtensionMessage = event.data
switch (message.type) {
case "didUpdateSettings":
if (pendingTabChange) {
vscode.postMessage({
type: "togglePlanActMode",
chatSettings: {
mode: pendingTabChange,
},
})
setPendingTabChange(null)
}
break
}
},
[pendingTabChange],
)
useEvent("message", handleMessage)
const handleResetState = () => {
vscode.postMessage({ type: "resetState" })
}
const handleTabChange = (tab: "plan" | "act") => {
if (tab === chatSettings.mode) {
return
}
setPendingTabChange(tab)
handleSubmit(true)
}
return (
<div
style={{
@@ -84,11 +150,11 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "17px",
marginBottom: "13px",
paddingRight: 17,
}}>
<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>Settings</h3>
<VSCodeButton onClick={handleSubmit}>Done</VSCodeButton>
<VSCodeButton onClick={() => handleSubmit(false)}>Done</VSCodeButton>
</div>
<div
style={{
@@ -98,13 +164,50 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
display: "flex",
flexDirection: "column",
}}>
<div style={{ marginBottom: 5 }}>
{/* Tabs container */}
{planActSeparateModelsSetting ? (
<div
style={{
border: "1px solid var(--vscode-panel-border)",
borderRadius: "4px",
padding: "10px",
marginBottom: "20px",
background: "var(--vscode-panel-background)",
}}>
<div
style={{
display: "flex",
gap: "1px",
marginBottom: "10px",
marginTop: -8,
borderBottom: "1px solid var(--vscode-panel-border)",
}}>
<TabButton isActive={chatSettings.mode === "plan"} onClick={() => handleTabChange("plan")}>
Plan Mode
</TabButton>
<TabButton isActive={chatSettings.mode === "act"} onClick={() => handleTabChange("act")}>
Act Mode
</TabButton>
</div>
{/* Content container */}
<div style={{ marginBottom: -12 }}>
<ApiOptions
key={chatSettings.mode}
showModelOptions={true}
apiErrorMessage={apiErrorMessage}
modelIdErrorMessage={modelIdErrorMessage}
/>
</div>
</div>
) : (
<ApiOptions
key={"single"}
showModelOptions={true}
apiErrorMessage={apiErrorMessage}
modelIdErrorMessage={modelIdErrorMessage}
/>
</div>
)}
<div style={{ marginBottom: 5 }}>
<VSCodeTextArea
@@ -126,6 +229,27 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
</p>
</div>
<div style={{ marginBottom: 5 }}>
<VSCodeCheckbox
style={{ marginBottom: "5px" }}
checked={planActSeparateModelsSetting}
onChange={(e: any) => {
const checked = e.target.checked === true
setPlanActSeparateModelsSetting(checked)
}}>
Use different models for Plan and Act modes
</VSCodeCheckbox>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
Switching between Plan and Act mode will persist the API and model used in the previous mode. This may be
helpful e.g. when using a strong reasoning model to architect a plan for a cheaper coding model to act on.
</p>
</div>
<div style={{ marginBottom: 5 }}>
<VSCodeCheckbox
style={{ marginBottom: "5px" }}
@@ -143,10 +267,12 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
color: "var(--vscode-descriptionForeground)",
}}>
Help improve Cline by sending anonymous usage data and error reports. No code, prompts, or personal
information is ever sent. See our{" "}
<VSCodeLink
href="https://github.com/cline/cline/blob/main/docs/PRIVACY.md"
style={{ fontSize: "inherit" }}>
information are ever sent. See our{" "}
<VSCodeLink href="https://docs.cline.bot/more-info/telemetry" style={{ fontSize: "inherit" }}>
telemetry overview
</VSCodeLink>{" "}
and{" "}
<VSCodeLink href="https://cline.bot/privacy" style={{ fontSize: "inherit" }}>
privacy policy
</VSCodeLink>{" "}
for more details.
@@ -34,7 +34,7 @@ const WelcomeView = () => {
left: 0,
right: 0,
bottom: 0,
padding: "0 20px",
padding: "0 0px",
display: "flex",
flexDirection: "column",
}}>
@@ -63,7 +63,7 @@ const WelcomeView = () => {
3.7 Sonnet.
</p>
<VSCodeButton appearance="primary" onClick={handleLogin} style={{ width: "100%" }}>
<VSCodeButton appearance="primary" onClick={handleLogin} style={{ width: "100%", marginTop: 4 }}>
Get Started for Free
</VSCodeButton>
@@ -20,10 +20,12 @@ interface ExtensionStateContextType extends ExtensionState {
mcpServers: McpServer[]
mcpMarketplaceCatalog: McpMarketplaceCatalog
filePaths: string[]
totalTasksSize: number | null
setApiConfiguration: (config: ApiConfiguration) => void
setCustomInstructions: (value?: string) => void
setTelemetrySetting: (value: TelemetrySetting) => void
setShowAnnouncement: (value: boolean) => void
setPlanActSeparateModelsSetting: (value: boolean) => void
}
const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
@@ -42,6 +44,7 @@ export const ExtensionStateContextProvider: React.FC<{
platform: DEFAULT_PLATFORM,
telemetrySetting: "unset",
vscMachineId: "",
planActSeparateModelsSetting: true,
})
const [didHydrateState, setDidHydrateState] = useState(false)
const [showWelcome, setShowWelcome] = useState(false)
@@ -50,6 +53,7 @@ export const ExtensionStateContextProvider: React.FC<{
const [openRouterModels, setOpenRouterModels] = useState<Record<string, ModelInfo>>({
[openRouterDefaultModelId]: openRouterDefaultModelInfo,
})
const [totalTasksSize, setTotalTasksSize] = useState<number | null>(null)
const [openAiModels, setOpenAiModels] = useState<string[]>([])
const [mcpServers, setMcpServers] = useState<McpServer[]>([])
@@ -81,6 +85,7 @@ export const ExtensionStateContextProvider: React.FC<{
config.clineApiKey,
config.asksageApiKey,
config.xaiApiKey,
config.sambanovaApiKey,
].some((key) => key !== undefined)
: false
setShowWelcome(!hasKey)
@@ -134,6 +139,10 @@ export const ExtensionStateContextProvider: React.FC<{
}
break
}
case "totalTasksSize": {
setTotalTasksSize(message.totalTasksSize ?? null)
break
}
}
}, [])
@@ -153,6 +162,7 @@ export const ExtensionStateContextProvider: React.FC<{
mcpServers,
mcpMarketplaceCatalog,
filePaths,
totalTasksSize,
setApiConfiguration: (value) =>
setState((prevState) => ({
...prevState,
@@ -168,6 +178,11 @@ export const ExtensionStateContextProvider: React.FC<{
...prevState,
telemetrySetting: value,
})),
setPlanActSeparateModelsSetting: (value) =>
setState((prevState) => ({
...prevState,
planActSeparateModelsSetting: value,
})),
setShowAnnouncement: (value) =>
setState((prevState) => ({
...prevState,
+6
View File
@@ -1,3 +1,9 @@
@layer theme, base, components, utilities;
@import "tailwindcss/theme.css" layer(theme);
/* Disable Tailwind's CSS reset to preserve existing styles */
/* @import "tailwindcss/preflight.css" layer(base); */
@import "tailwindcss/utilities.css" layer(utilities);
textarea:focus {
outline: 1.5px solid var(--vscode-focusBorder, #007fd4);
}
+5
View File
@@ -93,6 +93,11 @@ export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): s
return "You must provide a valid API key or choose a different provider."
}
break
case "sambanova":
if (!apiConfiguration.sambanovaApiKey) {
return "You must provide a valid API key or choose a different provider."
}
break
}
}
return undefined
+2 -1
View File
@@ -1,10 +1,11 @@
/// <reference types="vitest/config" />
import { defineConfig } from "vite"
import tailwindcss from "@tailwindcss/vite"
import react from "@vitejs/plugin-react-swc"
export default defineConfig({
plugins: [react()],
plugins: [react(), tailwindcss()],
test: {
environment: "jsdom",
globals: true,