Compare commits

..
Author SHA1 Message Date
Trevor Hudson a25e384956 remove sourcemap 2025-04-16 09:23:08 -07:00
Trevor Hudson 59275e77eb resolve conflicts 2025-04-14 18:31:06 -07:00
Trevor Hudson f7677fa856 remove colon if there is no message 2025-04-14 18:27:48 -07:00
Trevor Hudson 19c2de6067 - add ability to send context with an options selection
- add sourcemaps for debugging in the webview
2025-04-14 18:27:46 -07:00
Toshii 0b19ba6023 NEW model update (#2892)
* new

* changeset
2025-04-14 17:57:03 -07:00
EvanandDennis Bartlett 75143a718a add fetching global cline rules files (#2864)
* add fetching global cline rules files

* remove bad import from main merge

---------

Co-authored-by: Dennis Bartlett <bartlett.dc.1@gmail.com>
2025-04-14 16:20:19 -07:00
canvrnoandDennis Bartlett 7276f50d9e ENG-319/Add indicators when tools operate outside of workspace (#2836)
* rebased/mergefix

* one small remaining rebase fix

* more fixes

---------

Co-authored-by: Dennis Bartlett <bartlett.dc.1@gmail.com>
2025-04-14 12:17:03 -07:00
2964388585 feat: add copy code button to mermaid diagrams(#2129) (#2758)
* feat: add copy code button to mermaid diagrams(#2129)

- Added copy button to MermaidBlock component
- Improved loading message text
- Ensure image buffer type safety with explicit Uint8Array conversion

* Apply suggestions from code review

Enhance accessibility by adding an aria-label description to the "Copy Code" button .

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

* Update webview-ui/src/components/common/MermaidBlock.tsx

Add try/catch or handle promise rejection to provide feedback on copy failures.

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

* lint code after use CR suggestions

* fix: add async/await for clipboard operation handling - Fixes #2129

---------

Co-authored-by: qiaozhuoyue <qiaozhuoyue@bytedance.com>
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-04-14 12:58:09 -06:00
yusheng chen 6fcd43597e refactor: type improvements of multi files that doesn't modify functionality (#2878) 2025-04-14 12:57:18 -06:00
yusheng chen 9de6af51fd feat: add src/api/transform/vscode-lm-format.test.ts (#2600) 2025-04-14 12:56:35 -06:00
canvrnoandDennis Bartlett ab59bd9b50 initial (#2795)
Co-authored-by: Dennis Bartlett <bartlett.dc.1@gmail.com>
2025-04-14 11:42:04 -07:00
yusheng chen a0252e70d9 convert inline style to tailwind css of file SettingsView.tsx (#2684)
* convert inline style to tailwind css of file `SettingsView.tsx`

* style: fix `SettingsView.tsx` styling

https://github.com/cline/cline/pull/2684#issuecomment-2784520485
2025-04-14 11:35:05 -07:00
github-actions[bot] faa471b6e2 v3.12.2 Release Notes
v3.12.2 Release Notes
2025-04-14 10:59:17 -07:00
Dennis Bartlett 80dd10d815 Revert "chore: prompt cache up to the third-to-last message in the conversati…" (#2883)
This reverts commit 359f77c2e3.
2025-04-14 10:54:48 -07:00
Saoud Rizwan 109f0ec1a4 Add gpt-4.1 (#2880)
* Add gpt-4.1

* Create eighty-carpets-attack.md
2025-04-14 10:52:43 -07:00
yusheng chen e2cd3d706c refactor: extract interface LanguageModelChatSelector to file api/providers/types.ts (#2879) 2025-04-14 10:13:20 -07:00
atsushi-ishibashi 359f77c2e3 chore: prompt cache up to the third-to-last message in the conversation history for claude (#2847)
* chore: cache up to the third-to-last message in the conversation history

* chore: run format

* chore: create changeset

* chore: typo
2025-04-14 09:40:18 -07:00
Saoud Rizwan 2caf1dc26b Prepare for release 2025-04-12 23:30:20 -07:00
yusheng chen ca2b4168d5 refactor: type improvement of file Announcement.spec.tsx (#2853) 2025-04-12 23:21:43 -07:00
yusheng chen 0dabb06cb2 refactor: type improvement of file MarkdownBlock.tsx (#2854) 2025-04-12 23:21:21 -07:00
yusheng chen dace684afa refactor & perf of file HistoryView.tsx (#2855) 2025-04-12 23:20:59 -07:00
yusheng chen 86aadd1bb7 refactor: type improvement of file shell.test.ts (#2856) 2025-04-12 23:20:14 -07:00
yusheng chen 0c41159579 refactor: type improvement of file McpHub.ts (#2852) 2025-04-12 23:19:48 -07:00
yusheng chen 70cdd9d716 refactor: type improvement of file utils/hooks.ts (#2857) 2025-04-12 23:19:08 -07:00
yusheng chen 6589659c9d refactor: type improvement of file core/controller/index.ts (#2851) 2025-04-12 21:46:42 -07:00
yusheng chen 9224a33d01 refactor: type improvement of file BrowserSession.ts (#2850) 2025-04-12 21:45:49 -07:00
Saoud Rizwan ceaed842e0 Update Announcement 2025-04-12 21:40:39 -07:00
Saoud Rizwan 8d5d834a1f Remove legacy checkpoint overlay 2025-04-12 21:36:59 -07:00
Saoud Rizwan 5cdbf4ea38 fix: resolve conflicts 2025-04-12 21:32:17 -07:00
Saoud Rizwan 4004e9efed Revert "ENG-377 Changing Checkpoint UI to take less real space on the chat interface (#2752)"
This reverts commit 2ef4e56bca.
2025-04-12 21:29:17 -07:00
Saoud Rizwan 8fb419f273 Revert "Use line indicators for checkpoint markers (#2785)"
This reverts commit 386d5e41e7.
2025-04-12 21:25:44 -07:00
Saoud Rizwan e855e82d5e Update README 2025-04-12 20:48:40 -07:00
Saoud Rizwan 12139bf448 fix: resolve conflicts 2025-04-12 20:45:20 -07:00
Saoud Rizwan 1bca8a9d12 Use improved context manager 2025-04-12 20:41:59 -07:00
Saoud Rizwan ca5cdd13de Add mcp docs tool 2025-04-12 20:41:55 -07:00
Saoud Rizwan b3b074d90a fix: browser tool showing loading spinner when task is cancelled 2025-04-12 00:47:58 -07:00
Saoud Rizwan bf10cd4efb Modify prompt response to diff edit error 2025-04-12 00:34:57 -07:00
Saoud Rizwan 0bc355d141 Show favorited models at top always 2025-04-12 00:03:59 -07:00
Saoud Rizwan f9094c0fb6 Prepare for release 2025-04-11 23:40:35 -07:00
Saoud Rizwan 8497c435f4 Fix auto-approve menu showing no selected options 2025-04-11 23:37:38 -07:00
Saoud Rizwan 906dac25c6 Fix xAI provider name 2025-04-11 23:06:31 -07:00
Saoud Rizwan bdeec6a510 Refactor ServersToggleModal to improve layout 2025-04-11 23:02:45 -07:00
Saoud Rizwan 15d01434bb Update labels in AutoApproveMenu for clarity on file access permissions 2025-04-11 22:42:29 -07:00
Saoud Rizwan 45c041b781 Fix auto approve item types 2025-04-11 22:35:21 -07:00
Saoud Rizwan 40bf6241f9 Fix checkpoints bugs (#2841)
* Fix browser tool actions not being grouped because of checkpoints

* Fix bug where hovering mouse over checkpoint and not moving would make popover disappear

* Fix duplicate checkpoints bug

* Create slow-hornets-flash.md
2025-04-11 22:16:46 -07:00
Saoud Rizwan a26494e5cc Improve diff editing animation and prompts for large files (#2839)
* Remove streaming animation between chunks of edits

* Add quick scrolling animation between chunks of changes

* Modify prompts to handle large files

* Modify prompt to handle multi-edits to same file

* Add diff edit indicator

* Create dirty-guests-shout.md
2025-04-11 21:54:17 -07:00
yusheng chen 941414e87f chore: remove unused import of file BrowserSettingsMenu.tsx (#2680) 2025-04-11 20:10:27 -07:00
Saoud Rizwan 1af57b7c62 Remove options from plan mode tool + improve plan mode prompt (#2728)
* Remove options parameter from plan mode tool

* Improve task continuation prompt
2025-04-11 20:09:07 -07:00
yusheng chen b057710083 refactor: add try catch to file context-error-handling.ts (#2803) 2025-04-11 20:07:10 -07:00
yusheng chen 0096521966 refactor & perf of file ServerRow.tsx (#2805) 2025-04-11 20:05:42 -07:00
canvrno 1f50188c41 ENG-449/Checkpoint UI hover debounce (#2806)
* initial

* added debounce for checkmark expanded ui

* corrected bookmark size

* removed unnecesary cleanup

* fixed removed code

* removed cleanup for real this time

* refactored to reduce complexity, added cleanup
2025-04-11 20:03:24 -07:00
yusheng chen d00103419f refactor: type improvement of file LinkPreview.tsx (#2807) 2025-04-11 20:00:27 -07:00
yusheng chen 1c9bbba749 refactor: type improvement of file ChatTextArea.tsx (#2808)
* refactor: type improvement of file `ChatTextArea.tsx`

* refactor: type improvement of interface `GitCommit`

doc: add changeset
2025-04-11 19:59:54 -07:00
yusheng chen b54db8b82d refactor: type improvement of file controller/index.ts (#2810) 2025-04-11 19:59:18 -07:00
yusheng chen 7c7e86d055 chore: remove unused file TabNavbar.tsx (#2812) 2025-04-11 19:58:45 -07:00
yusheng chen 458583a476 refactor & perf of file ThinkingBudgetSlider.tsx (#2816) 2025-04-11 19:58:34 -07:00
dependabot[bot] f76ec25559 Bump vite from 6.2.5 to 6.2.6 in /webview-ui in the npm_and_yarn group (#2821)
Bumps the npm_and_yarn group in /webview-ui with 1 update: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `vite` from 6.2.5 to 6.2.6
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.2.6/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.2.6/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.2.6
  dependency-type: direct:development
  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-04-11 19:57:59 -07:00
EvanandSaoud Rizwan 90e9c49654 Factor out get cline rules function (#2827)
* factor out cline rules functionality

* changeset

* Update fs.ts

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-04-11 19:57:39 -07:00
Ara 0ea8506bf9 Eng-451 Fixing bugs in the provider name when we switch models halfway between a chat (#2833)
* Refactor Task class to use global state for API provider ID in telemetry events because this.apiProvider is readonly and shows the old value on model switch

* Updating README

* Updating README

* Updating README

* Updating README

* Updating README

* Updating README
2025-04-11 19:53:26 -07:00
EvanandSaoud Rizwan 1c22ee5896 Grok3 reasoning effort (#2837)
* stream reasoning tokens

* changeset

* toggle xai grok 3 mini reasoning

* changeset

* Add reasoning effort checkbox and fix plan mode toggling

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-04-11 19:52:04 -07:00
pashpashpash 30857e969e full automation (#2817) 2025-04-11 13:41:40 -07:00
0d07b421df Stream Grok 3 Mini reasoning tokens (#2829)
* stream reasoning tokens

* changeset

* Update .changeset/sixty-jokes-hope.md

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

---------

Co-authored-by: Dennis Bartlett <bartlett.dc.1@gmail.com>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-04-11 13:04:09 -07:00
pashpashpash ff9484e141 test server waits to respond until task completed (#2815)
* test server waits to respond until task completed

* removed taskCompleted
2025-04-11 01:57:08 -07:00
canvrnoandellipsis-dev[bot] ccc8e471e3 ENG-320/Auto-approve controls to restrict Cline actions outside of workspace (#2779)
* initial- buttons

* more buttons

* incremental

* increment

* Renamed old auto approve name

* comments

* started read options

* paused here

* cleanup

* de-duplication and renames

* renames

* restored unrelated test file

* labels and semantics

* fixed labels issue

* cleanup/dedup

* minor semantics

* cleanup

* changeset

* one line

* ellipsis-dev changes

* reverting settings names

* made new settings optional

* Update webview-ui/src/components/chat/AutoApproveMenu.tsx

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

* testserver fix

* testserver.ts fix / prettier

* testserver.ts fix / prettier

* Delete src/services/test/TestServer.ts

* restored testserver.ts

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-04-11 01:30:12 -07:00
pashpashpash 9859136e80 increased max tasks 100-10000 in test mode (#2814) 2025-04-11 01:15:10 -07:00
Saoud Rizwan 7969ba2d68 Remove WeakRef usage (#2811)
* Remove controllerRef

* Remove webviewProviderRef

* Remove controllerRef

* Fix test

* Create odd-jeans-wash.md

* Fix test
2025-04-11 00:28:56 -07:00
yusheng chen 16c0992672 refactor: remove unnecessary type assertion as any (#2802) 2025-04-10 23:53:32 -07:00
Suvarchal Kumar Cheedelaandellipsis-dev[bot] 3b8be75c7f Enhance ollama provider (#2708)
* Enhance Ollama provider with retry mechanism, timeout handling, and improved error handling

* Make Ollama tests optional when Ollama is not running

* Update src/api/providers/__tests__/ollama.test.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-04-10 23:50:05 -07:00
Daniel Trugman a00f24e182 Requesty: Add model info (#2190)
Adding model information to Requesty provider.

- Add a new model picker component for Requesty.
- Enable controlling thinking budget via a slider

IMPORTANT:
Model information is fetched ONLY(!) when the user chooses "requesty"
as their provider to avoid any boot latency.
2025-04-10 23:49:47 -07:00
Xudong Guo 238654e6a2 feat: Add more models support for doubao (#2736)
* feat: Add more models siport for doubao

* fix: package-lock.json  should not be submitted
2025-04-10 23:49:26 -07:00
pashpashpash 643319f106 setting auto approve settings on test server start (#2800) 2025-04-10 19:03:29 -07:00
pashpashpash 0645eccd2a Message catching added to test server (#2797)
* added message catching to test server

* added message catching to test server

* fixing logger initialization

* fixed race condition

* adding logging

* removed redundant if condition
2025-04-10 18:52:52 -07:00
Evan 3a6f0c2fd0 move context files to context folders (#2798) 2025-04-10 17:50:04 -07:00
canvrnoandellipsis-dev[bot] 17314cb88d ENG-317 / Feat: Add model "favorites" toggle for Cline & OpenRouter providers (#2722)
* initial

* Still facing issue with OR/Cline provider switching

* Working with provider switches

* cleanup

* cleanup

* changset

* Update .changeset/twelve-rocks-drum.md

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

* test (in progress)

* added telemetry

* removed test

* cleanup

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-04-10 17:20:16 -07:00
Evan 6f9cf8a028 Make .clinerules folder (#2781)
* switch clinerules to directory

* changeset
2025-04-10 17:01:56 -07:00
EvanandSaoud Rizwan 47aecdfc75 MCPToggleModal - Add button to open config (#2743)
* make ServerRow not optionally not expandable

* changeset

* factor out servers toggle list

* changeset

* add servers modal

* changest

* Reduce padding in modal

* separate fetch useEffect for more efficient rendering

* add button to open installed servers config

* changeset

* add mcptab type

* change codicon

* modify button display

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2025-04-10 16:25:08 -07:00
pashpashpash fb037a05c1 better mcp marketplace installation prompt (#2793) 2025-04-10 16:06:42 -07:00
Saoud Rizwan d9cedc41b7 Add announcement about redesigned checkpoints 2025-04-10 09:24:48 -07:00
Saoud Rizwan f978ecce52 Prepare for release 2025-04-10 02:13:04 -07:00
Saoud Rizwan 4c28760557 fix: update ErrorService to use isEnabled() consistently 2025-04-10 02:07:00 -07:00
ba79a51dd7 Error Service Respects Telemetry (#2780)
* Refactor Service to be able to be disabled based on telemetry settings

* Add telemetryService enabled check

* Create wet-avocados-kiss.md

* Update src/services/error/ErrorService.ts

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

* Update src/services/error/ErrorService.ts

* Update src/services/error/ErrorService.ts

* Update src/services/error/ErrorService.ts

* Update src/services/error/ErrorService.ts

* Update src/services/error/ErrorService.ts

* Apply suggestions from code review

* Add comment about opt in

* Fixes

---------

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-04-10 02:04:54 -07:00
Saoud RizwanandDennis Bartlett 4e5cc92065 Fixes issue where deleting tasks wasn't clearing the task metadata or context history files; let model recording fail gracefully (#2778)
* Fixes issue where deleting tasks wasn't clearing the task metadata or context history files; let model recording fail gracefully

* Create clean-boats-film.md

---------

Co-authored-by: Dennis Bartlett <bartlett.dc.1@gmail.com>
2025-04-10 02:04:10 -07:00
Saoud Rizwan b42c0f2571 Use legacy context manager (#2786)
* Use legacy context manager

* Revert changes post new context management
2025-04-10 01:51:50 -07:00
Saoud Rizwan 386d5e41e7 Use line indicators for checkpoint markers (#2785)
* Use line indicators for checkpoint markers

* revert comment removal

* Create unlucky-dragons-fly.md
2025-04-10 01:07:42 -07:00
Dennis Bartlett 2823e6c845 Update list of models and set new defualt for xAI (#2777) 2025-04-09 17:32:35 -07:00
Dennis Bartlettandarafatkatze cef9af16a4 Remove stream options from XAIHandler and add new Grok-3 model variants to API configuration (#2776)
Co-authored-by: arafatkatze <arafat.da.khan@gmail.com>
2025-04-09 17:20:06 -07:00
Dennis Bartlett 49d3bcfedc Update sentry in extension to add traceability and version (#2775)
* Add Sentry package

* Setup Logger to use Error Service

* Enhance traceability and message information
2025-04-09 16:51:58 -07:00
pashpashpash d36a44ec38 Cline Task Server (#2773)
* added cline task server

* moving test server into separate file

* removed redundant test server in extension
2025-04-09 16:24:47 -07:00
Dennis Bartlettandellipsis-dev[bot] cbcf89d634 Add sentry to extension (#2766)
* Add Sentry package

* Setup Logger to use Error Service

* Update src/services/error/ErrorService.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-04-09 16:01:10 -07:00
pashpashpash 6a10e30436 linebreak (#2765) 2025-04-09 15:33:42 -07:00
pashpashpash 2c0afbc3be added IS_TEST build flag (#2770)
* added IS_TEST build flag

* removed cross-env
2025-04-09 15:32:08 -07:00
Ara 2ef4e56bca ENG-377 Changing Checkpoint UI to take less real space on the chat interface (#2752)
* Enhance chat component interactivity by adding row index and hover state management. Updated BrowserSessionRow, ChatRow, and CheckmarkControl to support row-specific hover effects and state tracking, improving user experience during interactions.

* Adding hovered row index

* Adding hovered row index

* Adding hovered row index
2025-04-09 13:53:10 -07:00
Toshii e26d001585 ENG-422 (#2768)
* task restore user message block structure

* changeset

* check str
2025-04-09 13:19:58 -07:00
Trevor Hudson 6fc2cb128e Trevor/eng 253 system for users to report errors to GitHub (#2756)
* add metadata for model and apiProvider so it's available to Cline

* fix test

* check for existing array in case of old task

* only write if the metadata changes
2025-04-09 12:12:32 -07:00
Saoud Rizwan 8cc64f5e7e Revert "split user text during task resumption (#2699)"
This reverts commit fdd04bc942.
2025-04-09 02:48:05 -07:00
Saoud Rizwan a4412e8014 Update new task tool component styles 2025-04-09 02:44:26 -07:00
Saoud Rizwan 36f7abb8ec Prepare for release 2025-04-09 02:30:20 -07:00
Saoud Rizwan 080a79bd7d Remove mcp docs tool 2025-04-09 02:19:28 -07:00
pashpashpash 0208fdf555 fixed test (#2755) 2025-04-08 19:51:09 -07:00
Dennis Bartlett 226f20f28f Add new types to PR template (#2750)
* Update scripts so that test runs all tests by default

* Update PR Template to include new types
2025-04-08 19:20:02 -07:00
Dennis Bartlett fdc76c8802 Update scripts so that test runs all tests by default (#2749) 2025-04-08 19:19:12 -07:00
Saoud Rizwan 7099a00674 Add info about smarter context management (#2754)
* Add info about smarter context management

* Create nice-toys-help.md
2025-04-08 19:18:13 -07:00
monotykamary b470229a97 feat: add tiered pricing for gemini-2.5-pro (#2741)
* feat: add tiered pricing for gemini-2.5-pro

* fix: ensure price tiers are sorted before lookup

* refactor: remove old prices

* refactor(settings): improve model tier pricing display clarity

- Clarify token limit display using full numbers instead of 'k'.
- Specify price unit as '/million tokens' for better understanding.
2025-04-08 19:06:25 -07:00
pashpashpash e37f6e3b88 Context in context (#2745)
* context in context

* keeping comments

* minimal context window line

* nit
2025-04-08 17:55:27 -07:00
pashpashpash 4c72bd96ab New Horizons (#2747)
* new task tool added

* small fix

* fixed numbering

* system prompt
2025-04-08 17:20:30 -07:00
EvanandDennis Bartlett be120e85be Keybinding to quick-add context to Cline chat (#2748)
* add cmd + quote keybinding to add to cline chat

* changeset

* auto focus and start cursor on new line for easy UX

* changeset

* Remove extra new line character

---------

Co-authored-by: Dennis Bartlett <bartlett.dc.1@gmail.com>
2025-04-08 16:45:34 -07:00
Toshii fdd04bc942 split user text during task resumption (#2699)
* task restore user message block structure

* changeset
2025-04-08 16:13:07 -07:00
Shlomo b7c03af9ac fix: can't open as image diagrams with non-Latin1 characters (#2402) 2025-04-08 16:12:21 -07:00
github-actions[bot] 1961583eb6 v3.10.0 Release Notes and Banner
v3.10.0 Release Notes and Banner
2025-04-08 16:05:52 -07:00
132 changed files with 3853 additions and 1090 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
context management for file reads
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Fix auto approve state out of sync
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Reduce system prompt size by letting Cline dynamically load MCP documentation only when needed
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Added copy button to MermaidBlock component
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Feature to open basic settings & scroll a section into view with a highlight animation
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Disable notifications in browser
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Add cmd+quote default keybinding to add selected context to Cline chat
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Add modal for toggling MCP servers to the chat area
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Features to Relaunch browser in debug, test connection
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Added option to auto-approve all commands
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Remote browser control via devtools protocol
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
factor out servers list
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Allow user to send context with an option selection
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Fix tool toggle auto approve
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
convert inline style to tailwind css of file `SettingsView.tsx`
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
allow enabling prompt caching for LiteLLM + Claude
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Added feature to detect installed versions of chromium and display them as a placeholder if not already explicitly configured by the user
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Add stream options back to xai provider
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Auto focus the text field when the user uses the 'add to Cline' command
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Add the ability to fetch from global cline rules files
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Icons to indicate an action is occuring outside of the users workspace
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Fix for headless browser mode
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": minor
---
Drag and drop of file/folders into cline chat
@@ -2,4 +2,4 @@
"claude-dev": patch
---
TS errors
update to NEW model
+3
View File
@@ -13,7 +13,10 @@
- [ ] 🐛 Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (non-breaking change which adds functionality)
- [ ] 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] ♻️ Refactor Changes
- [ ] 💅 Cosmetic Changes
- [ ] 📚 Documentation update
- [ ] 🏃 Workflow Changes
### Pre-flight Checklist
+13
View File
@@ -16,6 +16,19 @@
"IS_DEV": "true",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}"
}
},
{
"name": "Run Extension (Test Mode)",
"type": "extensionHost",
"request": "launch",
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
"preLaunchTask": "${defaultBuildTask}",
"env": {
"IS_DEV": "true",
"IS_TEST": "true",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}"
}
}
]
}
+51
View File
@@ -14,6 +14,14 @@
"isDefault": true
}
},
{
"label": "watch:test",
"dependsOn": ["npm: build:webview:test", "npm: dev:webview", "npm: watch:tsc", "npm: watch:esbuild:test"],
"presentation": {
"reveal": "never"
},
"group": "build"
},
{
"type": "npm",
"script": "build:webview",
@@ -32,6 +40,25 @@
}
}
},
{
"type": "npm",
"script": "build:webview:test",
"group": "build",
"problemMatcher": [],
"isBackground": true,
"label": "npm: build:webview:test",
"presentation": {
"group": "watch",
"reveal": "never",
"close": true
},
"options": {
"env": {
"IS_DEV": "true",
"IS_TEST": "true"
}
}
},
{
"type": "npm",
"script": "dev:webview",
@@ -77,6 +104,30 @@
"group": "watch",
"reveal": "never",
"close": true
},
"options": {
"env": {
"IS_DEV": "true"
}
}
},
{
"type": "npm",
"script": "watch:esbuild:test",
"group": "build",
"problemMatcher": "$esbuild-watch",
"isBackground": true,
"label": "npm: watch:esbuild:test",
"presentation": {
"group": "watch",
"reveal": "never",
"close": true
},
"options": {
"env": {
"IS_DEV": "true",
"IS_TEST": "true"
}
}
},
{
+47
View File
@@ -1,5 +1,52 @@
# Changelog
## [3.12.2]
- Add gpt-4.1
## [3.12.1]
- Use visual checkpoint indicator to make it clear when checkpoints are created
- Big shoutout to @samuel871211 for numerous code quality improvements, refactoring contributions, and webview performance improvements!
- Use improved context manager
## [3.12.0]
- Add favorite toggles for models when using the Cline & OpenRouter providers
- Add auto-approve options for edits/reads outside of the workspace
- Improve diff editing animation for large files
- Add indicator showing number of diff edits when Cline edits a file
- Add streaming support and reasoning effort option to xAI's Grok 3 Mini
- Add settings button to MCP popover to easily modify installed servers
- Fix bug where browser tool actions would show unparsed results in the chat view
- Fix issue with new checkpoints popover hiding too quickly
- Fix duplicate checkpoints bug
- Improve Ollama provider with retry mechanism, timeout handling, and improved error handling (thanks suvarchal!)
## [3.11.0]
- Redesign checkpoint UI to declutter chat view by using a subtle indicator line that expands to a popover on hover, with a new date indicator for when it was created
- Add support for xAI's provider's Grok 3 models
- Add more robust error tracking for users opted in to telemetry (thank you for helping us make Cline better!)
## [3.10.1]
- Add CMD+' keyboard shortcut to add selected text to Cline
- Cline now auto focuses the text field when using 'Add to Cline' shortcut
- Add new 'Create New Task' tool to let Cline start a new task autonomously!
- Fix Mermaid diagram issues
- Fix Gemini provider cost calculation to take new tiered pricing structure into account
## [3.10.0]
- Add setting to let browser tool use local Chrome via remote debugging, enabling session-based browsing. Replaces sessionless Chromium, unlocking debugging and productivity workflows tied to your real browser state.
- Add new auto-approve option to approve _ALL_ commands (use at your own risk!)
- Add modal in the chat area to more easily enable or disable MCP servers
- Add drag and drop of file/folders into cline chat (Thanks eljapi!)
- Add prompt caching for LiteLLM + Claude (Thanks sammcj!)
- Add Improved context management
- Fix MCP auto approve toggle issues being out of sync with settings
## [3.9.2]
- Add recommended models for Cline provider
+1 -1
View File
@@ -2,7 +2,7 @@
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
# Cline
<p align="center">
<img src="https://media.githubusercontent.com/media/cline/cline/main/assets/docs/demo.gif" width="100%" />
+16
View File
@@ -82,6 +82,7 @@ Cline has access to the following tools for various tasks:
4. **Interaction Tools**
- `ask_followup_question`: Ask user for clarification
- `attempt_completion`: Present final results
- `new_task`: Start a new task with preloaded context
Each tool has specific parameters and usage patterns. Here are some examples:
@@ -114,6 +115,21 @@ Each tool has specific parameters and usage patterns. Here are some examples:
</execute_command>
```
- Start a new task with context (new_task):
```xml
<new_task>
<context>
We've completed the backend API with these endpoints:
- GET /api/tasks
- POST /api/tasks
- PUT /api/tasks/:id
- DELETE /api/tasks/:id
Now we need to implement the React frontend.
</context>
</new_task>
```
## Common Tasks
1. **Create a New Component**
+5
View File
@@ -4,6 +4,7 @@ const path = require("path")
const production = process.argv.includes("--production")
const watch = process.argv.includes("--watch")
const test = process.env.IS_TEST === "true"
/**
* @type {import('esbuild').Plugin}
@@ -68,6 +69,10 @@ const extensionConfig = {
minify: production,
sourcemap: !production,
logLevel: "silent",
define: {
"process.env.IS_DEV": JSON.stringify(!production),
"process.env.IS_TEST": JSON.stringify(test),
},
plugins: [
copyWasmFiles,
/* add to the end of plugins array */
+1 -1
View File
@@ -2,7 +2,7 @@
العربية | <a href="https://github.com/cline/cline/blob/main/locales/es/README.md" target="_blank">الإسبانية</a> | <a href="https://github.com/cline/cline/blob/main/locales/de/README.md" target="_blank">الألمانية</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/pt-BR/README.md" target="_blank">البرتغالية</a>
</sub></div>
# Cline \#1 على OpenRouter
# Cline
<p align="center">
<img src="https://media.githubusercontent.com/media/cline/cline/main/assets/docs/demo.gif" width="100%" />
+1 -1
View File
@@ -1,4 +1,4 @@
# Cline \#1 auf OpenRouter
# Cline
<p align="center">
<img src="https://media.githubusercontent.com/media/cline/cline/main/assets/docs/demo.gif" width="100%" />
+1 -1
View File
@@ -1,4 +1,4 @@
# Cline #1 en OpenRouter
# Cline
<p align="center">
<img src="https://media.githubusercontent.com/media/cline/cline/main/assets/docs/demo.gif" width="100%" />
+1 -1
View File
@@ -1,4 +1,4 @@
# Clineへの貢献
# Cline
Clineへの貢献に興味をお持ちいただきありがとうございます。
+1 -1
View File
@@ -1,4 +1,4 @@
# Cline OpenRouterでのナンバーワン
# Cline
<p align="center">
<img src="https://media.githubusercontent.com/media/cline/cline/main/assets/docs/demo.gif" width="100%" />
+1 -1
View File
@@ -1,4 +1,4 @@
# Cline에 기여하기
# Cline
Cline에 기여하는 것에 관심을 가져주셔서 감사합니다! 버그 수정, 기능 추가, 문서 개선 등 모든 기여는 Cline을 더욱 스마트하게 만드는 데 기여합니다. 활기차고 환영하는 커뮤니티를 유지하기 위해 모든 구성원은 [행동 강령](CODE_OF_CONDUCT.md)을 준수해야 합니다.
+1 -1
View File
@@ -1,4 +1,4 @@
# Cline - 최고의 OpenRouter
# Cline
<p align="center">
<img src="https://media.githubusercontent.com/media/cline/cline/main/assets/docs/demo.gif" width="100%" />
+1 -1
View File
@@ -1,4 +1,4 @@
# Cline #1 no OpenRouter
# Cline
<p align="center">
<img src="https://media.githubusercontent.com/media/cline/cline/main/assets/docs/demo.gif" width="100%" />
+1 -1
View File
@@ -1,4 +1,4 @@
# Cline OpenRouter 排名第一的 AI 工具
# Cline
<p align="center">
<img src="https://media.githubusercontent.com/media/cline/cline/main/assets/docs/demo.gif" width="100%" />
+1 -1
View File
@@ -2,7 +2,7 @@
<a href="https://github.com/cline/cline/blob/main/README.md" target="_blank">English</a> | <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/ko/README.md" target="_blank">한국어</a>
</sub></div>
# Cline OpenRouter 第一名的 AI 工具
# Cline
<p align="center">
<img src="https://media.githubusercontent.com/media/cline/cline/main/assets/docs/demo.gif" width="100%" />
+78 -2
View File
@@ -1,12 +1,12 @@
{
"name": "claude-dev",
"version": "3.9.2",
"version": "3.12.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-dev",
"version": "3.9.2",
"version": "3.12.0",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.12.4",
@@ -23,6 +23,7 @@
"@opentelemetry/sdk-node": "^0.39.1",
"@opentelemetry/sdk-trace-node": "^1.30.1",
"@opentelemetry/semantic-conventions": "^1.30.0",
"@sentry/browser": "^9.12.0",
"@vscode/codicons": "^0.0.36",
"axios": "^1.8.2",
"cheerio": "^1.0.0",
@@ -6856,6 +6857,81 @@
"integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==",
"license": "MIT"
},
"node_modules/@sentry-internal/browser-utils": {
"version": "9.12.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.12.0.tgz",
"integrity": "sha512-GXuDEG2Ix8DmVtTkjsItWdusk2CvJ6EPWKYVqFKifxt+IAT3ZbhGZd99Rg3wdRmt9xhCNuS4QrDzDTPMPgfdCw==",
"license": "MIT",
"dependencies": {
"@sentry/core": "9.12.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry-internal/feedback": {
"version": "9.12.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.12.0.tgz",
"integrity": "sha512-3+UxoT97QIXNSUQS4ATL1FFws0RkUb6PeaQN8CPndI6mFlqTW5tuVVLNg9Eo1seNg7R/dfk6WHCWrYN1NbFFKQ==",
"license": "MIT",
"dependencies": {
"@sentry/core": "9.12.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry-internal/replay": {
"version": "9.12.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.12.0.tgz",
"integrity": "sha512-njEQosFeO/UX+gG+DMRANkPUuz6OIJLb+A1GVylhq9adUgFQydQ9Ay3v7/x1gMhdfHVP6Jeb27qkti0BWYbzBQ==",
"license": "MIT",
"dependencies": {
"@sentry-internal/browser-utils": "9.12.0",
"@sentry/core": "9.12.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry-internal/replay-canvas": {
"version": "9.12.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.12.0.tgz",
"integrity": "sha512-p8LuKZgWT/CoQBbDOXkSGjWWnc8WsnAayWgna8M/ZFWNITCNEM2rCuqZOyWOElIlrni+M7qoEA3jS7MZe8Ejxw==",
"license": "MIT",
"dependencies": {
"@sentry-internal/replay": "9.12.0",
"@sentry/core": "9.12.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry/browser": {
"version": "9.12.0",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.12.0.tgz",
"integrity": "sha512-4xQYoZqi+VVhNvlhWiwRd57+SMr3Og4sLjuayAA+zIp1Wx/bDcIld697cugLwml/BR+mVJI2eokkgh1CBl6zag==",
"license": "MIT",
"dependencies": {
"@sentry-internal/browser-utils": "9.12.0",
"@sentry-internal/feedback": "9.12.0",
"@sentry-internal/replay": "9.12.0",
"@sentry-internal/replay-canvas": "9.12.0",
"@sentry/core": "9.12.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry/core": {
"version": "9.12.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.12.0.tgz",
"integrity": "sha512-jOqQK/90uzHmsBvkPTj/DAEFvA5poX4ZRyC7LE1zjg4F5jdOp3+M4W3qCy0CkSTu88Zu5VWBoppCU2Bs34XEqg==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@sindresorhus/merge-streams": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz",
+8 -2
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.9.2",
"version": "3.12.2",
"icon": "assets/icons/icon.png",
"engines": {
"vscode": "^1.84.0"
@@ -287,8 +287,12 @@
"compile": "npm run check-types && npm run lint && node esbuild.js",
"watch": "npm-run-all -p watch:*",
"watch:esbuild": "node esbuild.js --watch",
"watch:esbuild:test": "IS_TEST=true node esbuild.js --watch",
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
"package": "npm run build:webview && npm run check-types && npm run lint && node esbuild.js --production",
"package:test": "IS_TEST=true npm run build:webview:test && npm run check-types && npm run lint && IS_TEST=true node esbuild.js --production",
"build:webview:test": "cd webview-ui && IS_TEST=true npm run build",
"watch:test": "IS_TEST=true npm-run-all -p watch:tsc watch:esbuild:test",
"compile-tests": "tsc -p ./tsconfig.test.json --outDir out",
"watch-tests": "tsc -p . -w --outDir out",
"pretest": "npm run compile-tests && npm run compile && npm run lint",
@@ -296,8 +300,9 @@
"lint": "eslint src --ext ts && eslint webview-ui/src --ext ts",
"format": "prettier . --check",
"format:fix": "prettier . --write",
"test": "vscode-test",
"test": "npm-run-all test:unit test:integration",
"test:ci": "node scripts/test-ci.js",
"test:integration": "vscode-test",
"test:unit": "TS_NODE_PROJECT='./tsconfig.unit-test.json' mocha",
"test:coverage": "vscode-test --coverage",
"install:all": "npm install && cd webview-ui && npm install",
@@ -355,6 +360,7 @@
"@opentelemetry/sdk-node": "^0.39.1",
"@opentelemetry/sdk-trace-node": "^1.30.1",
"@opentelemetry/semantic-conventions": "^1.30.0",
"@sentry/browser": "^9.12.0",
"@vscode/codicons": "^0.0.36",
"axios": "^1.8.2",
"cheerio": "^1.0.0",
+2 -2
View File
@@ -9,10 +9,10 @@ try {
execSync("which xvfb-run", { stdio: "ignore" })
console.log("xvfb-run is installed. Running tests with xvfb-run...")
execSync("xvfb-run -a npm run test", { stdio: "inherit" })
execSync("xvfb-run -a npm run test:integration", { stdio: "inherit" })
} else {
console.log("Non-Linux environment detected. Running tests normally.")
execSync("npm run test", { stdio: "inherit" })
execSync("npm run test:integration", { stdio: "inherit" })
}
} catch (error) {
if (process.platform === "linux") {
+228
View File
@@ -0,0 +1,228 @@
import { describe, it, beforeEach, afterEach, before } from "mocha"
import "should"
import sinon from "sinon"
import { Anthropic } from "@anthropic-ai/sdk"
import { OllamaHandler } from "../ollama"
import { ApiHandlerOptions } from "../../../shared/api"
import axios from "axios"
describe("OllamaHandler", () => {
let ollamaAvailable = false
// Check if Ollama is running before running tests
before(async function () {
this.timeout(5000)
try {
await axios.get("http://localhost:11434/api/version", { timeout: 2000 })
ollamaAvailable = true
} catch (error) {
console.log("Ollama server not available, skipping tests")
ollamaAvailable = false
}
})
let handler: OllamaHandler
let options: ApiHandlerOptions
let clock: sinon.SinonFakeTimers
beforeEach(() => {
options = {
ollamaModelId: "llama2",
ollamaBaseUrl: "http://localhost:11434",
}
handler = new OllamaHandler(options)
// Use fake timers for testing timeouts
clock = sinon.useFakeTimers()
})
afterEach(() => {
clock.restore()
sinon.restore()
})
describe("createMessage", () => {
it("should handle successful responses", async function () {
if (!ollamaAvailable) {
this.skip()
}
this.timeout(5000)
// Mock the Ollama client's chat method
const chatStub = sinon.stub(handler["client"], "chat").resolves({
[Symbol.asyncIterator]: async function* () {
yield {
message: { content: "Hello, world!" },
eval_count: 10,
prompt_eval_count: 20,
}
},
} as any)
const systemPrompt = "You are a helpful assistant."
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
const result = []
const usageInfo = []
// Collect the results
for await (const chunk of handler.createMessage(systemPrompt, messages)) {
if (chunk.type === "text") {
result.push(chunk.text)
} else if (chunk.type === "usage") {
usageInfo.push({
inputTokens: chunk.inputTokens,
outputTokens: chunk.outputTokens,
})
}
}
// Verify the results
result.should.deepEqual(["Hello, world!"])
usageInfo.should.deepEqual([{ inputTokens: 20, outputTokens: 10 }])
chatStub.calledOnce.should.be.true()
})
it("should handle timeout errors", async function () {
if (!ollamaAvailable) {
this.skip()
}
this.timeout(10000)
// Restore real timers for this test
clock.restore()
// Create a handler with a very short timeout for testing
const testHandler = new OllamaHandler(options)
// Replace the createMessage method with one that has a shorter timeout
testHandler.createMessage = async function* (systemPrompt, messages) {
try {
// Create a promise that rejects after a short timeout
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error("Ollama request timed out after 30 seconds")), 100)
})
// Create a promise that never resolves
const neverPromise = new Promise(() => {})
// Race them
await Promise.race([timeoutPromise, neverPromise])
} catch (error: any) {
// Enhance error reporting
console.error(`Ollama API error: ${error.message}`)
throw error
}
}
const systemPrompt = "You are a helpful assistant."
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
// Start the request and catch the error
let errorMessage = ""
try {
for await (const _ of testHandler.createMessage(systemPrompt, messages)) {
// This should not be reached
}
} catch (error: any) {
errorMessage = error.message
}
// Check the result
errorMessage.should.equal("Ollama request timed out after 30 seconds")
// Restore the fake timers for other tests
clock = sinon.useFakeTimers()
})
it("should retry on errors when using the withRetry decorator", async function () {
if (!ollamaAvailable) {
this.skip()
}
this.timeout(10000)
// Restore real timers for this test
clock.restore()
// Mock the Ollama client's chat method to fail on first call and succeed on second
const chatStub = sinon.stub(handler["client"], "chat")
// First call throws an error
chatStub.onFirstCall().rejects(new Error("API Error"))
// Second call succeeds
chatStub.onSecondCall().resolves({
[Symbol.asyncIterator]: async function* () {
yield {
message: { content: "Success after retry" },
}
},
} as any)
const systemPrompt = "You are a helpful assistant."
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
const result = []
// Add a small delay to ensure the retry mechanism has time to work
await new Promise((resolve) => setTimeout(resolve, 100))
// Collect the results
for await (const chunk of handler.createMessage(systemPrompt, messages)) {
if (chunk.type === "text") {
result.push(chunk.text)
}
}
// Verify the results
result.should.deepEqual(["Success after retry"])
chatStub.calledTwice.should.be.true()
// Restore the fake timers for other tests
clock = sinon.useFakeTimers()
})
it("should handle stream processing errors", async function () {
if (!ollamaAvailable) {
this.skip()
}
this.timeout(10000)
// Restore real timers for this test
clock.restore()
// Create a handler with a custom implementation for testing
const testHandler = new OllamaHandler(options)
// Replace the createMessage method with one that simulates a stream error
testHandler.createMessage = async function* (systemPrompt, messages) {
// First yield a successful chunk
yield {
type: "text",
text: "Partial response",
}
// Then throw an error in the stream
throw new Error("Ollama stream processing error: Stream error")
}
const systemPrompt = "You are a helpful assistant."
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
const result = []
// Collect the results and catch the error
let errorMessage = ""
try {
for await (const chunk of testHandler.createMessage(systemPrompt, messages)) {
if (chunk.type === "text") {
result.push(chunk.text)
}
}
} catch (error: any) {
errorMessage = error.message
}
// Verify the results
errorMessage.should.equal("Ollama stream processing error: Stream error")
result.should.deepEqual(["Partial response"])
// Restore the fake timers for other tests
clock = sinon.useFakeTimers()
})
})
})
+2 -2
View File
@@ -114,7 +114,7 @@ export class AnthropicHandler implements ApiHandler {
break
}
default: {
stream = (await this.client.messages.create({
stream = await this.client.messages.create({
model: modelId,
max_tokens: model.info.maxTokens || 8192,
temperature: 0,
@@ -123,7 +123,7 @@ export class AnthropicHandler implements ApiHandler {
// tools,
// tool_choice: { type: "auto" },
stream: true,
})) as any
})
break
}
}
+53 -13
View File
@@ -4,6 +4,7 @@ import { ApiHandler } from "../"
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api"
import { convertToOllamaMessages } from "../transform/ollama-format"
import { ApiStream } from "../transform/stream"
import { withRetry } from "../retry"
export class OllamaHandler implements ApiHandler {
private options: ApiHandlerOptions
@@ -14,24 +15,63 @@ export class OllamaHandler implements ApiHandler {
this.client = new Ollama({ host: this.options.ollamaBaseUrl || "http://localhost:11434" })
}
@withRetry({ retryAllErrors: true })
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const ollamaMessages: Message[] = [{ role: "system", content: systemPrompt }, ...convertToOllamaMessages(messages)]
const stream = await this.client.chat({
model: this.getModel().id,
messages: ollamaMessages,
stream: true,
options: {
num_ctx: Number(this.options.ollamaApiOptionsCtxNum) || 32768,
},
})
for await (const chunk of stream) {
if (typeof chunk.message.content === "string") {
yield {
type: "text",
text: chunk.message.content,
try {
// Create a promise that rejects after timeout
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error("Ollama request timed out after 30 seconds")), 30000)
})
// Create the actual API request promise
const apiPromise = this.client.chat({
model: this.getModel().id,
messages: ollamaMessages,
stream: true,
options: {
num_ctx: Number(this.options.ollamaApiOptionsCtxNum) || 32768,
},
})
// Race the API request against the timeout
const stream = (await Promise.race([apiPromise, timeoutPromise])) as Awaited<typeof apiPromise>
try {
for await (const chunk of stream) {
if (typeof chunk.message.content === "string") {
yield {
type: "text",
text: chunk.message.content,
}
}
// Handle token usage if available
if (chunk.eval_count !== undefined || chunk.prompt_eval_count !== undefined) {
yield {
type: "usage",
inputTokens: chunk.prompt_eval_count || 0,
outputTokens: chunk.eval_count || 0,
}
}
}
} catch (streamError: any) {
console.error("Error processing Ollama stream:", streamError)
throw new Error(`Ollama stream processing error: ${streamError.message || "Unknown error"}`)
}
} catch (error: any) {
// Check if it's a timeout error
if (error.message && error.message.includes("timed out")) {
throw new Error("Ollama request timed out after 30 seconds")
}
// Enhance error reporting
const statusCode = error.status || error.statusCode
const errorMessage = error.message || "Unknown error"
console.error(`Ollama API error (${statusCode || "unknown"}): ${errorMessage}`)
throw error
}
}
+1 -1
View File
@@ -12,7 +12,7 @@ import {
import { convertToOpenAiMessages } from "../transform/openai-format"
import { calculateApiCostOpenAI } from "../../utils/cost"
import { ApiStream } from "../transform/stream"
import { ChatCompletionReasoningEffort } from "openai/resources/chat/completions.mjs"
import type { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
export class OpenAiNativeHandler implements ApiHandler {
private options: ApiHandlerOptions
+1 -1
View File
@@ -6,7 +6,7 @@ import { ApiHandler } from "../index"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { convertToR1Format } from "../transform/r1-format"
import { ChatCompletionReasoningEffort } from "openai/resources/chat/completions.mjs"
import type { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
export class OpenAiHandler implements ApiHandler {
private options: ApiHandlerOptions
+24 -8
View File
@@ -1,9 +1,10 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api"
import { ApiHandlerOptions, ModelInfo, requestyDefaultModelId, requestyDefaultModelInfo } from "../../shared/api"
import { ApiHandler } from "../index"
import { withRetry } from "../retry"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { calculateApiCostOpenAI } from "../../utils/cost"
import { ApiStream } from "../transform/stream"
export class RequestyHandler implements ApiHandler {
@@ -24,21 +25,34 @@ export class RequestyHandler implements ApiHandler {
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const modelId = this.options.requestyModelId ?? ""
const model = this.getModel()
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
const reasoningEffort = this.options.o3MiniReasoningEffort || "medium"
const reasoning = { reasoning_effort: reasoningEffort }
const reasoningArgs = model.id === "openai/o3-mini" ? reasoning : {}
const thinkingBudget = this.options.thinkingBudgetTokens || 0
const thinking =
thinkingBudget > 0
? { thinking: { type: "enabled", budget_tokens: thinkingBudget } }
: { thinking: { type: "disabled" } }
const thinkingArgs = model.id.includes("claude-3-7-sonnet") ? thinking : {}
// @ts-ignore-next-line
const stream = await this.client.chat.completions.create({
model: modelId,
model: model.id,
max_tokens: model.info.maxTokens || undefined,
messages: openAiMessages,
temperature: 0,
stream: true,
stream_options: { include_usage: true },
...(modelId === "openai/o3-mini" ? { reasoning_effort: this.options.o3MiniReasoningEffort || "medium" } : {}),
...reasoningArgs,
...thinkingArgs,
})
for await (const chunk of stream) {
@@ -73,7 +87,7 @@ export class RequestyHandler implements ApiHandler {
const outputTokens = usage.completion_tokens || 0
const cacheWriteTokens = usage.prompt_tokens_details?.caching_tokens || undefined
const cacheReadTokens = usage.prompt_tokens_details?.cached_tokens || undefined
const totalCost = 0 // TODO: Replace with calculateApiCostOpenAI(model.info, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens)
const totalCost = calculateApiCostOpenAI(model.info, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens)
yield {
type: "usage",
@@ -88,9 +102,11 @@ export class RequestyHandler implements ApiHandler {
}
getModel(): { id: string; info: ModelInfo } {
return {
id: this.options.requestyModelId ?? "",
info: openAiModelInfoSaneDefaults,
const modelId = this.options.requestyModelId
const modelInfo = this.options.requestyModelInfo
if (modelId && modelInfo) {
return { id: modelId, info: modelInfo }
}
return { id: requestyDefaultModelId, info: requestyDefaultModelInfo }
}
}
+7
View File
@@ -1,6 +1,13 @@
// For the following openrouter error type sources, see the docs here:
// https://openrouter.ai/docs/api-reference/errors
export interface LanguageModelChatSelector {
vendor?: string
family?: string
version?: string
id?: string
}
export type OpenRouterErrorResponse = {
error: {
message: string
+2 -6
View File
@@ -6,6 +6,7 @@ import { ApiStream } from "../transform/stream"
import { convertToVsCodeLmMessages } from "../transform/vscode-lm-format"
import { SELECTOR_SEPARATOR, stringifyVsCodeLmModelSelector } from "../../shared/vsCodeSelectorUtils"
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api"
import type { LanguageModelChatSelector as LanguageModelChatSelectorFromTypes } from "./types"
// Cline does not update VSCode type definitions or engine requirements to maintain compatibility.
// This declaration (as seen in src/integrations/TerminalManager.ts) provides types for the Language Model API in newer versions of VSCode.
@@ -19,12 +20,7 @@ declare module "vscode" {
Auto = 1,
Required = 2,
}
interface LanguageModelChatSelector {
vendor?: string
family?: string
version?: string
id?: string
}
interface LanguageModelChatSelector extends LanguageModelChatSelectorFromTypes {}
interface LanguageModelChatTool {
name: string
description: string
+20 -1
View File
@@ -4,6 +4,7 @@ import { ApiHandler } from "../"
import { ApiHandlerOptions, XAIModelId, ModelInfo, xaiDefaultModelId, xaiModels } from "../../shared/api"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { ChatCompletionReasoningEffort } from "openai/resources/chat/completions.mjs"
export class XAIHandler implements ApiHandler {
private options: ApiHandlerOptions
@@ -18,13 +19,23 @@ export class XAIHandler implements ApiHandler {
}
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const modelId = this.getModel().id
// ensure reasoning effort is either "low" or "high" for grok-3-mini
let reasoningEffort: ChatCompletionReasoningEffort | undefined
if (modelId.includes("3-mini")) {
let reasoningEffort = this.options.reasoningEffort
if (reasoningEffort && !["low", "high"].includes(reasoningEffort)) {
reasoningEffort = undefined
}
}
const stream = await this.client.chat.completions.create({
model: this.getModel().id,
model: modelId,
max_completion_tokens: this.getModel().info.maxTokens,
temperature: 0,
messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
stream: true,
stream_options: { include_usage: true },
reasoning_effort: reasoningEffort,
})
for await (const chunk of stream) {
@@ -36,6 +47,14 @@ export class XAIHandler implements ApiHandler {
}
}
if ("reasoning_content" in delta && delta.reasoning_content) {
yield {
type: "reasoning",
// @ts-ignore-next-line
reasoning: delta.reasoning_content,
}
}
if (chunk.usage) {
yield {
type: "usage",
+1 -1
View File
@@ -20,7 +20,7 @@ export function convertAnthropicContentToGemini(content: string | Anthropic.Cont
},
} as InlineDataPart
default:
throw new Error(`Unsupported content block type: ${(block as any).type}`)
throw new Error(`Unsupported content block type: ${block.type}`)
}
})
}
+211
View File
@@ -0,0 +1,211 @@
// This file contains `declare module "vscode"` so we must import it.
import "../providers/vscode-lm"
import { describe, it } from "mocha"
import "should"
import * as vscode from "vscode"
import { Anthropic } from "@anthropic-ai/sdk"
import { asObjectSafe, convertToAnthropicRole, convertToVsCodeLmMessages, convertToAnthropicMessage } from "./vscode-lm-format"
describe("asObjectSafe", () => {
it("should handle falsy values", () => {
asObjectSafe(0).should.deepEqual({})
asObjectSafe("").should.deepEqual({})
asObjectSafe(null).should.deepEqual({})
asObjectSafe(undefined).should.deepEqual({})
})
it("should parse valid JSON strings", () => {
asObjectSafe('{"key": "value"}').should.deepEqual({ key: "value" })
})
it("should return an empty object for invalid JSON strings", () => {
asObjectSafe("invalid json").should.deepEqual({})
})
it("should convert objects to plain objects", () => {
const input = { prop: "value" }
asObjectSafe(input).should.deepEqual(input)
asObjectSafe(input).should.not.equal(input) // Should be a new object
})
it("should convert arrays to plain objects", () => {
const input = ["hello world"]
asObjectSafe(input).should.deepEqual({ 0: "hello world" })
})
})
describe("convertToAnthropicRole", () => {
it("should convert VSCode roles to Anthropic roles", () => {
// @ts-expect-errorTesting with an invalid role
const unknownRole = "unknown" as vscode.LanguageModelChatMessageRole
;(convertToAnthropicRole(vscode.LanguageModelChatMessageRole.Assistant) === "assistant").should.be.true()
;(convertToAnthropicRole(vscode.LanguageModelChatMessageRole.User) === "user").should.be.true()
;(convertToAnthropicRole(unknownRole) === null).should.be.true()
})
})
describe("convertToVsCodeLmMessages", () => {
it("should convert simple string messages", () => {
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
{ role: "user", content: "Hello" },
{ role: "assistant", content: "Hi there" },
]
const result = convertToVsCodeLmMessages(anthropicMessages)
result.should.have.length(2)
result[0].role.should.equal(vscode.LanguageModelChatMessageRole.User)
result[0].content[0].should.be.instanceof(vscode.LanguageModelTextPart)
const textPart0 = result[0].content[0] as vscode.LanguageModelTextPart
textPart0.should.have.property("value", "Hello")
result[1].role.should.equal(vscode.LanguageModelChatMessageRole.Assistant)
result[1].content[0].should.be.instanceof(vscode.LanguageModelTextPart)
const textPart1 = result[1].content[0] as vscode.LanguageModelTextPart
textPart1.should.have.property("value", "Hi there")
})
it("should convert complex user messages with tool results", () => {
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
{
role: "user",
content: [
{ type: "text", text: "User text" },
{
type: "tool_result",
tool_use_id: "tool-123",
content: [{ type: "text", text: "Tool result" }],
},
],
},
]
const result = convertToVsCodeLmMessages(anthropicMessages)
result.should.have.length(1)
result[0].role.should.equal(vscode.LanguageModelChatMessageRole.User)
result[0].content.should.have.length(2)
// Check that the first content part is a ToolResultPart
result[0].content[0].should.be.instanceof(vscode.LanguageModelToolResultPart)
const toolResultPart = result[0].content[0] as vscode.LanguageModelToolResultPart
toolResultPart.should.have.property("callId", "tool-123")
// Skip detailed testing of internal structure as it may vary
// Just verify it's the right type with the right ID
// Check the second content part is a TextPart
result[0].content[1].should.be.instanceof(vscode.LanguageModelTextPart)
const textPart = result[0].content[1] as vscode.LanguageModelTextPart
textPart.should.have.property("value", "User text")
})
it("should convert complex assistant messages with tool calls", () => {
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
{
role: "assistant",
content: [
{ type: "text", text: "Assistant text" },
{
type: "tool_use",
id: "tool-123",
name: "testTool",
input: { param: "value" },
},
],
},
]
const result = convertToVsCodeLmMessages(anthropicMessages)
result.should.have.length(1)
result[0].role.should.equal(vscode.LanguageModelChatMessageRole.Assistant)
result[0].content.should.have.length(2)
result[0].content[0].should.be.instanceof(vscode.LanguageModelToolCallPart)
const toolCallPart = result[0].content[0] as vscode.LanguageModelToolCallPart
toolCallPart.should.have.property("callId", "tool-123")
toolCallPart.should.have.property("name", "testTool")
toolCallPart.should.have.property("input")
toolCallPart.input.should.deepEqual({ param: "value" })
result[0].content[1].should.be.instanceof(vscode.LanguageModelTextPart)
const textPart = result[0].content[1] as vscode.LanguageModelTextPart
textPart.should.have.property("value", "Assistant text")
})
it("should handle image blocks with appropriate placeholders", () => {
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
{
role: "user",
content: [
{
type: "image",
source: {
type: "base64",
media_type: "image/jpeg",
data: "base64data",
},
},
],
},
]
const result = convertToVsCodeLmMessages(anthropicMessages)
result.should.have.length(1)
result[0].content[0].should.be.instanceof(vscode.LanguageModelTextPart)
const textPart = result[0].content[0] as vscode.LanguageModelTextPart
textPart.should.have.property("value")
textPart.value.should.match(/Image \(base64\): image\/jpeg not supported by VSCode LM API/)
})
})
describe("convertToAnthropicMessage", () => {
it("should convert VSCode assistant messages to Anthropic format", () => {
const vsCodeMsg = vscode.LanguageModelChatMessage.Assistant([
new vscode.LanguageModelTextPart("Test message"),
new vscode.LanguageModelToolCallPart("tool-id", "testTool", { param: "value" }),
])
const result = convertToAnthropicMessage(vsCodeMsg)
result.should.have.property("role", "assistant")
result.should.have.property("content").which.is.an.Array()
result.content.should.have.length(2)
// Check properties carefully to avoid null reference errors
if (result.content && result.content.length >= 1) {
const textContent = result.content[0]
if (textContent) {
textContent.should.have.property("type", "text")
if (textContent.type === "text") {
textContent.should.have.property("text", "Test message")
}
}
}
if (result.content && result.content.length >= 2) {
const toolContent = result.content[1]
if (toolContent) {
toolContent.should.have.property("type", "tool_use")
if (toolContent.type === "tool_use") {
toolContent.should.have.property("id", "tool-id")
toolContent.should.have.property("name", "testTool")
toolContent.should.have.property("input").which.deepEqual({ param: "value" })
}
}
}
})
it("should throw an error for non-assistant messages", () => {
const vsCodeMsg = vscode.LanguageModelChatMessage.User("User message")
try {
convertToAnthropicMessage(vsCodeMsg)
throw new Error("Should have thrown an error")
} catch (error: any) {
error.message.should.match(/Only assistant messages are supported/)
}
})
})
+6 -6
View File
@@ -4,7 +4,7 @@ import * as vscode from "vscode"
/**
* Safely converts a value into a plain object.
*/
function asObjectSafe(value: any): object {
export function asObjectSafe(value: any): object {
// Handle null/undefined
if (!value) {
return {}
@@ -145,7 +145,9 @@ export function convertToVsCodeLmMessages(
return vsCodeLmMessages
}
export function convertToAnthropicRole(vsCodeLmMessageRole: vscode.LanguageModelChatMessageRole): string | null {
export function convertToAnthropicRole(
vsCodeLmMessageRole: vscode.LanguageModelChatMessageRole,
): Anthropic.Messages.MessageParam["role"] | null {
switch (vsCodeLmMessageRole) {
case vscode.LanguageModelChatMessageRole.Assistant:
return "assistant"
@@ -156,10 +158,8 @@ export function convertToAnthropicRole(vsCodeLmMessageRole: vscode.LanguageModel
}
}
export async function convertToAnthropicMessage(
vsCodeLmMessage: vscode.LanguageModelChatMessage,
): Promise<Anthropic.Messages.Message> {
const anthropicRole: string | null = convertToAnthropicRole(vsCodeLmMessage.role)
export function convertToAnthropicMessage(vsCodeLmMessage: vscode.LanguageModelChatMessage): Anthropic.Messages.Message {
const anthropicRole = convertToAnthropicRole(vsCodeLmMessage.role)
if (anthropicRole !== "assistant") {
throw new Error("Cline <Language Model API>: Only assistant messages are supported.")
}
+1 -1
View File
@@ -284,7 +284,7 @@ export async function constructNewFileContent(diffContent: string, originalConte
;[searchMatchIndex, searchEndIndex] = blockMatch
} else {
throw new Error(
`The SEARCH block:\n${currentSearchContent.trimEnd()}\n...does not match anything in the file.`,
`The SEARCH block:\n${currentSearchContent.trimEnd()}\n...does not match anything in the file or was searched out of order in the provided blocks.`,
)
}
}
+2
View File
@@ -23,6 +23,7 @@ export const toolUseNames = [
"plan_mode_respond",
"load_mcp_documentation",
"attempt_completion",
"new_task",
] as const
// Converts array of tool call names into a union type ("execute_command" | "read_file" | ...)
@@ -49,6 +50,7 @@ export const toolParamNames = [
"options",
"response",
"result",
"context",
] as const
export type ToolParamName = (typeof toolParamNames)[number]
@@ -1,10 +0,0 @@
export function checkIsOpenRouterContextWindowError(error: any): boolean {
return error.code === 400 && error.message?.includes("context length")
}
export function checkIsAnthropicContextWindowError(response: any): boolean {
return (
response?.error?.error?.type === "invalid_request_error" &&
response?.error?.error?.message?.includes("prompt is too long")
)
}
@@ -0,0 +1,102 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ClineApiReqInfo, ClineMessage } from "../../../shared/ExtensionMessage"
import { ApiHandler } from "../../../api"
import { getContextWindowInfo } from "./context-window-utils"
class ContextManager {
getNewContextMessagesAndMetadata(
apiConversationHistory: Anthropic.Messages.MessageParam[],
clineMessages: ClineMessage[],
api: ApiHandler,
conversationHistoryDeletedRange: [number, number] | undefined,
previousApiReqIndex: number,
) {
let updatedConversationHistoryDeletedRange = false
// If the previous API request's total token usage is close to the context window, truncate the conversation history to free up space for the new request
if (previousApiReqIndex >= 0) {
const previousRequest = clineMessages[previousApiReqIndex]
if (previousRequest && previousRequest.text) {
const { tokensIn, tokensOut, cacheWrites, cacheReads }: ClineApiReqInfo = JSON.parse(previousRequest.text)
const totalTokens = (tokensIn || 0) + (tokensOut || 0) + (cacheWrites || 0) + (cacheReads || 0)
const { maxAllowedSize } = getContextWindowInfo(api)
// This is the most reliable way to know when we're close to hitting the context window.
if (totalTokens >= maxAllowedSize) {
// Since the user may switch between models with different context windows, truncating half may not be enough (ie if switching from claude 200k to deepseek 64k, half truncation will only remove 100k tokens, but we need to remove much more)
// So if totalTokens/2 is greater than maxAllowedSize, we truncate 3/4 instead of 1/2
// FIXME: truncating the conversation in a way that is optimal for prompt caching AND takes into account multi-context window complexity is something we need to improve
const keep = totalTokens / 2 > maxAllowedSize ? "quarter" : "half"
// NOTE: it's okay that we overwriteConversationHistory in resume task since we're only ever removing the last user message and not anything in the middle which would affect this range
conversationHistoryDeletedRange = this.getNextTruncationRange(
apiConversationHistory,
conversationHistoryDeletedRange,
keep,
)
updatedConversationHistoryDeletedRange = true
}
}
}
// conversationHistoryDeletedRange is updated only when we're close to hitting the context window, so we don't continuously break the prompt cache
const truncatedConversationHistory = this.getTruncatedMessages(apiConversationHistory, conversationHistoryDeletedRange)
return {
conversationHistoryDeletedRange: conversationHistoryDeletedRange,
updatedConversationHistoryDeletedRange: updatedConversationHistoryDeletedRange,
truncatedConversationHistory: truncatedConversationHistory,
}
}
public getNextTruncationRange(
apiMessages: Anthropic.Messages.MessageParam[],
currentDeletedRange: [number, number] | undefined,
keep: "half" | "quarter",
): [number, number] {
// Since we always keep the first message, currentDeletedRange[0] will always be 1 (for now until we have a smarter truncation algorithm)
const rangeStartIndex = 1
const startOfRest = currentDeletedRange ? currentDeletedRange[1] + 1 : 1
let messagesToRemove: number
if (keep === "half") {
// Remove half of remaining user-assistant pairs
// We first calculate half of the messages then divide by 2 to get the number of pairs.
// After flooring, we multiply by 2 to get the number of messages.
// Note that this will also always be an even number.
messagesToRemove = Math.floor((apiMessages.length - startOfRest) / 4) * 2 // Keep even number
} else {
// Remove 3/4 of remaining user-assistant pairs
// We calculate 3/4ths of the messages then divide by 2 to get the number of pairs.
// After flooring, we multiply by 2 to get the number of messages.
// Note that this will also always be an even number.
messagesToRemove = Math.floor(((apiMessages.length - startOfRest) * 3) / 4 / 2) * 2
}
let rangeEndIndex = startOfRest + messagesToRemove - 1
// Make sure the last message being removed is a user message, so that the next message after the initial task message is an assistant message. This preservers the user-assistant-user-assistant structure.
// NOTE: anthropic format messages are always user-assistant-user-assistant, while openai format messages can have multiple user messages in a row (we use anthropic format throughout cline)
if (apiMessages[rangeEndIndex].role !== "user") {
rangeEndIndex -= 1
}
// this is an inclusive range that will be removed from the conversation history
return [rangeStartIndex, rangeEndIndex]
}
public getTruncatedMessages(
messages: Anthropic.Messages.MessageParam[],
deletedRange: [number, number] | undefined,
): Anthropic.Messages.MessageParam[] {
if (!deletedRange) {
return messages
}
const [start, end] = deletedRange
// the range is inclusive - both start and end indices and everything in between will be removed from the final result.
// NOTE: if you try to console log these, don't forget that logging a reference to an array may not provide the same result as logging a slice() snapshot of that array at that exact moment. The following DOES in fact include the latest assistant message.
return [...messages.slice(0, start), ...messages.slice(end + 1)]
}
}
@@ -1,13 +1,13 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { ClineApiReqInfo, ClineMessage } from "../../shared/ExtensionMessage"
import { ApiHandler } from "../../api"
import { OpenAiHandler } from "../../api/providers/openai"
import { formatResponse } from "../prompts/responses"
import { GlobalFileNames } from "../storage/disk"
import { fileExistsAtPath } from "../../utils/fs"
import { getContextWindowInfo } from "./context-window-utils"
import { formatResponse } from "../../prompts/responses"
import { GlobalFileNames } from "../../storage/disk"
import { fileExistsAtPath } from "../../../utils/fs"
import * as path from "path"
import fs from "fs/promises"
import cloneDeep from "clone-deep"
import { ClineApiReqInfo, ClineMessage } from "../../../shared/ExtensionMessage"
import { ApiHandler } from "../../../api"
import { Anthropic } from "@anthropic-ai/sdk"
enum EditType {
UNDEFINED = 0,
@@ -125,25 +125,7 @@ export class ContextManager {
const timestamp = previousRequest.ts
const { tokensIn, tokensOut, cacheWrites, cacheReads }: ClineApiReqInfo = JSON.parse(previousRequest.text)
const totalTokens = (tokensIn || 0) + (tokensOut || 0) + (cacheWrites || 0) + (cacheReads || 0)
let contextWindow = api.getModel().info.contextWindow || 128_000
// FIXME: hack to get anyone using openai compatible with deepseek to have the proper context window instead of the default 128k. We need a way for the user to specify the context window for models they input through openai compatible
if (api instanceof OpenAiHandler && api.getModel().id.toLowerCase().includes("deepseek")) {
contextWindow = 64_000
}
let maxAllowedSize: number
switch (contextWindow) {
case 64_000: // deepseek models
maxAllowedSize = contextWindow - 27_000
break
case 128_000: // most models
maxAllowedSize = contextWindow - 30_000
break
case 200_000: // claude models
maxAllowedSize = contextWindow - 40_000
break
default:
maxAllowedSize = Math.max(contextWindow - 40_000, contextWindow * 0.8) // for deepseek, 80% of 64k meant only ~10k buffer which was too small and resulted in users getting context window errors.
}
const { maxAllowedSize } = getContextWindowInfo(api)
// This is the most reliable way to know when we're close to hitting the context window.
if (totalTokens >= maxAllowedSize) {
@@ -0,0 +1,18 @@
export function checkIsOpenRouterContextWindowError(error: any): boolean {
try {
return error.code === 400 && error.message?.includes("context length")
} catch (e: unknown) {
return false
}
}
export function checkIsAnthropicContextWindowError(response: any): boolean {
try {
return (
response?.error?.error?.type === "invalid_request_error" &&
response?.error?.error?.message?.includes("prompt is too long")
)
} catch (e: unknown) {
return false
}
}
@@ -0,0 +1,35 @@
import { ApiHandler } from "../../../api"
import { OpenAiHandler } from "../../../api/providers/openai"
/**
* Gets context window information for the given API handler
*
* @param api The API handler to get context window information for
* @returns An object containing the raw context window size and the effective max allowed size
*/
export function getContextWindowInfo(api: ApiHandler) {
let contextWindow = api.getModel().info.contextWindow || 128_000
// FIXME: hack to get anyone using openai compatible with deepseek to have the proper context window instead of the default 128k. We need a way for the user to specify the context window for models they input through openai compatible
// Handle special cases like DeepSeek
if (api instanceof OpenAiHandler && api.getModel().id.toLowerCase().includes("deepseek")) {
contextWindow = 64_000
}
let maxAllowedSize: number
switch (contextWindow) {
case 64_000: // deepseek models
maxAllowedSize = contextWindow - 27_000
break
case 128_000: // most models
maxAllowedSize = contextWindow - 30_000
break
case 200_000: // claude models
maxAllowedSize = contextWindow - 40_000
break
default:
maxAllowedSize = Math.max(contextWindow - 40_000, contextWindow * 0.8) // for deepseek, 80% of 64k meant only ~10k buffer which was too small and resulted in users getting context window errors.
}
return { contextWindow, maxAllowedSize }
}
@@ -10,11 +10,14 @@ export interface FileMetadataEntry {
user_edit_date?: number | null
}
export interface TaskMetadata {
files_in_context: FileMetadataEntry[]
export interface ModelMetadataEntry {
ts: number
model_id: string
model_provider_id: string
mode: string
}
// Interface for the controller to avoid direct dependency
export interface ControllerLike {
context: vscode.ExtensionContext
export interface TaskMetadata {
files_in_context: FileMetadataEntry[]
model_usage: ModelMetadataEntry[]
}
@@ -4,12 +4,11 @@ import * as sinon from "sinon"
import * as vscode from "vscode"
import * as path from "path"
import { FileContextTracker } from "./FileContextTracker"
import * as diskModule from "../storage/disk"
import type { TaskMetadata, ControllerLike, FileMetadataEntry } from "./FileContextTrackerTypes"
import * as diskModule from "../../storage/disk"
import type { TaskMetadata, FileMetadataEntry } from "./ContextTrackerTypes"
describe("FileContextTracker", () => {
let sandbox: sinon.SinonSandbox
let mockController: ControllerLike
let mockContext: vscode.ExtensionContext
let mockWorkspace: sinon.SinonStub
let mockFileSystemWatcher: any
@@ -41,25 +40,21 @@ describe("FileContextTracker", () => {
const originalCreateFileSystemWatcher = vscode.workspace.createFileSystemWatcher
vscode.workspace.createFileSystemWatcher = function () {
return mockFileSystemWatcher
} as any
}
// Mock controller and context
mockContext = {
globalStorageUri: { fsPath: "/mock/storage" },
} as unknown as vscode.ExtensionContext
mockController = {
context: mockContext,
}
// Mock disk module functions
mockTaskMetadata = { files_in_context: [] }
mockTaskMetadata = { files_in_context: [], model_usage: [] }
getTaskMetadataStub = sandbox.stub(diskModule, "getTaskMetadata").resolves(mockTaskMetadata)
saveTaskMetadataStub = sandbox.stub(diskModule, "saveTaskMetadata").resolves()
// Create tracker instance
taskId = "test-task-id"
tracker = new FileContextTracker(mockController, taskId)
tracker = new FileContextTracker(mockContext, taskId)
})
afterEach(() => {
@@ -1,7 +1,7 @@
import * as path from "path"
import * as vscode from "vscode"
import { getTaskMetadata, saveTaskMetadata } from "../storage/disk"
import type { FileMetadataEntry, ControllerLike } from "./FileContextTrackerTypes"
import { getTaskMetadata, saveTaskMetadata } from "../../storage/disk"
import type { FileMetadataEntry } from "./ContextTrackerTypes"
// This class is responsible for tracking file operations that may result in stale context.
// If a user modifies a file outside of Cline, the context may become stale and need to be updated.
@@ -16,29 +16,19 @@ import type { FileMetadataEntry, ControllerLike } from "./FileContextTrackerType
// If the full contents of a file are pass to Cline via a tool, mention, or edit, the file is marked as active.
// If a file is modified outside of Cline, we detect and track this change to prevent stale context.
export class FileContextTracker {
private context: vscode.ExtensionContext
readonly taskId: string
private controllerRef: WeakRef<ControllerLike>
// File tracking and watching
private fileWatchers = new Map<string, vscode.FileSystemWatcher>()
private recentlyModifiedFiles = new Set<string>()
private recentlyEditedByCline = new Set<string>()
constructor(controller: ControllerLike, taskId: string) {
this.controllerRef = new WeakRef(controller)
constructor(context: vscode.ExtensionContext, taskId: string) {
this.context = context
this.taskId = taskId
}
// While a task is ref'd by a controller, it will always have access to the extension context
// This error is thrown if the controller derefs the task after e.g., aborting the task
private context(): vscode.ExtensionContext {
const context = this.controllerRef.deref()?.context
if (!context) {
throw new Error("Unable to access extension context")
}
return context
}
// Gets the current working directory or returns undefined if it cannot be determined
private getCwd(): string | undefined {
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
@@ -89,9 +79,8 @@ export class FileContextTracker {
return
}
const context = this.context()
// Add file to metadata
await this.addFileToFileContextTracker(context, this.taskId, filePath, operation)
await this.addFileToFileContextTracker(this.context, this.taskId, filePath, operation)
// Set up file watcher for this file
await this.setupFileWatcher(filePath)
@@ -0,0 +1,205 @@
import { describe, it, beforeEach, afterEach } from "mocha"
import { expect } from "chai"
import * as sinon from "sinon"
import * as vscode from "vscode"
import { ModelContextTracker } from "./ModelContextTracker"
import * as diskModule from "../../storage/disk"
import type { TaskMetadata } from "./ContextTrackerTypes"
describe("ModelContextTracker", () => {
let sandbox: sinon.SinonSandbox
let mockContext: vscode.ExtensionContext
let tracker: ModelContextTracker
let taskId: string
let mockTaskMetadata: TaskMetadata
let getTaskMetadataStub: sinon.SinonStub
let saveTaskMetadataStub: sinon.SinonStub
beforeEach(() => {
sandbox = sinon.createSandbox()
// Mock controller and context
mockContext = {
globalStorageUri: { fsPath: "/mock/storage" },
} as unknown as vscode.ExtensionContext
// Mock disk module functions
mockTaskMetadata = { files_in_context: [], model_usage: [] }
getTaskMetadataStub = sandbox.stub(diskModule, "getTaskMetadata").resolves(mockTaskMetadata)
saveTaskMetadataStub = sandbox.stub(diskModule, "saveTaskMetadata").resolves()
// Create tracker instance
taskId = "test-task-id"
tracker = new ModelContextTracker(mockContext, taskId)
})
afterEach(() => {
sandbox.restore()
})
it("should record model usage with correct data", async () => {
// Test data
const apiProviderId = "anthropic"
const modelId = "claude-3-opus"
const mode = "act"
// Use a fake timer to have a predictable timestamp
const fakeNow = 1617293940000 // Some fixed timestamp
const clock = sandbox.useFakeTimers(fakeNow)
try {
// Call the method being tested
await tracker.recordModelUsage(apiProviderId, modelId, mode)
// Verify getTaskMetadata was called with correct parameters
expect(getTaskMetadataStub.calledOnce).to.be.true
expect(getTaskMetadataStub.firstCall.args[1]).to.equal(taskId)
// Verify saveTaskMetadata was called with the correct data
expect(saveTaskMetadataStub.calledOnce).to.be.true
// Extract the saved metadata from the call arguments
const savedMetadata = saveTaskMetadataStub.firstCall.args[2]
// Verify model_usage array has one entry
expect(savedMetadata.model_usage.length).to.equal(1)
// Verify the entry has the correct properties
const modelUsageEntry = savedMetadata.model_usage[0]
expect(modelUsageEntry.ts).to.equal(fakeNow)
expect(modelUsageEntry.model_id).to.equal(modelId)
expect(modelUsageEntry.model_provider_id).to.equal(apiProviderId)
expect(modelUsageEntry.mode).to.equal(mode)
} finally {
// Restore the clock
clock.restore()
}
})
it("should throw an error when controller is dereferenced", async () => {
// Create a new tracker with a controller that will be garbage collected
const weakTracker = new ModelContextTracker(mockContext, taskId)
// Force the WeakRef to return null by overriding the deref method
const weakRef = { deref: sandbox.stub().returns(null) }
sandbox.stub(WeakRef.prototype, "deref").callsFake(() => weakRef.deref())
try {
// Try to call the method - this should throw
await weakTracker.recordModelUsage("any-provider", "any-model", "any-mode")
// If we get here, the test should fail
expect.fail("Expected an error to be thrown")
} catch (error) {
// Verify the error message
expect(error.message).to.equal("Unable to access extension context")
}
})
it("should append model usage to existing entries", async () => {
// Add an existing model usage entry
const existingTimestamp = 1617200000000
mockTaskMetadata.model_usage = [
{
ts: existingTimestamp,
model_id: "existing-model",
model_provider_id: "existing-provider",
mode: "plan",
},
]
// Test data for new entry
const apiProviderId = "anthropic"
const modelId = "claude-3-sonnet"
const mode = "act"
// Use a fake timer
const newTimestamp = 1617300000000
const clock = sandbox.useFakeTimers(newTimestamp)
try {
// Call the method being tested
await tracker.recordModelUsage(apiProviderId, modelId, mode)
// Verify saveTaskMetadata was called
expect(saveTaskMetadataStub.calledOnce).to.be.true
// Extract the saved metadata
const savedMetadata = saveTaskMetadataStub.firstCall.args[2]
// Verify model_usage array now has two entries
expect(savedMetadata.model_usage.length).to.equal(2)
// Verify the existing entry is preserved
expect(savedMetadata.model_usage[0]).to.deep.equal({
ts: existingTimestamp,
model_id: "existing-model",
model_provider_id: "existing-provider",
mode: "plan",
})
// Verify the new entry has correct data
expect(savedMetadata.model_usage[1]).to.deep.equal({
ts: newTimestamp,
model_id: modelId,
model_provider_id: apiProviderId,
mode: mode,
})
} finally {
clock.restore()
}
})
it("should handle multiple model usages in sequence", async () => {
// Test data for sequential calls
const usages = [
{ provider: "anthropic", model: "claude-3-opus", mode: "plan" },
{ provider: "openai", model: "gpt-4", mode: "act" },
{ provider: "anthropic", model: "claude-3-haiku", mode: "plan" },
]
// Use a fake timer that advances with each call
const startTime = 1617300000000
const clock = sandbox.useFakeTimers(startTime)
try {
// Record multiple model usages
for (let i = 0; i < usages.length; i++) {
const { provider, model, mode } = usages[i]
// Advance time by 1 second for each call
clock.tick(1000)
const expectedTime = startTime + (i + 1) * 1000
// Reset history between calls to check individual call behavior
getTaskMetadataStub.resetHistory()
saveTaskMetadataStub.resetHistory()
// Reset mock metadata for each iteration to avoid accumulation
mockTaskMetadata.model_usage = []
// Call the method
await tracker.recordModelUsage(provider, model, mode)
// Verify interaction with disk module
expect(getTaskMetadataStub.calledOnce).to.be.true
expect(saveTaskMetadataStub.calledOnce).to.be.true
// Get the saved metadata
const savedMetadata = saveTaskMetadataStub.firstCall.args[2]
// Since we reset the array for each call, we should always have 1 entry
expect(savedMetadata.model_usage.length).to.equal(1)
// Check the entry
const entry = savedMetadata.model_usage[0]
expect(entry.ts).to.equal(expectedTime)
expect(entry.model_id).to.equal(model)
expect(entry.model_provider_id).to.equal(provider)
expect(entry.mode).to.equal(mode)
}
} finally {
clock.restore()
}
})
})
@@ -0,0 +1,40 @@
import * as vscode from "vscode"
import { getTaskMetadata, saveTaskMetadata } from "../../storage/disk"
export class ModelContextTracker {
readonly taskId: string
private context: vscode.ExtensionContext
constructor(context: vscode.ExtensionContext, taskId: string) {
this.context = context
this.taskId = taskId
}
async recordModelUsage(apiProviderId: string, modelId: string, mode: string) {
const metadata = await getTaskMetadata(this.context, this.taskId)
if (!metadata.model_usage) {
metadata.model_usage = []
}
// check to see if the last entry is the same as the new one
const lastEntry = metadata.model_usage[metadata.model_usage.length - 1]
if (
lastEntry &&
lastEntry.model_id === modelId &&
lastEntry.model_provider_id === apiProviderId &&
lastEntry.mode === mode
) {
return
}
metadata.model_usage.push({
ts: Date.now(),
model_id: modelId,
model_provider_id: apiProviderId,
mode: mode,
})
await saveTaskMetadata(this.context, this.taskId, metadata)
}
}
@@ -0,0 +1,65 @@
import path from "path"
import { GlobalFileNames } from "../../../storage/disk"
import { fileExistsAtPath, isDirectory, readDirectory } from "../../../../utils/fs"
import { formatResponse } from "../../../prompts/responses"
import fs from "fs/promises"
export const getGlobalClineRules = async (globalClineRulesFilePath: string) => {
if (await fileExistsAtPath(globalClineRulesFilePath)) {
if (await isDirectory(globalClineRulesFilePath)) {
try {
const rulesFilePaths = await readDirectory(globalClineRulesFilePath)
const rulesFilesTotalContent = await getClineRulesFilesTotalContent(rulesFilePaths, globalClineRulesFilePath)
const clineRulesFileInstructions = formatResponse.clineRulesGlobalDirectoryInstructions(rulesFilesTotalContent)
return clineRulesFileInstructions
} catch {
console.error(`Failed to read .clinerules directory at ${globalClineRulesFilePath}`)
}
} else {
console.error(`${globalClineRulesFilePath} is not a directory`)
return undefined
}
}
return undefined
}
export const getLocalClineRules = async (cwd: string) => {
const clineRulesFilePath = path.resolve(cwd, GlobalFileNames.clineRules)
let clineRulesFileInstructions: string | undefined
if (await fileExistsAtPath(clineRulesFilePath)) {
if (await isDirectory(clineRulesFilePath)) {
try {
const rulesFilePaths = await readDirectory(path.join(cwd, GlobalFileNames.clineRules))
const rulesFilesTotalContent = await getClineRulesFilesTotalContent(rulesFilePaths, cwd)
clineRulesFileInstructions = formatResponse.clineRulesLocalDirectoryInstructions(cwd, rulesFilesTotalContent)
} catch {
console.error(`Failed to read .clinerules directory at ${clineRulesFilePath}`)
}
} else {
try {
const ruleFileContent = (await fs.readFile(clineRulesFilePath, "utf8")).trim()
if (ruleFileContent) {
clineRulesFileInstructions = formatResponse.clineRulesLocalFileInstructions(cwd, ruleFileContent)
}
} catch {
console.error(`Failed to read .clinerules file at ${clineRulesFilePath}`)
}
}
}
return clineRulesFileInstructions
}
const getClineRulesFilesTotalContent = async (rulesFilePaths: string[], basePath: string) => {
const ruleFilesTotalContent = await Promise.all(
rulesFilePaths.map(async (filePath) => {
const ruleFilePath = path.resolve(basePath, filePath)
const ruleFilePathRelative = path.relative(basePath, ruleFilePath)
return `${ruleFilePathRelative}\n` + (await fs.readFile(ruleFilePath, "utf8")).trim()
}),
).then((contents) => contents.join("\n\n"))
return ruleFilesTotalContent
}
+165 -122
View File
@@ -1,10 +1,9 @@
import { Anthropic } from "@anthropic-ai/sdk"
import axios from "axios"
import type { AxiosRequestConfig } from "axios"
import crypto from "crypto"
import { execa } from "execa"
import fs from "fs/promises"
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
import os from "os"
import pWaitFor from "p-wait-for"
import * as path from "path"
import * as vscode from "vscode"
@@ -17,10 +16,12 @@ import { selectImages } from "../../integrations/misc/process-images"
import { getTheme } from "../../integrations/theme/getTheme"
import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
import { ClineAccountService } from "../../services/account/ClineAccountService"
import { discoverChromeInstances } from "../../services/browser/BrowserDiscovery"
import { BrowserSession } from "../../services/browser/BrowserSession"
import { McpHub } from "../../services/mcp/McpHub"
import { searchWorkspaceFiles } from "../../services/search/file-search"
import { telemetryService } from "../../services/telemetry/TelemetryService"
import { ApiProvider, ModelInfo } from "../../shared/api"
import { findLast } from "../../shared/array"
import { ChatContent } from "../../shared/ChatContent"
import { ChatSettings } from "../../shared/ChatSettings"
import { ExtensionMessage, ExtensionState, Invoke, Platform } from "../../shared/ExtensionMessage"
@@ -30,9 +31,10 @@ import { TelemetrySetting } from "../../shared/TelemetrySetting"
import { ClineCheckpointRestore, WebviewMessage } from "../../shared/WebviewMessage"
import { fileExistsAtPath } from "../../utils/fs"
import { searchCommits } from "../../utils/git"
import { getWorkspacePath } from "../../utils/path"
import { getTotalTasksSize } from "../../utils/storage"
import { Task } from "../task"
import { openMention } from "../mentions"
import { ensureMcpServersDirectoryExists, ensureSettingsDirectoryExists, GlobalFileNames } from "../storage/disk"
import {
getAllExtensionState,
getGlobalState,
@@ -42,12 +44,7 @@ import {
updateApiConfiguration,
updateGlobalState,
} from "../storage/state"
import { WebviewProvider } from "../webview"
import { BrowserSession } from "../../services/browser/BrowserSession"
import { GlobalFileNames } from "../storage/disk"
import { discoverChromeInstances } from "../../services/browser/BrowserDiscovery"
import { searchWorkspaceFiles } from "../../services/search/file-search"
import { getWorkspacePath } from "../../utils/path"
import { Task } from "../task"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -56,25 +53,37 @@ https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/c
*/
export class Controller {
private postMessage: (message: ExtensionMessage) => Thenable<boolean> | undefined
private disposables: vscode.Disposable[] = []
private task?: Task
workspaceTracker?: WorkspaceTracker
mcpHub?: McpHub
accountService?: ClineAccountService
private latestAnnouncementId = "march-22-2025" // update to some unique identifier when we add a new announcement
private webviewProviderRef: WeakRef<WebviewProvider>
workspaceTracker: WorkspaceTracker
mcpHub: McpHub
accountService: ClineAccountService
private latestAnnouncementId = "april-11-2025" // update to some unique identifier when we add a new announcement
constructor(
readonly context: vscode.ExtensionContext,
private readonly outputChannel: vscode.OutputChannel,
webviewProvider: WebviewProvider,
postMessage: (message: ExtensionMessage) => Thenable<boolean> | undefined,
) {
this.outputChannel.appendLine("ClineProvider instantiated")
this.webviewProviderRef = new WeakRef(webviewProvider)
this.postMessage = postMessage
this.workspaceTracker = new WorkspaceTracker(this)
this.mcpHub = new McpHub(this)
this.accountService = new ClineAccountService(this)
this.workspaceTracker = new WorkspaceTracker((msg) => this.postMessageToWebview(msg))
this.mcpHub = new McpHub(
() => ensureMcpServersDirectoryExists(),
() => ensureSettingsDirectoryExists(this.context),
(msg) => this.postMessageToWebview(msg),
this.context.extension?.packageJSON?.version ?? "1.0.0",
)
this.accountService = new ClineAccountService(
(msg) => this.postMessageToWebview(msg),
async () => {
const { apiConfiguration } = await this.getStateToPostToWebview()
return apiConfiguration?.clineApiKey
},
)
// Clean up legacy checkpoints
cleanupLegacyCheckpoints(this.context.globalStorageUri.fsPath, this.outputChannel).catch((error) => {
@@ -97,11 +106,8 @@ export class Controller {
x.dispose()
}
}
this.workspaceTracker?.dispose()
this.workspaceTracker = undefined
this.mcpHub?.dispose()
this.mcpHub = undefined
this.accountService = undefined
this.workspaceTracker.dispose()
this.mcpHub.dispose()
this.outputChannel.appendLine("Disposed all disposables")
console.error("Controller disposed")
@@ -124,12 +130,19 @@ export class Controller {
await updateGlobalState(this.context, "userInfo", info)
}
async initClineWithTask(task?: string, images?: string[]) {
async initTask(task?: string, images?: string[], historyItem?: HistoryItem) {
await this.clearTask() // ensures that an existing task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one
const { apiConfiguration, customInstructions, autoApprovalSettings, browserSettings, chatSettings } =
await getAllExtensionState(this.context)
this.task = new Task(
this,
this.context,
this.mcpHub,
this.workspaceTracker,
(historyItem) => this.updateTaskHistory(historyItem),
() => this.postStateToWebview(),
(message) => this.postMessageToWebview(message),
(taskId) => this.reinitExistingTaskFromId(taskId),
() => this.cancelTask(),
apiConfiguration,
autoApprovalSettings,
browserSettings,
@@ -137,29 +150,20 @@ export class Controller {
customInstructions,
task,
images,
)
}
async initClineWithHistoryItem(historyItem: HistoryItem) {
await this.clearTask()
const { apiConfiguration, customInstructions, autoApprovalSettings, browserSettings, chatSettings } =
await getAllExtensionState(this.context)
this.task = new Task(
this,
apiConfiguration,
autoApprovalSettings,
browserSettings,
chatSettings,
customInstructions,
undefined,
undefined,
historyItem,
)
}
async reinitExistingTaskFromId(taskId: string) {
const history = await this.getTaskWithId(taskId)
if (history) {
await this.initTask(undefined, undefined, history.historyItem)
}
}
// Send any JSON serializable data to the react app
async postMessageToWebview(message: ExtensionMessage) {
await this.webviewProviderRef.deref()?.view?.webview.postMessage(message)
await this.postMessage(message)
}
/**
@@ -259,7 +263,7 @@ export class Controller {
// Could also do this in extension .ts
//this.postMessageToWebview({ type: "text", text: `Extension: ${Date.now()}` })
// initializing new instance of Cline will make sure that any agentically running promises in old instance don't affect our new task. this essentially creates a fresh slate for the new task
await this.initClineWithTask(message.text, message.images)
await this.initTask(message.text, message.images)
break
case "apiConfiguration":
if (message.apiConfiguration) {
@@ -487,6 +491,9 @@ export class Controller {
case "refreshOpenRouterModels":
await this.refreshOpenRouterModels()
break
case "refreshRequestyModels":
await this.refreshRequestyModels()
break
case "refreshOpenAiModels":
const { apiConfiguration } = await getAllExtensionState(this.context)
const openAiModels = await this.getOpenAiModels(apiConfiguration.openAiBaseUrl, apiConfiguration.openAiApiKey)
@@ -575,7 +582,7 @@ export class Controller {
break
}
case "showMcpView": {
await this.postMessageToWebview({ type: "action", action: "mcpButtonClicked" })
await this.postMessageToWebview({ type: "action", action: "mcpButtonClicked", tab: message.tab || undefined })
break
}
case "openMcpSettings": {
@@ -882,6 +889,27 @@ export class Controller {
}
break
}
case "toggleFavoriteModel": {
if (message.modelId) {
const { apiConfiguration } = await getAllExtensionState(this.context)
const favoritedModelIds = apiConfiguration.favoritedModelIds || []
// Toggle favorite status
const updatedFavorites = favoritedModelIds.includes(message.modelId)
? favoritedModelIds.filter((id) => id !== message.modelId)
: [...favoritedModelIds, message.modelId]
await updateGlobalState(this.context, "favoritedModelIds", updatedFavorites)
// Capture telemetry for model favorite toggle
const isFavorited = !favoritedModelIds.includes(message.modelId)
telemetryService.captureModelFavoritesUsage(message.modelId, isFavorited)
// Post state to webview without changing any other configuration
await this.postStateToWebview()
}
break
}
// Add more switch case statements here as more webview message commands
// are created within the webview context (i.e. inside media/main.js)
}
@@ -907,6 +935,7 @@ export class Controller {
previousModeModelInfo: newModelInfo,
previousModeVsCodeLmModelSelector: newVsCodeLmModelSelector,
previousModeThinkingBudgetTokens: newThinkingBudgetTokens,
previousModeReasoningEffort: newReasoningEffort,
planActSeparateModelsSetting,
} = await getAllExtensionState(this.context)
@@ -916,6 +945,7 @@ export class Controller {
// Save the last model used in this mode
await updateGlobalState(this.context, "previousModeApiProvider", apiConfiguration.apiProvider)
await updateGlobalState(this.context, "previousModeThinkingBudgetTokens", apiConfiguration.thinkingBudgetTokens)
await updateGlobalState(this.context, "previousModeReasoningEffort", apiConfiguration.reasoningEffort)
switch (apiConfiguration.apiProvider) {
case "anthropic":
case "bedrock":
@@ -925,6 +955,7 @@ export class Controller {
case "openai-native":
case "qwen":
case "deepseek":
case "xai":
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.apiModelId)
break
case "openrouter":
@@ -955,13 +986,21 @@ export class Controller {
break
case "requesty":
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.requestyModelId)
await updateGlobalState(this.context, "previousModeModelInfo", apiConfiguration.requestyModelInfo)
break
}
// Restore the model used in previous mode
if (newApiProvider || newModelId || newThinkingBudgetTokens !== undefined || newVsCodeLmModelSelector) {
if (
newApiProvider ||
newModelId ||
newThinkingBudgetTokens !== undefined ||
newReasoningEffort ||
newVsCodeLmModelSelector
) {
await updateGlobalState(this.context, "apiProvider", newApiProvider)
await updateGlobalState(this.context, "thinkingBudgetTokens", newThinkingBudgetTokens)
await updateGlobalState(this.context, "reasoningEffort", newReasoningEffort)
switch (newApiProvider) {
case "anthropic":
case "bedrock":
@@ -971,6 +1010,7 @@ export class Controller {
case "openai-native":
case "qwen":
case "deepseek":
case "xai":
await updateGlobalState(this.context, "apiModelId", newModelId)
break
case "openrouter":
@@ -996,6 +1036,7 @@ export class Controller {
break
case "requesty":
await updateGlobalState(this.context, "requestyModelId", newModelId)
await updateGlobalState(this.context, "requestyModelInfo", newModelInfo)
break
}
@@ -1050,7 +1091,7 @@ export class Controller {
// 'abandoned' will prevent this cline instance from affecting future cline instance gui. this may happen if its hanging on a streaming request
this.task.abandoned = true
}
await this.initClineWithHistoryItem(historyItem) // clears task again, so we need to abortTask manually above
await this.initTask(undefined, undefined, historyItem) // clears task again, so we need to abortTask manually above
// await this.postStateToWebview() // new Cline instance will post state when it's ready. having this here sent an empty messages array to webview leading to virtuoso having to reload the entire list
}
}
@@ -1063,61 +1104,6 @@ export class Controller {
}
}
// MCP
async getDocumentsPath(): Promise<string> {
if (process.platform === "win32") {
try {
const { stdout: docsPath } = await execa("powershell", [
"-NoProfile", // Ignore user's PowerShell profile(s)
"-Command",
"[System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::MyDocuments)",
])
const trimmedPath = docsPath.trim()
if (trimmedPath) {
return trimmedPath
}
} catch (err) {
console.error("Failed to retrieve Windows Documents path. Falling back to homedir/Documents.")
}
} else if (process.platform === "linux") {
try {
// First check if xdg-user-dir exists
await execa("which", ["xdg-user-dir"])
// If it exists, try to get XDG documents path
const { stdout } = await execa("xdg-user-dir", ["DOCUMENTS"])
const trimmedPath = stdout.trim()
if (trimmedPath) {
return trimmedPath
}
} catch {
// Log error but continue to fallback
console.error("Failed to retrieve XDG Documents path. Falling back to homedir/Documents.")
}
}
// Default fallback for all platforms
return path.join(os.homedir(), "Documents")
}
async ensureMcpServersDirectoryExists(): Promise<string> {
const userDocumentsPath = await this.getDocumentsPath()
const mcpServersDir = path.join(userDocumentsPath, "Cline", "MCP")
try {
await fs.mkdir(mcpServersDir, { recursive: true })
} catch (error) {
return "~/Documents/Cline/MCP" // in case creating a directory in documents fails for whatever reason (e.g. permissions) - this is fine since this path is only ever used in the system prompt
}
return mcpServersDir
}
async ensureSettingsDirectoryExists(): Promise<string> {
const settingsDir = path.join(this.context.globalStorageUri.fsPath, "settings")
await fs.mkdir(settingsDir, { recursive: true })
return settingsDir
}
// VSCode LM API
private async getVsCodeLmModels() {
@@ -1362,13 +1348,14 @@ export class Controller {
- Start by loading the MCP documentation.
- Use "${mcpDetails.mcpId}" as the server name in cline_mcp_settings.json.
- Create the directory for the new MCP server before starting installation.
- Make sure you read the user's existing cline_mcp_settings.json file before editing it with this new mcp, to not overwrite any existing servers.
- Use commands aligned with the user's shell and operating system best practices.
- The following README may contain instructions that conflict with the user's OS, in which case proceed thoughtfully.
- Once installed, demonstrate the server's capabilities by using one of its tools.
Here is the project's README to help you get started:\n\n${mcpDetails.readmeContent}\n${mcpDetails.llmsInstallationContent}`
// Initialize task and show chat view
await this.initClineWithTask(task)
await this.initTask(task)
await this.postMessageToWebview({
type: "action",
action: "chatButtonClicked",
@@ -1412,7 +1399,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
return []
}
const config: Record<string, any> = {}
const config: AxiosRequestConfig = {}
if (apiKey) {
config["headers"] = { Authorization: `Bearer ${apiKey}` }
}
@@ -1593,6 +1580,52 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
return models
}
async refreshRequestyModels() {
const parsePrice = (price: any) => {
if (price) {
return parseFloat(price) * 1_000_000
}
return undefined
}
let models: Record<string, ModelInfo> = {}
try {
const apiKey = await getSecret(this.context, "requestyApiKey")
const headers = {
Authorization: `Bearer ${apiKey}`,
}
const response = await axios.get("https://router.requesty.ai/v1/models", { headers })
if (response.data?.data) {
for (const model of response.data.data) {
const modelInfo: ModelInfo = {
maxTokens: model.max_output_tokens || undefined,
contextWindow: model.context_window,
supportsImages: model.supports_vision || undefined,
supportsComputerUse: model.supports_computer_use || undefined,
supportsPromptCache: model.supports_caching || undefined,
inputPrice: parsePrice(model.input_price),
outputPrice: parsePrice(model.output_price),
cacheWritesPrice: parsePrice(model.caching_price),
cacheReadsPrice: parsePrice(model.cached_price),
description: model.description,
}
models[model.id] = modelInfo
}
console.log("Requesty models fetched", models)
} else {
console.error("Invalid response from Requesty API")
}
} catch (error) {
console.error("Error fetching Requesty models:", error)
}
await this.postMessageToWebview({
type: "requestyModels",
requestyModels: models,
})
return models
}
// Context menus and code actions
getFileMentionFromPath(filePath: string) {
@@ -1656,9 +1689,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
const fileMention = this.getFileMentionFromPath(filePath)
const problemsString = this.convertDiagnosticsToProblemsString(diagnostics)
await this.initClineWithTask(
`Fix the following code in ${fileMention}\n\`\`\`\n${code}\n\`\`\`\n\nProblems:\n${problemsString}`,
)
await this.initTask(`Fix the following code in ${fileMention}\n\`\`\`\n${code}\n\`\`\`\n\nProblems:\n${problemsString}`)
console.log("fixWithCline", code, filePath, languageId, diagnostics, problemsString)
}
@@ -1698,6 +1729,8 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
taskDirPath: string
apiConversationHistoryFilePath: string
uiMessagesFilePath: string
contextHistoryFilePath: string
taskMetadataFilePath: string
apiConversationHistory: Anthropic.MessageParam[]
}> {
const history = ((await getGlobalState(this.context, "taskHistory")) as HistoryItem[] | undefined) || []
@@ -1706,6 +1739,8 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
const taskDirPath = path.join(this.context.globalStorageUri.fsPath, "tasks", id)
const apiConversationHistoryFilePath = path.join(taskDirPath, GlobalFileNames.apiConversationHistory)
const uiMessagesFilePath = path.join(taskDirPath, GlobalFileNames.uiMessages)
const contextHistoryFilePath = path.join(taskDirPath, GlobalFileNames.contextHistory)
const taskMetadataFilePath = path.join(taskDirPath, GlobalFileNames.taskMetadata)
const fileExists = await fileExistsAtPath(apiConversationHistoryFilePath)
if (fileExists) {
const apiConversationHistory = JSON.parse(await fs.readFile(apiConversationHistoryFilePath, "utf8"))
@@ -1714,6 +1749,8 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
taskDirPath,
apiConversationHistoryFilePath,
uiMessagesFilePath,
contextHistoryFilePath,
taskMetadataFilePath,
apiConversationHistory,
}
}
@@ -1728,7 +1765,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
if (id !== this.task?.taskId) {
// non-current task
const { historyItem } = await this.getTaskWithId(id)
await this.initClineWithHistoryItem(historyItem) // clears existing task
await this.initTask(undefined, undefined, historyItem) // clears existing task
}
await this.postMessageToWebview({
type: "action",
@@ -1785,22 +1822,28 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
console.debug("cleared task")
}
const { taskDirPath, apiConversationHistoryFilePath, uiMessagesFilePath } = await this.getTaskWithId(id)
const {
taskDirPath,
apiConversationHistoryFilePath,
uiMessagesFilePath,
contextHistoryFilePath,
taskMetadataFilePath,
} = await this.getTaskWithId(id)
const legacyMessagesFilePath = path.join(taskDirPath, "claude_messages.json")
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)
for (const filePath of [
apiConversationHistoryFilePath,
uiMessagesFilePath,
contextHistoryFilePath,
taskMetadataFilePath,
legacyMessagesFilePath,
]) {
const fileExists = await fileExistsAtPath(filePath)
if (fileExists) {
await fs.unlink(filePath)
}
}
await fs.rmdir(taskDirPath) // succeeds if the dir is empty
+1 -1
View File
@@ -10,7 +10,7 @@ import { diagnosticsToProblemsString } from "../../integrations/diagnostics"
import { getLatestTerminalOutput } from "../../integrations/terminal/get-latest-output"
import { getCommitInfo } from "../../utils/git"
import { getWorkingState } from "../../utils/git"
import { FileContextTracker } from "../context-tracking/FileContextTracker"
import { FileContextTracker } from "../context/context-tracking/FileContextTracker"
export function openMention(mention?: string): void {
if (!mention) {
+15 -8
View File
@@ -130,8 +130,8 @@ Otherwise, if you have not completed the task and do not need additional informa
cwd: string,
wasRecent: boolean | 0 | undefined,
responseText?: string,
) => {
return `[TASK RESUMPTION] ${
): [string, string] => {
const taskResumptionMessage = `[TASK RESUMPTION] ${
mode === "plan"
? `This task was interrupted ${agoText}. The conversation may have been incomplete. Be aware that the project state may have changed since then. The current working directory is now '${cwd.toPosix()}'.\n\nNote: If you previously attempted a tool use that the user did not provide a result for, you should assume the tool use was not successful. However you are in PLAN MODE, so rather than continuing the task, you must respond to the user's message.`
: `This task was interrupted ${agoText}. It may or may not be complete, so please reassess the task context. Be aware that the project state may have changed since then. The current working directory is now '${cwd.toPosix()}'. If the task has not been completed, retry the last step before interruption and proceed with completing the task.\n\nNote: If you previously attempted a tool use that the user did not provide a result for, you should assume the tool use was not successful and assess whether you should retry. If the last tool was a browser_action, the browser has been closed and you must launch a new browser if needed.`
@@ -139,13 +139,17 @@ Otherwise, if you have not completed the task and do not need additional informa
wasRecent
? "\n\nIMPORTANT: If the last tool use was a replace_in_file or write_to_file that was interrupted, the file was reverted back to its original state before the interrupted edit, and you do NOT need to re-read the file as you already have its up-to-date contents."
: ""
}${
}`
const userResponseMessage = `${
responseText
? `\n\n${mode === "plan" ? "New message to respond to with plan_mode_respond tool (be sure to provide your response in the <response> parameter)" : "New instructions for task continuation"}:\n<user_message>\n${responseText}\n</user_message>`
? `${mode === "plan" ? "New message to respond to with plan_mode_respond tool (be sure to provide your response in the <response> parameter)" : "New instructions for task continuation"}:\n<user_message>\n${responseText}\n</user_message>`
: mode === "plan"
? "(The user did not provide a new message. Consider asking them how they'd like you to proceed, or to switch to Act mode to continue with the task.)"
? "(The user did not provide a new message. Consider asking them how they'd like you to proceed, or suggest to them to switch to Act mode to continue with the task.)"
: ""
}`
return [taskResumptionMessage, userResponseMessage]
},
planModeInstructions: () => {
@@ -192,7 +196,7 @@ Otherwise, if you have not completed the task and do not need additional informa
`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${originalContent}\n</file_content>\n\n` +
`Now that you have the latest state of the file, try the operation again with fewer/more precise SEARCH blocks.\n(If you run into this error 3 times in a row, you may use the write_to_file tool as a fallback. Keep in mind, the write_to_file fallback is far from ideal, as this means you'll be re-writing the entire contents of the file just to make a few edits, which takes time and money. So let's bias towards using replace_in_file as effectively as possible)`,
`Now that you have the latest state of the file, try the operation again with fewer, more precise SEARCH blocks. For large files especially, it may be prudent to try to limit yourself to <5 SEARCH/REPLACE blocks at a time, then wait for the user to respond with the result of the operation before following up with another replace_in_file call to make additional edits.\n(If you run into this error 3 times in a row, you may use the write_to_file tool as a fallback.)`,
toolAlreadyUsed: (toolName: string) =>
`Tool [${toolName}] was not executed because a tool has already been used in this message. Only one tool may be used per message. You must assess the first tool's result before proceeding to use the next tool.`,
@@ -200,10 +204,13 @@ Otherwise, if you have not completed the task and do not need additional informa
clineIgnoreInstructions: (content: string) =>
`# .clineignore\n\n(The following is provided by a root-level .clineignore file where the user has specified files and directories that should not be accessed. When using list_files, you'll notice a ${LOCK_TEXT_SYMBOL} next to files that are blocked. Attempting to access the file's contents e.g. through read_file will result in an error.)\n\n${content}\n.clineignore`,
clineRulesDirectoryInstructions: (cwd: string, content: string) =>
clineRulesGlobalDirectoryInstructions: (content: string) =>
`# .clinerules/\n\nThe following is provided by a global .clinerules/ directory where the user has specified instructions:\n\n${content}`,
clineRulesLocalDirectoryInstructions: (cwd: string, content: string) =>
`# .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${content}`,
clineRulesFileInstructions: (cwd: string, content: string) =>
clineRulesLocalFileInstructions: (cwd: string, content: string) =>
`# .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${content}`,
}
+42 -10
View File
@@ -233,17 +233,27 @@ Your final result description here
<command>Command to demonstrate result (optional)</command>
</attempt_completion>
## new_task
Description: Request to create a new task with preloaded context. The user will be presented with a preview of the context and can choose to create a new task or keep chatting in the current conversation. The user may choose to start a new task at any point.
Parameters:
- context: (required) The context to preload the new task with. This should include:
* Comprehensively explain what has been accomplished in the current task - mention specific file names that are relevant
* The specific next steps or focus for the new task - mention specific file names that are relevant
* Any critical information needed to continue the work
* Clear indication of how this new task relates to the overall workflow
* This should be akin to a long handoff file, enough for a totally new developer to be able to pick up where you left off and know exactly what to do next and which files to look at.
Usage:
<new_task>
<context>context to preload new task with</context>
</new_task>
## plan_mode_respond
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. (You MUST use the response parameter, do not simply place the response text directly within <plan_mode_respond> tags.)
- 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_respond>
<response>Your response here</response>
<options>
Array of options here (optional), e.g. ["Option 1", "Option 2", "Option 3"]
</options>
</plan_mode_respond>
## load_mcp_documentation
@@ -284,7 +294,25 @@ Usage:
</content>
</write_to_file>
## Example 3: Requesting to make targeted edits to a file
## Example 3: Creating a new task
<new_task>
<context>
Authentication System Implementation:
- We've implemented the basic user model with email/password
- Password hashing is working with bcrypt
- Login endpoint is functional with proper validation
- JWT token generation is implemented
Next Steps:
- Implement refresh token functionality
- Add token validation middleware
- Create password reset flow
- Implement role-based access control
</context>
</new_task>
## Example 4: Requesting to make targeted edits to a file
<replace_in_file>
<path>src/components/App.tsx</path>
@@ -319,7 +347,7 @@ return (
</diff>
</replace_in_file>
## Example 4: Requesting to use an MCP tool
## Example 5: Requesting to use an MCP tool
<use_mcp_tool>
<server_name>weather-server</server_name>
@@ -332,7 +360,7 @@ return (
</arguments>
</use_mcp_tool>
## Example 5: Another example of using an MCP tool (where the server name is a unique identifier such as a URL)
## Example 6: Another example of using an MCP tool (where the server name is a unique identifier such as a URL)
<use_mcp_tool>
<server_name>github.com/modelcontextprotocol/servers/tree/main/src/github</server_name>
@@ -591,7 +619,8 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
export function addUserInstructions(
settingsCustomInstructions?: string,
clineRulesFileInstructions?: string,
globalClineRulesFileInstructions?: string,
localClineRulesFileInstructions?: string,
clineIgnoreInstructions?: string,
preferredLanguageInstructions?: string,
) {
@@ -602,8 +631,11 @@ export function addUserInstructions(
if (settingsCustomInstructions) {
customInstructions += settingsCustomInstructions + "\n\n"
}
if (clineRulesFileInstructions) {
customInstructions += clineRulesFileInstructions + "\n\n"
if (globalClineRulesFileInstructions) {
customInstructions += globalClineRulesFileInstructions + "\n\n"
}
if (localClineRulesFileInstructions) {
customInstructions += localClineRulesFileInstructions + "\n\n"
}
if (clineIgnoreInstructions) {
customInstructions += clineIgnoreInstructions
+68 -14
View File
@@ -4,19 +4,9 @@ import fs from "fs/promises"
import { Anthropic } from "@anthropic-ai/sdk"
import { fileExistsAtPath } from "../../utils/fs"
import { ClineMessage } from "../../shared/ExtensionMessage"
export interface FileMetadataEntry {
path: string
record_state: "active" | "stale"
record_source: "read_tool" | "user_edited" | "cline_edited" | "file_mentioned"
cline_read_date: number | null
cline_edit_date: number | null
user_edit_date?: number | null
}
export interface TaskMetadata {
files_in_context: FileMetadataEntry[]
}
import { TaskMetadata } from "../context/context-tracking/ContextTrackerTypes"
import os from "os"
import { execa } from "execa"
export const GlobalFileNames = {
apiConversationHistory: "api_conversation_history.json",
@@ -28,6 +18,42 @@ export const GlobalFileNames = {
taskMetadata: "task_metadata.json",
}
export async function getDocumentsPath(): Promise<string> {
if (process.platform === "win32") {
try {
const { stdout: docsPath } = await execa("powershell", [
"-NoProfile", // Ignore user's PowerShell profile(s)
"-Command",
"[System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::MyDocuments)",
])
const trimmedPath = docsPath.trim()
if (trimmedPath) {
return trimmedPath
}
} catch (err) {
console.error("Failed to retrieve Windows Documents path. Falling back to homedir/Documents.")
}
} else if (process.platform === "linux") {
try {
// First check if xdg-user-dir exists
await execa("which", ["xdg-user-dir"])
// If it exists, try to get XDG documents path
const { stdout } = await execa("xdg-user-dir", ["DOCUMENTS"])
const trimmedPath = stdout.trim()
if (trimmedPath) {
return trimmedPath
}
} catch {
// Log error but continue to fallback
console.error("Failed to retrieve XDG Documents path. Falling back to homedir/Documents.")
}
}
// Default fallback for all platforms
return path.join(os.homedir(), "Documents")
}
export async function ensureTaskDirectoryExists(context: vscode.ExtensionContext, taskId: string): Promise<string> {
const globalStoragePath = context.globalStorageUri.fsPath
const taskDir = path.join(globalStoragePath, "tasks", taskId)
@@ -35,6 +61,34 @@ export async function ensureTaskDirectoryExists(context: vscode.ExtensionContext
return taskDir
}
export async function ensureRulesDirectoryExists(): Promise<string> {
const userDocumentsPath = await getDocumentsPath()
const clineRulesDir = path.join(userDocumentsPath, "Cline", "Rules")
try {
await fs.mkdir(clineRulesDir, { recursive: true })
} catch (error) {
return path.join(os.homedir(), "Documents", "Cline", "Rules") // in case creating a directory in documents fails for whatever reason (e.g. permissions) - this is fine because we will fail gracefully with a path that does not exist
}
return clineRulesDir
}
export async function ensureMcpServersDirectoryExists(): Promise<string> {
const userDocumentsPath = await getDocumentsPath()
const mcpServersDir = path.join(userDocumentsPath, "Cline", "MCP")
try {
await fs.mkdir(mcpServersDir, { recursive: true })
} catch (error) {
return "~/Documents/Cline/MCP" // in case creating a directory in documents fails for whatever reason (e.g. permissions) - this is fine since this path is only ever used in the system prompt
}
return mcpServersDir
}
export async function ensureSettingsDirectoryExists(context: vscode.ExtensionContext): Promise<string> {
const settingsDir = path.join(context.globalStorageUri.fsPath, "settings")
await fs.mkdir(settingsDir, { recursive: true })
return settingsDir
}
export async function getSavedApiConversationHistory(
context: vscode.ExtensionContext,
taskId: string,
@@ -96,7 +150,7 @@ export async function getTaskMetadata(context: vscode.ExtensionContext, taskId:
} catch (error) {
console.error("Failed to read task metadata:", error)
}
return { files_in_context: [] }
return { files_in_context: [], model_usage: [] }
}
export async function saveTaskMetadata(context: vscode.ExtensionContext, taskId: string, metadata: TaskMetadata) {
+4
View File
@@ -54,6 +54,7 @@ export type GlobalStateKey =
| "previousModeApiProvider"
| "previousModeModelId"
| "previousModeThinkingBudgetTokens"
| "previousModeReasoningEffort"
| "previousModeVsCodeLmModelSelector"
| "previousModeModelInfo"
| "liteLlmBaseUrl"
@@ -61,9 +62,12 @@ export type GlobalStateKey =
| "liteLlmUsePromptCache"
| "qwenApiLine"
| "requestyModelId"
| "requestyModelInfo"
| "togetherModelId"
| "mcpMarketplaceCatalog"
| "telemetrySetting"
| "asksageApiUrl"
| "thinkingBudgetTokens"
| "reasoningEffort"
| "planActSeparateModelsSetting"
| "favoritedModelIds"
+18
View File
@@ -83,6 +83,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
deepSeekApiKey,
requestyApiKey,
requestyModelId,
requestyModelInfo,
togetherApiKey,
togetherModelId,
qwenApiKey,
@@ -108,6 +109,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
previousModeModelInfo,
previousModeVsCodeLmModelSelector,
previousModeThinkingBudgetTokens,
previousModeReasoningEffort,
qwenApiLine,
liteLlmApiKey,
telemetrySetting,
@@ -115,8 +117,10 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
asksageApiUrl,
xaiApiKey,
thinkingBudgetTokens,
reasoningEffort,
sambanovaApiKey,
planActSeparateModelsSettingRaw,
favoritedModelIds,
] = await Promise.all([
getGlobalState(context, "apiProvider") as Promise<ApiProvider | undefined>,
getGlobalState(context, "apiModelId") as Promise<string | undefined>,
@@ -149,6 +153,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
getSecret(context, "deepSeekApiKey") as Promise<string | undefined>,
getSecret(context, "requestyApiKey") as Promise<string | undefined>,
getGlobalState(context, "requestyModelId") as Promise<string | undefined>,
getGlobalState(context, "requestyModelInfo") as Promise<ModelInfo | undefined>,
getSecret(context, "togetherApiKey") as Promise<string | undefined>,
getGlobalState(context, "togetherModelId") as Promise<string | undefined>,
getSecret(context, "qwenApiKey") as Promise<string | undefined>,
@@ -174,6 +179,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
getGlobalState(context, "previousModeModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "previousModeVsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
getGlobalState(context, "previousModeThinkingBudgetTokens") as Promise<number | undefined>,
getGlobalState(context, "previousModeReasoningEffort") as Promise<string | undefined>,
getGlobalState(context, "qwenApiLine") as Promise<string | undefined>,
getSecret(context, "liteLlmApiKey") as Promise<string | undefined>,
getGlobalState(context, "telemetrySetting") as Promise<TelemetrySetting | undefined>,
@@ -181,8 +187,10 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
getGlobalState(context, "asksageApiUrl") as Promise<string | undefined>,
getSecret(context, "xaiApiKey") as Promise<string | undefined>,
getGlobalState(context, "thinkingBudgetTokens") as Promise<number | undefined>,
getGlobalState(context, "reasoningEffort") as Promise<string | undefined>,
getSecret(context, "sambanovaApiKey") as Promise<string | undefined>,
getGlobalState(context, "planActSeparateModelsSetting") as Promise<boolean | undefined>,
getGlobalState(context, "favoritedModelIds") as Promise<string[] | undefined>,
])
let apiProvider: ApiProvider
@@ -254,6 +262,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
deepSeekApiKey,
requestyApiKey,
requestyModelId,
requestyModelInfo,
togetherApiKey,
togetherModelId,
qwenApiKey,
@@ -267,6 +276,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
vsCodeLmModelSelector,
o3MiniReasoningEffort,
thinkingBudgetTokens,
reasoningEffort,
liteLlmBaseUrl,
liteLlmModelId,
liteLlmApiKey,
@@ -275,6 +285,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
asksageApiUrl,
xaiApiKey,
sambanovaApiKey,
favoritedModelIds,
},
lastShownAnnouncementId,
customInstructions,
@@ -288,6 +299,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
previousModeModelInfo,
previousModeVsCodeLmModelSelector,
previousModeThinkingBudgetTokens,
previousModeReasoningEffort,
mcpMarketplaceEnabled,
telemetrySetting: telemetrySetting || "unset",
planActSeparateModelsSetting,
@@ -326,6 +338,7 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
deepSeekApiKey,
requestyApiKey,
requestyModelId,
requestyModelInfo,
togetherApiKey,
togetherModelId,
qwenApiKey,
@@ -345,8 +358,10 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
asksageApiUrl,
xaiApiKey,
thinkingBudgetTokens,
reasoningEffort,
clineApiKey,
sambanovaApiKey,
favoritedModelIds,
} = apiConfiguration
await updateGlobalState(context, "apiProvider", apiProvider)
await updateGlobalState(context, "apiModelId", apiModelId)
@@ -393,12 +408,15 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
await updateGlobalState(context, "liteLlmUsePromptCache", liteLlmUsePromptCache)
await updateGlobalState(context, "qwenApiLine", qwenApiLine)
await updateGlobalState(context, "requestyModelId", requestyModelId)
await updateGlobalState(context, "requestyModelInfo", requestyModelInfo)
await updateGlobalState(context, "togetherModelId", togetherModelId)
await storeSecret(context, "asksageApiKey", asksageApiKey)
await updateGlobalState(context, "asksageApiUrl", asksageApiUrl)
await updateGlobalState(context, "thinkingBudgetTokens", thinkingBudgetTokens)
await updateGlobalState(context, "reasoningEffort", reasoningEffort)
await storeSecret(context, "clineApiKey", clineApiKey)
await storeSecret(context, "sambanovaApiKey", sambanovaApiKey)
await updateGlobalState(context, "favoritedModelIds", favoritedModelIds)
}
export async function resetExtensionState(context: vscode.ExtensionContext) {
+273 -155
View File
@@ -1,6 +1,5 @@
import { Anthropic } from "@anthropic-ai/sdk"
import cloneDeep from "clone-deep"
import fs from "fs/promises"
import getFolderSize from "get-folder-size"
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
import os from "os"
@@ -48,37 +47,43 @@ import {
ClineSayBrowserAction,
ClineSayTool,
COMPLETION_RESULT_CHANGES_FLAG,
ExtensionMessage,
} from "../../shared/ExtensionMessage"
import { getApiMetrics } from "../../shared/getApiMetrics"
import { HistoryItem } from "../../shared/HistoryItem"
import { DEFAULT_LANGUAGE_SETTINGS, getLanguageKey, LanguageDisplay } from "../../shared/Languages"
import { ClineAskResponse, ClineCheckpointRestore } from "../../shared/WebviewMessage"
import { calculateApiCostAnthropic } from "../../utils/cost"
import { fileExistsAtPath, isDirectory } from "../../utils/fs"
import { arePathsEqual, getReadablePath } from "../../utils/path"
import { fileExistsAtPath } from "../../utils/fs"
import { arePathsEqual, getReadablePath, isLocatedInWorkspace } from "../../utils/path"
import { fixModelHtmlEscaping, removeInvalidChars } from "../../utils/string"
import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from ".././assistant-message"
import { constructNewFileContent } from ".././assistant-message/diff"
import { ContextManager } from ".././context-management/ContextManager"
import { ClineIgnoreController } from ".././ignore/ClineIgnoreController"
import { parseMentions } from ".././mentions"
import { formatResponse } from ".././prompts/responses"
import { addUserInstructions, SYSTEM_PROMPT } from ".././prompts/system"
import { FileContextTracker } from "../context-tracking/FileContextTracker"
import { getContextWindowInfo } from "../context/context-management/context-window-utils"
import { FileContextTracker } from "../context/context-tracking/FileContextTracker"
import { ModelContextTracker } from "../context/context-tracking/ModelContextTracker"
import {
checkIsAnthropicContextWindowError,
checkIsOpenRouterContextWindowError,
} from "../context-management/context-error-handling"
import { Controller } from "../controller"
} from "../context/context-management/context-error-handling"
import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
import { McpHub } from "../../services/mcp/McpHub"
import { ContextManager } from "../context/context-management/ContextManager"
import { loadMcpDocumentation } from "../prompts/loadMcpDocumentation"
import {
ensureRulesDirectoryExists,
ensureTaskDirectoryExists,
getSavedApiConversationHistory,
getSavedClineMessages,
saveApiConversationHistory,
saveClineMessages,
GlobalFileNames,
} from "../storage/disk"
import { loadMcpDocumentation } from "../prompts/loadMcpDocumentation"
import { getGlobalClineRules, getLocalClineRules } from "../context/instructions/user-instructions/cline-rules"
import { getGlobalState } from "../storage/state"
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
@@ -86,8 +91,17 @@ type ToolResponse = string | Array<Anthropic.TextBlockParam | Anthropic.ImageBlo
type UserContent = Array<Anthropic.ContentBlockParam>
export class Task {
// dependencies
private context: vscode.ExtensionContext
private mcpHub: McpHub
private workspaceTracker: WorkspaceTracker
private updateTaskHistory: (historyItem: HistoryItem) => Promise<HistoryItem[]>
private postStateToWebview: () => Promise<void>
private postMessageToWebview: (message: ExtensionMessage) => Promise<void>
private reinitExistingTaskFromId: (taskId: string) => Promise<void>
private cancelTask: () => Promise<void>
readonly taskId: string
readonly apiProvider?: string
api: ApiHandler
private terminalManager: TerminalManager
private urlContentFetcher: UrlContentFetcher
@@ -107,7 +121,6 @@ export class Task {
private lastMessageTs?: number
private consecutiveAutoApprovedRequestsCount: number = 0
private consecutiveMistakeCount: number = 0
private controllerRef: WeakRef<Controller>
private abort: boolean = false
didFinishAbortingStream = false
abandoned = false
@@ -119,8 +132,9 @@ export class Task {
isAwaitingPlanResponse = false
didRespondToPlanAskBySwitchingMode = false
// File tracking
// Metadata tracking
private fileContextTracker: FileContextTracker
private modelContextTracker: ModelContextTracker
// streaming
isWaitingForFirstChunk = false
@@ -137,7 +151,14 @@ export class Task {
private didAutomaticallyRetryFailedApiRequest = false
constructor(
controller: Controller,
context: vscode.ExtensionContext,
mcpHub: McpHub,
workspaceTracker: WorkspaceTracker,
updateTaskHistory: (historyItem: HistoryItem) => Promise<HistoryItem[]>,
postStateToWebview: () => Promise<void>,
postMessageToWebview: (message: ExtensionMessage) => Promise<void>,
reinitExistingTaskFromId: (taskId: string) => Promise<void>,
cancelTask: () => Promise<void>,
apiConfiguration: ApiConfiguration,
autoApprovalSettings: AutoApprovalSettings,
browserSettings: BrowserSettings,
@@ -147,15 +168,21 @@ export class Task {
images?: string[],
historyItem?: HistoryItem,
) {
this.context = context
this.mcpHub = mcpHub
this.workspaceTracker = workspaceTracker
this.updateTaskHistory = updateTaskHistory
this.postStateToWebview = postStateToWebview
this.postMessageToWebview = postMessageToWebview
this.reinitExistingTaskFromId = reinitExistingTaskFromId
this.cancelTask = cancelTask
this.clineIgnoreController = new ClineIgnoreController(cwd)
this.clineIgnoreController.initialize().catch((error) => {
console.error("Failed to initialize ClineIgnoreController:", error)
})
this.controllerRef = new WeakRef(controller)
this.apiProvider = apiConfiguration.apiProvider
this.terminalManager = new TerminalManager()
this.urlContentFetcher = new UrlContentFetcher(controller.context)
this.browserSession = new BrowserSession(controller.context, browserSettings)
this.urlContentFetcher = new UrlContentFetcher(context)
this.browserSession = new BrowserSession(context, browserSettings)
this.contextManager = new ContextManager()
this.diffViewProvider = new DiffViewProvider(cwd)
this.customInstructions = customInstructions
@@ -174,8 +201,8 @@ export class Task {
}
// Initialize file context tracker
this.fileContextTracker = new FileContextTracker(controller, this.taskId)
this.fileContextTracker = new FileContextTracker(context, this.taskId)
this.modelContextTracker = new ModelContextTracker(context, this.taskId)
// Now that taskId is initialized, we can build the API handler
this.api = buildApiHandler({
...apiConfiguration,
@@ -192,19 +219,20 @@ export class Task {
this.startTask(task, images)
}
// initialize telemetry
if (historyItem) {
// Open task from history
telemetryService.captureTaskRestarted(this.taskId, this.apiProvider)
telemetryService.captureTaskRestarted(this.taskId, apiConfiguration.apiProvider)
} else {
// New task started
telemetryService.captureTaskCreated(this.taskId, this.apiProvider)
telemetryService.captureTaskCreated(this.taskId, apiConfiguration.apiProvider)
}
}
// While a task is ref'd by a controller, it will always have access to the extension context
// This error is thrown if the controller derefs the task after e.g., aborting the task
private getContext(): vscode.ExtensionContext {
const context = this.controllerRef.deref()?.context
const context = this.context
if (!context) {
throw new Error("Unable to access extension context")
}
@@ -256,7 +284,7 @@ export class Task {
} catch (error) {
console.error("Failed to get task directory size:", taskDir, error)
}
await this.controllerRef.deref()?.updateTaskHistory({
await this.updateTaskHistory({
id: this.taskId,
ts: lastRelevantMessage.ts,
task: taskMessage.text ?? "",
@@ -291,15 +319,12 @@ export class Task {
case "workspace":
if (!this.checkpointTracker && !this.checkpointTrackerErrorMessage) {
try {
this.checkpointTracker = await CheckpointTracker.create(
this.taskId,
this.controllerRef.deref()?.context.globalStorageUri.fsPath,
)
this.checkpointTracker = await CheckpointTracker.create(this.taskId, this.context.globalStorageUri.fsPath)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error"
console.error("Failed to initialize checkpoint tracker:", errorMessage)
this.checkpointTrackerErrorMessage = errorMessage
await this.controllerRef.deref()?.postStateToWebview()
await this.postStateToWebview()
vscode.window.showErrorMessage(errorMessage)
didWorkspaceRestoreFail = true
}
@@ -381,17 +406,17 @@ export class Task {
await this.saveClineMessagesAndUpdateHistory()
await this.controllerRef.deref()?.postMessageToWebview({ type: "relinquishControl" })
await this.postMessageToWebview({ type: "relinquishControl" })
this.controllerRef.deref()?.cancelTask() // the task is already cancelled by the provider beforehand, but we need to re-init to get the updated messages
this.cancelTask() // the task is already cancelled by the provider beforehand, but we need to re-init to get the updated messages
} else {
await this.controllerRef.deref()?.postMessageToWebview({ type: "relinquishControl" })
await this.postMessageToWebview({ type: "relinquishControl" })
}
}
async presentMultifileDiff(messageTs: number, seeNewChangesSinceLastTaskCompletion: boolean) {
const relinquishButton = () => {
this.controllerRef.deref()?.postMessageToWebview({ type: "relinquishControl" })
this.postMessageToWebview({ type: "relinquishControl" })
}
console.log("presentMultifileDiff", messageTs)
@@ -412,15 +437,12 @@ export class Task {
// 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 && !this.checkpointTrackerErrorMessage) {
try {
this.checkpointTracker = await CheckpointTracker.create(
this.taskId,
this.controllerRef.deref()?.context.globalStorageUri.fsPath,
)
this.checkpointTracker = await CheckpointTracker.create(this.taskId, this.context.globalStorageUri.fsPath)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error"
console.error("Failed to initialize checkpoint tracker:", errorMessage)
this.checkpointTrackerErrorMessage = errorMessage
await this.controllerRef.deref()?.postStateToWebview()
await this.postStateToWebview()
vscode.window.showErrorMessage(errorMessage)
relinquishButton()
return
@@ -527,10 +549,7 @@ export class Task {
if (!this.checkpointTracker && !this.checkpointTrackerErrorMessage) {
try {
this.checkpointTracker = await CheckpointTracker.create(
this.taskId,
this.controllerRef.deref()?.context.globalStorageUri.fsPath,
)
this.checkpointTracker = await CheckpointTracker.create(this.taskId, this.context.globalStorageUri.fsPath)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error"
console.error("Failed to initialize checkpoint tracker:", errorMessage)
@@ -601,8 +620,8 @@ export class Task {
lastMessage.partial = partial
// todo be more efficient about saving and posting only new data or one whole message at a time so ignore partial for saves, and only post parts of partial message instead of whole array in new listener
// await this.saveClineMessagesAndUpdateHistory()
// await this.controllerRef.deref()?.postStateToWebview()
await this.controllerRef.deref()?.postMessageToWebview({
// await this.postStateToWebview()
await this.postMessageToWebview({
type: "partialMessage",
partialMessage: lastMessage,
})
@@ -621,7 +640,7 @@ export class Task {
text,
partial,
})
await this.controllerRef.deref()?.postStateToWebview()
await this.postStateToWebview()
throw new Error("Current ask promise was ignored 2")
}
} else {
@@ -644,8 +663,8 @@ export class Task {
lastMessage.text = text
lastMessage.partial = false
await this.saveClineMessagesAndUpdateHistory()
// await this.controllerRef.deref()?.postStateToWebview()
await this.controllerRef.deref()?.postMessageToWebview({
// await this.postStateToWebview()
await this.postMessageToWebview({
type: "partialMessage",
partialMessage: lastMessage,
})
@@ -662,7 +681,7 @@ export class Task {
ask: type,
text,
})
await this.controllerRef.deref()?.postStateToWebview()
await this.postStateToWebview()
}
}
} else {
@@ -679,7 +698,7 @@ export class Task {
ask: type,
text,
})
await this.controllerRef.deref()?.postStateToWebview()
await this.postStateToWebview()
}
await pWaitFor(() => this.askResponse !== undefined || this.lastMessageTs !== askTs, { interval: 100 })
@@ -718,7 +737,7 @@ export class Task {
lastMessage.text = text
lastMessage.images = images
lastMessage.partial = partial
await this.controllerRef.deref()?.postMessageToWebview({
await this.postMessageToWebview({
type: "partialMessage",
partialMessage: lastMessage,
})
@@ -734,7 +753,7 @@ export class Task {
images,
partial,
})
await this.controllerRef.deref()?.postStateToWebview()
await this.postStateToWebview()
}
} else {
// partial=false means its a complete version of a previously partial message
@@ -748,8 +767,8 @@ export class Task {
// instead of streaming partialMessage events, we do a save and post like normal to persist to disk
await this.saveClineMessagesAndUpdateHistory()
// await this.controllerRef.deref()?.postStateToWebview()
await this.controllerRef.deref()?.postMessageToWebview({
// await this.postStateToWebview()
await this.postMessageToWebview({
type: "partialMessage",
partialMessage: lastMessage,
}) // more performant than an entire postStateToWebview
@@ -764,7 +783,7 @@ export class Task {
text,
images,
})
await this.controllerRef.deref()?.postStateToWebview()
await this.postStateToWebview()
}
}
} else {
@@ -778,7 +797,7 @@ export class Task {
text,
images,
})
await this.controllerRef.deref()?.postStateToWebview()
await this.postStateToWebview()
}
}
@@ -797,7 +816,7 @@ export class Task {
if (lastMessage?.partial && lastMessage.type === type && (lastMessage.ask === askOrSay || lastMessage.say === askOrSay)) {
this.clineMessages.pop()
await this.saveClineMessagesAndUpdateHistory()
await this.controllerRef.deref()?.postStateToWebview()
await this.postStateToWebview()
}
}
@@ -809,7 +828,7 @@ export class Task {
this.clineMessages = []
this.apiConversationHistory = []
await this.controllerRef.deref()?.postStateToWebview()
await this.postStateToWebview()
await this.say("text", task, images)
@@ -942,16 +961,27 @@ export class Task {
const wasRecent = lastClineMessage?.ts && Date.now() - lastClineMessage.ts < 30_000
newUserContent.push({
type: "text",
text: formatResponse.taskResumption(
this.chatSettings?.mode === "plan" ? "plan" : "act",
agoText,
cwd,
wasRecent,
responseText,
),
})
const [taskResumptionMessage, userResponseMessage] = formatResponse.taskResumption(
this.chatSettings?.mode === "plan" ? "plan" : "act",
agoText,
cwd,
wasRecent,
responseText,
)
if (taskResumptionMessage !== "") {
newUserContent.push({
type: "text",
text: taskResumptionMessage,
})
}
if (userResponseMessage !== "") {
newUserContent.push({
type: "text",
text: userResponseMessage,
})
}
if (responseImages && responseImages.length > 0) {
newUserContent.push(...formatResponse.imageBlocks(responseImages))
@@ -1013,6 +1043,12 @@ export class Task {
})
if (!isAttemptCompletionMessage) {
// ensure we aren't creating a duplicate checkpoint
const lastMessage = this.clineMessages.at(-1)
if (lastMessage?.say === "checkpoint_created") {
return
}
// For non-attempt completion we just say checkpoints
await this.say("checkpoint_created")
this.checkpointTracker?.commit().then(async (commitHash) => {
@@ -1155,7 +1191,7 @@ export class Task {
}
// Check if the tool should be auto-approved based on the settings
// Returns bool for most tools, tuple for execute_command (and future nested auto appoved settings)
// Returns bool for most tools, and tuple for tools with nested settings
shouldAutoApproveTool(toolName: ToolUseName): boolean | [boolean, boolean] {
if (this.autoApprovalSettings.enabled) {
switch (toolName) {
@@ -1163,14 +1199,20 @@ export class Task {
case "list_files":
case "list_code_definition_names":
case "search_files":
return this.autoApprovalSettings.actions.readFiles
return [
this.autoApprovalSettings.actions.readFiles,
this.autoApprovalSettings.actions.readFilesExternally ?? false,
]
case "write_to_file":
case "replace_in_file":
return this.autoApprovalSettings.actions.editFiles
return [
this.autoApprovalSettings.actions.editFiles,
this.autoApprovalSettings.actions.editFilesExternally ?? false,
]
case "execute_command":
return [
this.autoApprovalSettings.actions.executeSafeCommands,
this.autoApprovalSettings.actions.executeAllCommands,
this.autoApprovalSettings.actions.executeSafeCommands ?? false,
this.autoApprovalSettings.actions.executeAllCommands ?? false,
]
case "browser_action":
return this.autoApprovalSettings.actions.useBrowser
@@ -1182,6 +1224,32 @@ export class Task {
return false
}
// Check if the tool should be auto-approved based on the settings
// and the path of the action. Returns true if the tool should be auto-approved
// based on the user's settings and the path of the action.
shouldAutoApproveToolWithPath(blockname: ToolUseName, autoApproveActionpath: string | undefined): boolean {
let isLocalRead: boolean = false
if (autoApproveActionpath) {
const absolutePath = path.resolve(cwd, autoApproveActionpath)
isLocalRead = absolutePath.startsWith(cwd)
} else {
// If we do not get a path for some reason, default to a (safer) false return
isLocalRead = false
}
// Get auto-approve settings for local and external edits
const autoApproveResult = this.shouldAutoApproveTool(blockname)
const [autoApproveLocal, autoApproveExternal] = Array.isArray(autoApproveResult)
? autoApproveResult
: [autoApproveResult, false]
if ((isLocalRead && autoApproveLocal) || (!isLocalRead && autoApproveLocal && autoApproveExternal)) {
return true
} else {
return false
}
}
private formatErrorWithStatusCode(error: any): string {
const statusCode = error.status || error.statusCode || (error.response && error.response.status)
const message = error.message ?? JSON.stringify(serializeError(error), null, 2)
@@ -1192,21 +1260,16 @@ export class Task {
async *attemptApiRequest(previousApiReqIndex: number): ApiStream {
// Wait for MCP servers to be connected before generating system prompt
await pWaitFor(() => this.controllerRef.deref()?.mcpHub?.isConnecting !== true, { timeout: 10_000 }).catch(() => {
await pWaitFor(() => this.mcpHub.isConnecting !== true, { timeout: 10_000 }).catch(() => {
console.error("MCP servers failed to connect in time")
})
const mcpHub = this.controllerRef.deref()?.mcpHub
if (!mcpHub) {
throw new Error("MCP hub not available")
}
const disableBrowserTool = vscode.workspace.getConfiguration("cline").get<boolean>("disableBrowserTool") ?? false
const modelSupportsComputerUse = this.api.getModel().info.supportsComputerUse ?? false
const supportsComputerUse = modelSupportsComputerUse && !disableBrowserTool // only enable computer use if the model supports it and the user hasn't disabled it
let systemPrompt = await SYSTEM_PROMPT(cwd, supportsComputerUse, mcpHub, this.browserSettings)
let systemPrompt = await SYSTEM_PROMPT(cwd, supportsComputerUse, this.mcpHub, this.browserSettings)
let settingsCustomInstructions = this.customInstructions?.trim()
const preferredLanguage = getLanguageKey(
@@ -1216,38 +1279,11 @@ export class Task {
preferredLanguage && preferredLanguage !== DEFAULT_LANGUAGE_SETTINGS
? `# Preferred Language\n\nSpeak in ${preferredLanguage}.`
: ""
const clineRulesFilePath = path.resolve(cwd, GlobalFileNames.clineRules)
let clineRulesFileInstructions: string | undefined
if (await fileExistsAtPath(clineRulesFilePath)) {
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 ruleFilesTotalContent = 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 = formatResponse.clineRulesDirectoryInstructions(cwd, ruleFilesTotalContent)
} catch {
console.error(`Failed to read .clinerules directory at ${clineRulesFilePath}`)
}
} else {
try {
const ruleFileContent = (await fs.readFile(clineRulesFilePath, "utf8")).trim()
if (ruleFileContent) {
clineRulesFileInstructions = formatResponse.clineRulesFileInstructions(cwd, ruleFileContent)
}
} catch {
console.error(`Failed to read .clinerules file at ${clineRulesFilePath}`)
}
}
}
const localClineRulesFileInstructions = await getLocalClineRules(cwd)
const globalClineRulesFilePath = await ensureRulesDirectoryExists()
const globalClineRulesFileInstructions = await getGlobalClineRules(globalClineRulesFilePath)
const clineIgnoreContent = this.clineIgnoreController.clineIgnoreContent
let clineIgnoreInstructions: string | undefined
@@ -1257,14 +1293,16 @@ export class Task {
if (
settingsCustomInstructions ||
clineRulesFileInstructions ||
globalClineRulesFileInstructions ||
localClineRulesFileInstructions ||
clineIgnoreInstructions ||
preferredLanguageInstructions
) {
// altering the system prompt mid-task will break the prompt cache, but in the grand scheme this will not change often so it's better to not pollute user messages with it the way we have to with <potentially relevant details>
systemPrompt += addUserInstructions(
settingsCustomInstructions,
clineRulesFileInstructions,
globalClineRulesFileInstructions,
localClineRulesFileInstructions,
clineIgnoreInstructions,
preferredLanguageInstructions,
)
@@ -1475,6 +1513,8 @@ export class Task {
return `[${block.name}]`
case "attempt_completion":
return `[${block.name}]`
case "new_task":
return `[${block.name} for creating a new task]`
}
}
@@ -1715,12 +1755,14 @@ export class Task {
tool: fileExists ? "editedExistingFile" : "newFileCreated",
path: getReadablePath(cwd, removeClosingTag("path", relPath)),
content: diff || content,
operationIsLocatedInWorkspace: isLocatedInWorkspace(relPath),
}
if (block.partial) {
// update gui message
const partialMessage = JSON.stringify(sharedMessageProps)
if (this.shouldAutoApproveTool(block.name)) {
if (this.shouldAutoApproveToolWithPath(block.name, relPath)) {
this.removeLastPartialMessageIfExistsWithType("ask", "tool") // in case the user changes auto-approval settings mid stream
await this.say("tool", partialMessage, undefined, block.partial)
} else {
@@ -1777,6 +1819,7 @@ export class Task {
const completeMessage = JSON.stringify({
...sharedMessageProps,
content: diff || content,
operationIsLocatedInWorkspace: isLocatedInWorkspace(relPath),
// ? formatResponse.createPrettyPatch(
// relPath,
// this.diffViewProvider.originalContent,
@@ -1784,8 +1827,7 @@ export class Task {
// )
// : undefined,
} satisfies ClineSayTool)
if (this.shouldAutoApproveTool(block.name)) {
if (this.shouldAutoApproveToolWithPath(block.name, relPath)) {
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
await this.say("tool", completeMessage, undefined, false)
this.consecutiveAutoApprovedRequestsCount++
@@ -1875,7 +1917,7 @@ export class Task {
}
if (!fileExists) {
this.controllerRef.deref()?.workspaceTracker?.populateFilePaths()
this.workspaceTracker.populateFilePaths()
}
await this.diffViewProvider.reset()
@@ -1903,8 +1945,9 @@ export class Task {
const partialMessage = JSON.stringify({
...sharedMessageProps,
content: undefined,
operationIsLocatedInWorkspace: isLocatedInWorkspace(relPath),
} satisfies ClineSayTool)
if (this.shouldAutoApproveTool(block.name)) {
if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
await this.say("tool", partialMessage, undefined, block.partial)
} else {
@@ -1933,8 +1976,9 @@ export class Task {
const completeMessage = JSON.stringify({
...sharedMessageProps,
content: absolutePath,
operationIsLocatedInWorkspace: isLocatedInWorkspace(relPath),
} satisfies ClineSayTool)
if (this.shouldAutoApproveTool(block.name)) {
if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
await this.say("tool", completeMessage, undefined, false) // need to be sending partialValue bool, since undefined has its own purpose in that the message is treated neither as a partial or completion of a partial, but as a single complete message
this.consecutiveAutoApprovedRequestsCount++
@@ -1980,8 +2024,9 @@ export class Task {
const partialMessage = JSON.stringify({
...sharedMessageProps,
content: "",
operationIsLocatedInWorkspace: isLocatedInWorkspace(block.params.path),
} satisfies ClineSayTool)
if (this.shouldAutoApproveTool(block.name)) {
if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
await this.say("tool", partialMessage, undefined, block.partial)
} else {
@@ -2011,8 +2056,9 @@ export class Task {
const completeMessage = JSON.stringify({
...sharedMessageProps,
content: result,
operationIsLocatedInWorkspace: isLocatedInWorkspace(block.params.path),
} satisfies ClineSayTool)
if (this.shouldAutoApproveTool(block.name)) {
if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
await this.say("tool", completeMessage, undefined, false)
this.consecutiveAutoApprovedRequestsCount++
@@ -2050,8 +2096,9 @@ export class Task {
const partialMessage = JSON.stringify({
...sharedMessageProps,
content: "",
operationIsLocatedInWorkspace: isLocatedInWorkspace(block.params.path),
} satisfies ClineSayTool)
if (this.shouldAutoApproveTool(block.name)) {
if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
await this.say("tool", partialMessage, undefined, block.partial)
} else {
@@ -2078,8 +2125,9 @@ export class Task {
const completeMessage = JSON.stringify({
...sharedMessageProps,
content: result,
operationIsLocatedInWorkspace: isLocatedInWorkspace(block.params.path),
} satisfies ClineSayTool)
if (this.shouldAutoApproveTool(block.name)) {
if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
await this.say("tool", completeMessage, undefined, false)
this.consecutiveAutoApprovedRequestsCount++
@@ -2121,8 +2169,9 @@ export class Task {
const partialMessage = JSON.stringify({
...sharedMessageProps,
content: "",
operationIsLocatedInWorkspace: isLocatedInWorkspace(block.params.path),
} satisfies ClineSayTool)
if (this.shouldAutoApproveTool(block.name)) {
if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
await this.say("tool", partialMessage, undefined, block.partial)
} else {
@@ -2157,8 +2206,9 @@ export class Task {
const completeMessage = JSON.stringify({
...sharedMessageProps,
content: results,
operationIsLocatedInWorkspace: isLocatedInWorkspace(block.params.path),
} satisfies ClineSayTool)
if (this.shouldAutoApproveTool(block.name)) {
if (this.shouldAutoApproveToolWithPath(block.name, block.params.path)) {
this.removeLastPartialMessageIfExistsWithType("ask", "tool")
await this.say("tool", completeMessage, undefined, false)
this.consecutiveAutoApprovedRequestsCount++
@@ -2265,10 +2315,9 @@ export class Task {
await this.say("browser_action_result", "") // starts loading spinner
// Re-make browserSession to make sure latest settings apply
const localContext = this.controllerRef.deref()?.context
if (localContext) {
if (this.context) {
await this.browserSession.dispose()
this.browserSession = new BrowserSession(localContext, this.browserSettings)
this.browserSession = new BrowserSession(this.context, this.browserSettings)
} else {
console.warn("no controller context available for browserSession")
}
@@ -2466,7 +2515,7 @@ export class Task {
}
// Re-populate file paths in case the command modified the workspace (vscode listeners do not trigger unless the user manually creates/deletes files)
this.controllerRef.deref()?.workspaceTracker?.populateFilePaths()
this.workspaceTracker.populateFilePaths()
pushToolResult(result)
@@ -2548,9 +2597,8 @@ export class Task {
arguments: mcp_arguments,
} satisfies ClineAskUseMcpServer)
const isToolAutoApproved = this.controllerRef
.deref()
?.mcpHub?.connections?.find((conn) => conn.server.name === server_name)
const isToolAutoApproved = this.mcpHub.connections
?.find((conn) => conn.server.name === server_name)
?.server.tools?.find((tool) => tool.name === tool_name)?.autoApprove
if (this.shouldAutoApproveTool(block.name) && isToolAutoApproved) {
@@ -2570,9 +2618,7 @@ export class Task {
// now execute the tool
await this.say("mcp_server_request_started") // same as browser_action_result
const toolResult = await this.controllerRef
.deref()
?.mcpHub?.callTool(server_name, tool_name, parsedArguments)
const toolResult = await this.mcpHub.callTool(server_name, tool_name, parsedArguments)
// TODO: add progress indicator and ability to parse images and non-text responses
const toolResultPretty =
@@ -2660,7 +2706,7 @@ export class Task {
// now execute the tool
await this.say("mcp_server_request_started")
const resourceResult = await this.controllerRef.deref()?.mcpHub?.readResource(server_name, uri)
const resourceResult = await this.mcpHub.readResource(server_name, uri)
const resourceResultPretty =
resourceResult?.contents
.map((item) => {
@@ -2743,6 +2789,51 @@ export class Task {
break
}
}
case "new_task": {
const context: string | undefined = block.params.context
try {
if (block.partial) {
await this.ask("new_task", removeClosingTag("context", context), block.partial).catch(() => {})
break
} else {
if (!context) {
this.consecutiveMistakeCount++
pushToolResult(await this.sayAndCreateMissingParamError("new_task", "context"))
break
}
this.consecutiveMistakeCount = 0
if (this.autoApprovalSettings.enabled && this.autoApprovalSettings.enableNotifications) {
showSystemNotification({
subtitle: "Cline wants to start a new task...",
message: `Cline is suggesting to start a new task with: ${context}`,
})
}
const { text, images } = await this.ask("new_task", context, false)
// If the user provided a response, treat it as feedback
if (text || images?.length) {
await this.say("user_feedback", text ?? "", images)
pushToolResult(
formatResponse.toolResult(
`The user provided feedback instead of creating a new task:\n<feedback>\n${text}\n</feedback>`,
images,
),
)
} else {
// If no response, the user clicked the "Create New Task" button
pushToolResult(
formatResponse.toolResult(`The user has created a new task with the provided context.`),
)
}
break
}
} catch (error) {
await handleError("creating new task", error)
break
}
}
case "plan_mode_respond": {
const response: string | undefined = block.params.response
const optionsRaw: string | undefined = block.params.options
@@ -2834,14 +2925,7 @@ export class Task {
break
} else {
await this.say("load_mcp_documentation", "", undefined, false)
const mcpHub = this.controllerRef.deref()?.mcpHub
if (!mcpHub) {
throw new Error("MCP hub not available")
}
pushToolResult(await loadMcpDocumentation(mcpHub))
pushToolResult(await loadMcpDocumentation(this.mcpHub))
break
}
} catch (error) {
@@ -3049,6 +3133,14 @@ export class Task {
throw new Error("Cline instance aborted")
}
// Used to know what models were used in the task if user wants to export metadata for error reporting purposes
const currentProviderId = (await getGlobalState(this.getContext(), "apiProvider")) as string
if (currentProviderId && this.api.getModel().id) {
try {
await this.modelContextTracker.recordModelUsage(currentProviderId, this.api.getModel().id, this.chatSettings.mode)
} catch {}
}
if (this.consecutiveMistakeCount >= 3) {
if (this.autoApprovalSettings.enabled && this.autoApprovalSettings.enableNotifications) {
showSystemNotification({
@@ -3118,7 +3210,7 @@ export class Task {
if (!this.checkpointTracker && !this.checkpointTrackerErrorMessage) {
try {
this.checkpointTracker = await pTimeout(
CheckpointTracker.create(this.taskId, this.controllerRef.deref()?.context.globalStorageUri.fsPath),
CheckpointTracker.create(this.taskId, this.context.globalStorageUri.fsPath),
{
milliseconds: 15_000,
message:
@@ -3152,7 +3244,7 @@ export class Task {
content: userContent,
})
telemetryService.captureConversationTurnEvent(this.taskId, this.apiProvider, this.api.getModel().id, "user")
telemetryService.captureConversationTurnEvent(this.taskId, currentProviderId, this.api.getModel().id, "user")
// since we sent off a placeholder api_req_started message to update the webview while waiting to actually start the API request (to load potential details for example), we need to update the text of that message
const lastApiReqIndex = findLastIndex(this.clineMessages, (m) => m.say === "api_req_started")
@@ -3160,7 +3252,7 @@ export class Task {
request: userContent.map((block) => formatContentBlockToMarkdown(block)).join("\n\n"),
} satisfies ClineApiReqInfo)
await this.saveClineMessagesAndUpdateHistory()
await this.controllerRef.deref()?.postStateToWebview()
await this.postStateToWebview()
try {
let cacheWriteTokens = 0
@@ -3229,7 +3321,7 @@ export class Task {
updateApiReqMsg(cancelReason, streamingFailedMessage)
await this.saveClineMessagesAndUpdateHistory()
telemetryService.captureConversationTurnEvent(this.taskId, this.apiProvider, this.api.getModel().id, "assistant")
telemetryService.captureConversationTurnEvent(this.taskId, currentProviderId, this.api.getModel().id, "assistant")
// signals to provider that it can retrieve the saved messages from disk, as abortTask can not be awaited on in nature
this.didFinishAbortingStream = true
@@ -3320,11 +3412,7 @@ export class Task {
const errorMessage = this.formatErrorWithStatusCode(error)
await abortStream("streaming_failed", errorMessage)
const history = await this.controllerRef.deref()?.getTaskWithId(this.taskId)
if (history) {
await this.controllerRef.deref()?.initClineWithHistoryItem(history.historyItem)
// await this.controllerRef.deref()?.postStateToWebview()
}
await this.reinitExistingTaskFromId(this.taskId)
}
} finally {
this.isStreaming = false
@@ -3343,7 +3431,7 @@ export class Task {
}
updateApiReqMsg()
await this.saveClineMessagesAndUpdateHistory()
await this.controllerRef.deref()?.postStateToWebview()
await this.postStateToWebview()
})
}
@@ -3367,13 +3455,13 @@ export class Task {
updateApiReqMsg()
await this.saveClineMessagesAndUpdateHistory()
await this.controllerRef.deref()?.postStateToWebview()
await this.postStateToWebview()
// now add to apiconversationhistory
// need to save assistant responses to file before proceeding to tool use since user can exit at any moment and we wouldn't be able to save the assistant's response
let didEndLoop = false
if (assistantMessage.length > 0) {
telemetryService.captureConversationTurnEvent(this.taskId, this.apiProvider, this.api.getModel().id, "assistant")
telemetryService.captureConversationTurnEvent(this.taskId, currentProviderId, this.api.getModel().id, "assistant")
await this.addToApiConversationHistory({
role: "assistant",
@@ -3628,6 +3716,36 @@ export class Task {
}
}
// Add context window usage information
const { contextWindow, maxAllowedSize } = getContextWindowInfo(this.api)
// Get the token count from the most recent API request to accurately reflect context management
const getTotalTokensFromApiReqMessage = (msg: ClineMessage) => {
if (!msg.text) {
return 0
}
try {
const { tokensIn, tokensOut, cacheWrites, cacheReads } = JSON.parse(msg.text)
return (tokensIn || 0) + (tokensOut || 0) + (cacheWrites || 0) + (cacheReads || 0)
} catch (e) {
return 0
}
}
const modifiedMessages = combineApiRequests(combineCommandSequences(this.clineMessages.slice(1)))
const lastApiReqMessage = findLast(modifiedMessages, (msg) => {
if (msg.say !== "api_req_started") {
return false
}
return getTotalTokensFromApiReqMessage(msg) > 0
})
const lastApiReqTotalTokens = lastApiReqMessage ? getTotalTokensFromApiReqMessage(lastApiReqMessage) : 0
const usagePercentage = Math.round((lastApiReqTotalTokens / contextWindow) * 100)
details += "\n\n# Context Window Usage"
details += `\n${lastApiReqTotalTokens.toLocaleString()} / ${(contextWindow / 1000).toLocaleString()}K tokens used (${usagePercentage}%)`
details += "\n\n# Current Mode"
if (this.chatSettings.mode === "plan") {
details += "\nPLAN MODE\n" + formatResponse.planModeInstructions()
+1 -1
View File
@@ -23,7 +23,7 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
private readonly outputChannel: vscode.OutputChannel,
) {
WebviewProvider.activeInstances.add(this)
this.controller = new Controller(context, outputChannel, this)
this.controller = new Controller(context, outputChannel, (message) => this.view?.webview.postMessage(message))
}
async dispose() {
+21 -8
View File
@@ -9,6 +9,8 @@ import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider"
import assert from "node:assert"
import { telemetryService } from "./services/telemetry/TelemetryService"
import { WebviewProvider } from "./core/webview"
import { createTestServer, shutdownTestServer } from "./services/test/TestServer"
import { ErrorService } from "./services/error/ErrorService"
/*
Built using https://github.com/microsoft/vscode-webview-ui-toolkit
@@ -27,12 +29,14 @@ export function activate(context: vscode.ExtensionContext) {
outputChannel = vscode.window.createOutputChannel("Cline")
context.subscriptions.push(outputChannel)
ErrorService.initialize()
Logger.initialize(outputChannel)
Logger.log("Cline extension activated")
const sidebarWebview = new WebviewProvider(context, outputChannel)
vscode.commands.executeCommand("setContext", "cline.isDevMode", IS_DEV && IS_DEV === "true")
vscode.commands.executeCommand("setContext", "cline.isTestMode", IS_TEST && IS_TEST === "true")
context.subscriptions.push(
vscode.window.registerWebviewViewProvider(WebviewProvider.sideBarId, sidebarWebview, {
@@ -377,7 +381,7 @@ export function activate(context: vscode.ExtensionContext) {
// Register the command handler
context.subscriptions.push(
vscode.commands.registerCommand("cline.fixWithCline", async (range: vscode.Range, diagnostics: any[]) => {
vscode.commands.registerCommand("cline.fixWithCline", async (range: vscode.Range, diagnostics: vscode.Diagnostic[]) => {
const editor = vscode.window.activeTextEditor
if (!editor) {
return
@@ -393,13 +397,12 @@ export function activate(context: vscode.ExtensionContext) {
}),
)
return createClineAPI(outputChannel, sidebarWebview.controller)
}
// Set up test server if in test mode
if (IS_TEST === "true") {
createTestServer(sidebarWebview)
}
// This method is called when your extension is deactivated
export function deactivate() {
telemetryService.shutdown()
Logger.log("Cline extension deactivated")
return createClineAPI(outputChannel, sidebarWebview.controller)
}
// TODO: Find a solution for automatically removing DEV related content from production builds.
@@ -408,8 +411,18 @@ export function deactivate() {
//
// This is a workaround to reload the extension when the source code changes
// since vscode doesn't support hot reload for extensions
const { IS_DEV, DEV_WORKSPACE_FOLDER } = process.env
const { IS_DEV, DEV_WORKSPACE_FOLDER, IS_TEST } = process.env
// This method is called when your extension is deactivated
export function deactivate() {
// Shutdown the test server if it exists
shutdownTestServer()
telemetryService.shutdown()
Logger.log("Cline extension deactivated")
}
// Set up development mode file watcher
if (IS_DEV && IS_DEV !== "false") {
assert(DEV_WORKSPACE_FOLDER, "DEV_WORKSPACE_FOLDER must be set in development")
const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(DEV_WORKSPACE_FOLDER, "src/**/*"))
+29 -5
View File
@@ -105,8 +105,11 @@ export class DiffViewProvider {
const beginningOfDocument = new vscode.Position(0, 0)
diffEditor.selection = new vscode.Selection(beginningOfDocument, beginningOfDocument)
for (let i = 0; i < diffLines.length; i++) {
const currentLine = this.streamedLines.length + i
// Instead of animating each line, we'll update in larger chunks
const currentLine = this.streamedLines.length + diffLines.length - 1
if (currentLine >= 0) {
// Only proceed if we have new lines
// Replace all content up to the current line with accumulated lines
// This is necessary (as compared to inserting one line at a time) to handle cases where html tags on previous lines are auto closed for example
const edit = new vscode.WorkspaceEdit()
@@ -114,12 +117,33 @@ export class DiffViewProvider {
const contentToReplace = accumulatedLines.slice(0, currentLine + 1).join("\n") + "\n"
edit.replace(document.uri, rangeToReplace, contentToReplace)
await vscode.workspace.applyEdit(edit)
// Update decorations
// Update decorations for the entire changed section
this.activeLineController.setActiveLine(currentLine)
this.fadedOverlayController.updateOverlayAfterLine(currentLine, document.lineCount)
// Scroll to the current line
this.scrollEditorToLine(currentLine)
// Scroll to the last changed line
if (diffLines.length <= 5) {
// For small changes, just jump directly to the line
this.scrollEditorToLine(currentLine)
} else {
// For larger changes, create a quick scrolling animation
const startLine = this.streamedLines.length
const endLine = currentLine
const totalLines = endLine - startLine
const numSteps = 10 // Adjust this number to control animation speed
const stepSize = Math.max(1, Math.floor(totalLines / numSteps))
// Create and await the smooth scrolling animation
for (let line = startLine; line <= endLine; line += stepSize) {
this.activeDiffEditor?.revealRange(new vscode.Range(line, 0, line, 0), vscode.TextEditorRevealType.InCenter)
await new Promise((resolve) => setTimeout(resolve, 16)) // ~60fps
}
// Ensure we end at the final line
this.scrollEditorToLine(currentLine)
}
}
// Update the streamedLines with the new accumulated content
this.streamedLines = accumulatedLines
if (isFinal) {
+1 -1
View File
@@ -13,7 +13,7 @@ export async function openImage(dataUri: string) {
const imageBuffer = Buffer.from(base64Data, "base64")
const tempFilePath = path.join(os.tmpdir(), `temp_image_${Date.now()}.${format}`)
try {
await vscode.workspace.fs.writeFile(vscode.Uri.file(tempFilePath), imageBuffer)
await vscode.workspace.fs.writeFile(vscode.Uri.file(tempFilePath), new Uint8Array(imageBuffer))
await vscode.commands.executeCommand("vscode.open", vscode.Uri.file(tempFilePath))
} catch (error) {
vscode.window.showErrorMessage(`Error opening image: ${error}`)
@@ -2,17 +2,17 @@ import * as vscode from "vscode"
import * as path from "path"
import { listFiles } from "../../services/glob/list-files"
import { Controller } from "../../core/controller"
import { ExtensionMessage } from "../../shared/ExtensionMessage"
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
// Note: this is not a drop-in replacement for listFiles at the start of tasks, since that will be done for Desktops when there is no workspace selected
class WorkspaceTracker {
private controllerRef: WeakRef<Controller>
private disposables: vscode.Disposable[] = []
private filePaths: Set<string> = new Set()
constructor(controller: Controller) {
this.controllerRef = new WeakRef(controller)
constructor(private readonly postMessageToWebview: (message: ExtensionMessage) => Promise<void>) {
this.postMessageToWebview = postMessageToWebview
this.registerListeners()
}
@@ -85,7 +85,7 @@ class WorkspaceTracker {
if (!cwd) {
return
}
this.controllerRef.deref()?.postMessageToWebview({
this.postMessageToWebview({
type: "workspaceUpdated",
filePaths: Array.from(this.filePaths).map((file) => {
const relativePath = path.relative(cwd, file).toPosix()
+12 -20
View File
@@ -1,26 +1,18 @@
import axios, { AxiosRequestConfig, AxiosResponse } from "axios"
import { Controller } from "../../core/controller"
import type { BalanceResponse, PaymentTransaction, UsageTransaction } from "../../shared/ClineAccount"
import { ExtensionMessage } from "../../shared/ExtensionMessage"
export class ClineAccountService {
private readonly baseUrl = "https://api.cline.bot/v1"
private controllerRef: WeakRef<Controller>
private postMessageToWebview: (message: ExtensionMessage) => Promise<void>
private getClineApiKey: () => Promise<string | undefined>
constructor(controller: Controller) {
this.controllerRef = new WeakRef(controller)
}
/**
* Get the user's Cline Account key from the apiConfiguration
*/
private async getClineApiKey(): Promise<string | undefined> {
const provider = this.controllerRef.deref()
if (!provider) {
return undefined
}
const { apiConfiguration } = await provider.getStateToPostToWebview()
return apiConfiguration?.clineApiKey
constructor(
postMessageToWebview: (message: ExtensionMessage) => Promise<void>,
getClineApiKey: () => Promise<string | undefined>,
) {
this.postMessageToWebview = postMessageToWebview
this.getClineApiKey = getClineApiKey
}
/**
@@ -64,7 +56,7 @@ export class ClineAccountService {
const data = await this.authenticatedRequest<BalanceResponse>("/user/credits/balance")
// Post to webview
await this.controllerRef.deref()?.postMessageToWebview({
await this.postMessageToWebview({
type: "userCreditsBalance",
userCreditsBalance: data,
})
@@ -84,7 +76,7 @@ export class ClineAccountService {
const data = await this.authenticatedRequest<UsageTransaction[]>("/user/credits/usage")
// Post to webview
await this.controllerRef.deref()?.postMessageToWebview({
await this.postMessageToWebview({
type: "userCreditsUsage",
userCreditsUsage: data,
})
@@ -104,7 +96,7 @@ export class ClineAccountService {
const data = await this.authenticatedRequest<PaymentTransaction[]>("/user/credits/payments")
// Post to webview
await this.controllerRef.deref()?.postMessageToWebview({
await this.postMessageToWebview({
type: "userCreditsPayments",
userCreditsPayments: data,
})
+3 -2
View File
@@ -2,7 +2,8 @@ import * as vscode from "vscode"
import * as fs from "fs/promises"
import * as path from "path"
import { exec, spawn } from "child_process"
import { Browser, Page, ScreenshotOptions, TimeoutError, launch, connect } from "puppeteer-core"
import { Browser, Page, TimeoutError, launch, connect } from "puppeteer-core"
import type { ScreenshotOptions, ConsoleMessage } from "puppeteer-core"
// @ts-ignore
import PCR from "puppeteer-chromium-resolver"
import pWaitFor from "p-wait-for"
@@ -451,7 +452,7 @@ export class BrowserSession {
const logs: string[] = []
let lastLogTs = Date.now()
const consoleListener = (msg: any) => {
const consoleListener = (msg: ConsoleMessage) => {
if (msg.type() === "log") {
logs.push(msg.text())
} else {
+96
View File
@@ -0,0 +1,96 @@
import * as Sentry from "@sentry/browser"
import * as vscode from "vscode"
import { telemetryService } from "../telemetry/TelemetryService"
import * as pkg from "../../../package.json"
let telemetryLevel = vscode.workspace.getConfiguration("telemetry").get<string>("telemetryLevel", "all")
let isTelemetryEnabled = ["all", "error"].includes(telemetryLevel)
vscode.workspace.onDidChangeConfiguration(() => {
telemetryLevel = vscode.workspace.getConfiguration("telemetry").get<string>("telemetryLevel", "all")
isTelemetryEnabled = ["all", "error"].includes(telemetryLevel)
ErrorService.toggleEnabled(isTelemetryEnabled)
if (isTelemetryEnabled) {
ErrorService.setLevel(telemetryLevel as "error" | "all")
}
})
export class ErrorService {
private static serviceEnabled: boolean
private static serviceLevel: string
static initialize() {
// Initialize sentry
Sentry.init({
dsn: "https://7936780e3f0f0290fcf8d4a395c249b7@o4509028819664896.ingest.us.sentry.io/4509052955983872",
environment: process.env.NODE_ENV,
release: `cline@${pkg.version}`,
integrations: [Sentry.browserTracingIntegration(), Sentry.replayIntegration()],
beforeSend(event) {
// TelemetryService keeps track of whether the user has opted in to telemetry/error reporting
const isUserManuallyOptedIn = telemetryService.isTelemetryEnabled()
if (isUserManuallyOptedIn && ErrorService.isEnabled()) {
return event
}
return null
},
})
ErrorService.toggleEnabled(true)
ErrorService.setLevel("error")
}
static toggleEnabled(state: boolean) {
if (state === false) {
ErrorService.serviceEnabled = false
return
}
// If we are trying to enable the service, check that we are allowed to.
if (isTelemetryEnabled) {
ErrorService.serviceEnabled = true
}
}
static setLevel(level: "error" | "all") {
switch (telemetryLevel) {
case "error": {
if (level === "error") {
ErrorService.serviceLevel = level
}
break
}
default: {
ErrorService.serviceLevel = level
}
}
}
static logException(error: Error): void {
// Don't log if telemetry is off
const isUserManuallyOptedIn = telemetryService.isTelemetryEnabled()
if (!isUserManuallyOptedIn || !ErrorService.isEnabled()) {
return
}
// Log the error to Sentry
Sentry.captureException(error)
}
static logMessage(message: string, level: "error" | "warning" | "log" | "debug" | "info" = "log"): void {
// Don't log if telemetry is off
const isUserManuallyOptedIn = telemetryService.isTelemetryEnabled()
if (!isUserManuallyOptedIn || !ErrorService.isEnabled()) {
return
}
if (ErrorService.serviceLevel === "error" && level === "error") {
// Log the message if allowed
Sentry.captureMessage(message, { level })
return
}
// Log the message if allowed
Sentry.captureMessage(message, { level })
}
static isEnabled(): boolean {
return ErrorService.serviceEnabled
}
}
+20 -1
View File
@@ -1,4 +1,5 @@
import type { OutputChannel } from "vscode"
import { ErrorService } from "../error/ErrorService"
/**
* Simple logging utility for the extension's backend code.
@@ -12,7 +13,25 @@ export class Logger {
Logger.outputChannel = outputChannel
}
static error(message: string, exception?: Error) {
Logger.outputChannel.appendLine(`ERROR: ${message}`)
ErrorService.logMessage(message, "error")
exception && ErrorService.logException(exception)
}
static warn(message: string) {
Logger.outputChannel.appendLine(`WARN: ${message}`)
ErrorService.logMessage(message, "warning")
}
static log(message: string) {
Logger.outputChannel.appendLine(message)
Logger.outputChannel.appendLine(`LOG: ${message}`)
}
static debug(message: string) {
Logger.outputChannel.appendLine(`DEBUG: ${message}`)
}
static info(message: string) {
Logger.outputChannel.appendLine(`INFO: ${message}`)
}
static trace(message: string) {
Logger.outputChannel.appendLine(`TRACE: ${message}`)
}
}
+20 -25
View File
@@ -14,7 +14,6 @@ import * as fs from "fs/promises"
import * as path from "path"
import * as vscode from "vscode"
import { z } from "zod"
import { Controller } from "../../core/controller"
import {
DEFAULT_MCP_TIMEOUT_SECONDS,
McpMode,
@@ -31,6 +30,7 @@ import { arePathsEqual } from "../../utils/path"
import { secondsToMs } from "../../utils/time"
import { GlobalFileNames } from "../../core/storage/disk"
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
import { ExtensionMessage } from "../../shared/ExtensionMessage"
// Default timeout for internal MCP data requests in milliseconds; is not the same as the user facing timeout stored as DEFAULT_MCP_TIMEOUT_SECONDS
const DEFAULT_REQUEST_TIMEOUT_MS = 5000
@@ -76,15 +76,27 @@ const McpSettingsSchema = z.object({
})
export class McpHub {
private controllerRef: WeakRef<Controller>
getMcpServersPath: () => Promise<string>
private getSettingsDirectoryPath: () => Promise<string>
private postMessageToWebview: (message: ExtensionMessage) => Promise<void>
private clientVersion: string
private disposables: vscode.Disposable[] = []
private settingsWatcher?: vscode.FileSystemWatcher
private fileWatchers: Map<string, FSWatcher> = new Map()
connections: McpConnection[] = []
isConnecting: boolean = false
constructor(controller: Controller) {
this.controllerRef = new WeakRef(controller)
constructor(
getMcpServersPath: () => Promise<string>,
getSettingsDirectoryPath: () => Promise<string>,
postMessageToWebview: (message: ExtensionMessage) => Promise<void>,
clientVersion: string,
) {
this.getMcpServersPath = getMcpServersPath
this.getSettingsDirectoryPath = getSettingsDirectoryPath
this.postMessageToWebview = postMessageToWebview
this.clientVersion = clientVersion
this.watchMcpSettingsFile()
this.initializeMcpServers()
}
@@ -94,21 +106,8 @@ export class McpHub {
return this.connections.filter((conn) => !conn.server.disabled).map((conn) => conn.server)
}
async getMcpServersPath(): Promise<string> {
const provider = this.controllerRef.deref()
if (!provider) {
throw new Error("Provider not available")
}
const mcpServersPath = await provider.ensureMcpServersDirectoryExists()
return mcpServersPath
}
async getMcpSettingsFilePath(): Promise<string> {
const provider = this.controllerRef.deref()
if (!provider) {
throw new Error("Provider not available")
}
const mcpSettingsFilePath = path.join(await provider.ensureSettingsDirectoryExists(), GlobalFileNames.mcpSettings)
const mcpSettingsFilePath = path.join(await this.getSettingsDirectoryPath(), GlobalFileNames.mcpSettings)
const fileExists = await fileExistsAtPath(mcpSettingsFilePath)
if (!fileExists) {
await fs.writeFile(
@@ -193,7 +192,7 @@ export class McpHub {
const client = new Client(
{
name: "Cline",
version: this.controllerRef.deref()?.context.extension?.packageJSON?.version ?? "1.0.0",
version: this.clientVersion,
},
{
capabilities: {},
@@ -425,7 +424,7 @@ export class McpHub {
this.isConnecting = false
}
private setupFileWatcher(name: string, config: any) {
private setupFileWatcher(name: string, config: Extract<McpServerConfig, { transportType: "stdio" }>) {
const filePath = config.args?.find((arg: string) => arg.includes("build/index.js"))
if (filePath) {
// we use chokidar instead of onDidSaveTextDocument because it doesn't require the file to be open in the editor. The settings config is better suited for onDidSave since that will be manually updated by the user or Cline (and we want to detect save events, not every file change)
@@ -451,10 +450,6 @@ export class McpHub {
async restartConnection(serverName: string): Promise<void> {
this.isConnecting = true
const provider = this.controllerRef.deref()
if (!provider) {
return
}
// Get existing connection and update its status
const connection = this.connections.find((conn) => conn.server.name === serverName)
@@ -486,7 +481,7 @@ export class McpHub {
const content = await fs.readFile(settingsPath, "utf-8")
const config = JSON.parse(content)
const serverOrder = Object.keys(config.mcpServers || {})
await this.controllerRef.deref()?.postMessageToWebview({
await this.postMessageToWebview({
type: "mcpServers",
mcpServers: [...this.connections]
.sort((a, b) => {
+19 -1
View File
@@ -3,6 +3,7 @@ import * as vscode from "vscode"
import { version as extensionVersion } from "../../../package.json"
import type { TaskFeedbackType } from "../../shared/WebviewMessage"
import type { BrowserSettings } from "../../shared/BrowserSettings"
/**
* PostHogClient handles telemetry event tracking for the Cline extension
@@ -71,6 +72,8 @@ class PostHogClient {
PLAN_MODE_TOGGLED: "ui.plan_mode_toggled",
// Tracks when action mode is toggled on
ACT_MODE_TOGGLED: "ui.act_mode_toggled",
// Tracks when users use the "favorite" button in the model picker
MODEL_FAVORITE_TOGGLED: "ui.model_favorite_toggled",
},
}
@@ -472,7 +475,7 @@ class PostHogClient {
* @param taskId Unique identifier for the task
* @param browserSettings The browser settings being used
*/
public captureBrowserToolStart(taskId: string, browserSettings: any) {
public captureBrowserToolStart(taskId: string, browserSettings: BrowserSettings) {
this.capture({
event: PostHogClient.EVENTS.TASK.BROWSER_TOOL_START,
properties: {
@@ -574,6 +577,21 @@ class PostHogClient {
})
}
/**
* Records when the user uses the model favorite button in the model picker
* @param model The name of the model the user has interacted with
* @param isFavorited Whether the model is being favorited (true) or unfavorited (false)
*/
public captureModelFavoritesUsage(model: string, isFavorited: boolean) {
this.capture({
event: PostHogClient.EVENTS.UI.MODEL_FAVORITE_TOGGLED,
properties: {
model,
isFavorited,
},
})
}
public isTelemetryEnabled(): boolean {
return this.telemetryEnabled
}
+362
View File
@@ -0,0 +1,362 @@
import * as http from "http"
import * as vscode from "vscode"
import { Logger } from "../../services/logging/Logger"
import { WebviewProvider } from "../../core/webview"
import { AutoApprovalSettings } from "../../shared/AutoApprovalSettings"
import { updateGlobalState, getAllExtensionState } from "../../core/storage/state"
import { ClineAsk, ExtensionMessage } from "../../shared/ExtensionMessage"
import { WebviewMessage } from "../../shared/WebviewMessage"
// Task completion tracking
let taskCompletionResolver: (() => void) | null = null
// Function to create a new task completion promise
function createTaskCompletionTracker(): Promise<void> {
// Create a new promise that will resolve when the task is completed
return new Promise<void>((resolve) => {
taskCompletionResolver = resolve
})
}
// Function to mark the current task as completed
function completeTask(): void {
if (taskCompletionResolver) {
taskCompletionResolver()
taskCompletionResolver = null
Logger.log("Task marked as completed")
}
}
let testServer: http.Server | undefined
let messageCatcherDisposable: vscode.Disposable | undefined
/**
* Updates the auto approval settings to enable all actions
* @param context The VSCode extension context
* @param provider The webview provider instance
*/
async function updateAutoApprovalSettings(context: vscode.ExtensionContext, provider?: WebviewProvider) {
try {
const { autoApprovalSettings } = await getAllExtensionState(context)
// Enable all actions
const updatedSettings: AutoApprovalSettings = {
...autoApprovalSettings,
enabled: true,
actions: {
readFiles: true,
readFilesExternally: true,
editFiles: true,
editFilesExternally: true,
executeSafeCommands: true,
executeAllCommands: true,
useBrowser: false, // Keep browser disabled for tests
useMcp: false, // Keep MCP disabled for tests
},
maxRequests: 10000, // Increase max requests for tests
}
await updateGlobalState(context, "autoApprovalSettings", updatedSettings)
Logger.log("Auto approval settings updated for test mode")
// Update the webview with the new state
if (provider?.controller) {
await provider.controller.postStateToWebview()
}
} catch (error) {
Logger.log(`Error updating auto approval settings: ${error}`)
}
}
/**
* Creates and starts an HTTP server for test automation
* @param webviewProvider The webview provider instance to use for message catching
* @returns The created HTTP server instance
*/
export function createTestServer(webviewProvider?: WebviewProvider): http.Server {
// Update auto approval settings if webviewProvider is available
if (webviewProvider?.controller?.context) {
updateAutoApprovalSettings(webviewProvider.controller.context, webviewProvider)
}
const PORT = 9876
testServer = http.createServer((req, res) => {
// Set CORS headers
res.setHeader("Access-Control-Allow-Origin", "*")
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS")
res.setHeader("Access-Control-Allow-Headers", "Content-Type")
// Handle preflight requests
if (req.method === "OPTIONS") {
res.writeHead(204)
res.end()
return
}
// Only handle POST requests to /task
if (req.method !== "POST" || req.url !== "/task") {
res.writeHead(404)
res.end(JSON.stringify({ error: "Not found" }))
return
}
// Parse the request body
let body = ""
req.on("data", (chunk) => {
body += chunk.toString()
})
req.on("end", async () => {
try {
// Parse the JSON body
const { task } = JSON.parse(body)
if (!task) {
res.writeHead(400)
res.end(JSON.stringify({ error: "Missing task parameter" }))
return
}
// Get a visible webview instance
const visibleWebview = WebviewProvider.getVisibleInstance()
if (!visibleWebview || !visibleWebview.controller) {
res.writeHead(500)
res.end(JSON.stringify({ error: "No active Cline instance found" }))
return
}
// Initiate a new task
Logger.log(`Test server initiating task: ${task}`)
try {
// Clear any existing task
await visibleWebview.controller.clearTask()
// Ensure we're in Act mode before initiating the task
const { chatSettings } = await visibleWebview.controller.getStateToPostToWebview()
if (chatSettings.mode === "plan") {
// Switch to Act mode if currently in Plan mode
await visibleWebview.controller.togglePlanActModeWithChatSettings({ mode: "act" })
}
// Initiate the new task
const taskId = await visibleWebview.controller.initTask(task)
// Create a completion tracker for this task
const completionPromise = createTaskCompletionTracker()
// Wait for the task to complete with a timeout
const timeoutPromise = new Promise<void>((_, reject) => {
setTimeout(() => reject(new Error("Task completion timeout")), 15 * 60 * 1000) // 15 minute timeout
})
try {
// Wait for either completion or timeout
await Promise.race([completionPromise, timeoutPromise])
// Return success response with the task ID
res.writeHead(200, { "Content-Type": "application/json" })
res.end(
JSON.stringify({
success: true,
taskId,
completed: true,
}),
)
} catch (timeoutError) {
// Task didn't complete within the timeout period
res.writeHead(200, { "Content-Type": "application/json" })
res.end(
JSON.stringify({
success: true,
taskId,
completed: false,
timeout: true,
}),
)
}
} catch (error) {
Logger.log(`Error initiating task: ${error}`)
res.writeHead(500)
res.end(JSON.stringify({ error: `Failed to initiate task: ${error}` }))
}
} catch (error) {
res.writeHead(400)
res.end(JSON.stringify({ error: `Invalid JSON: ${error}` }))
}
})
})
testServer.listen(PORT, () => {
Logger.log(`Test server listening on port ${PORT}`)
})
// Handle server errors
testServer.on("error", (error) => {
Logger.log(`Test server error: ${error}`)
})
// Set up message catcher for the provided webview instance or try to get the visible one
if (webviewProvider) {
messageCatcherDisposable = createMessageCatcher(webviewProvider)
} else {
const visibleWebview = WebviewProvider.getVisibleInstance()
if (visibleWebview) {
messageCatcherDisposable = createMessageCatcher(visibleWebview)
} else {
Logger.log("No visible webview instance found for message catcher")
}
}
return testServer
}
/**
* Creates a message catcher that logs all messages sent to the webview
* and automatically responds to messages that require user intervention
* @param webviewProvider The webview provider instance
* @returns A disposable that can be used to clean up the message catcher
*/
export function createMessageCatcher(webviewProvider: WebviewProvider): vscode.Disposable {
Logger.log("Cline message catcher registered")
if (webviewProvider && webviewProvider.controller) {
const originalPostMessageToWebview = webviewProvider.controller.postMessageToWebview
// Intercept outgoing messages from extension to webview
webviewProvider.controller.postMessageToWebview = async (message: ExtensionMessage) => {
Logger.log("Cline message received: " + JSON.stringify(message))
// Check for completion_result message
if (message.type === "partialMessage" && message.partialMessage?.say === "completion_result") {
// Complete the current task
completeTask()
}
// Check for ask messages that require user intervention
if (message.type === "partialMessage" && message.partialMessage?.type === "ask" && !message.partialMessage.partial) {
const askType = message.partialMessage.ask as ClineAsk
const askText = message.partialMessage.text
// Automatically respond to different types of asks
setTimeout(() => {
autoRespondToAsk(webviewProvider, askType, askText)
}, 100) // Small delay to ensure the message is processed first
}
return originalPostMessageToWebview.call(webviewProvider.controller, message)
}
} else {
Logger.log("No visible webview instance found for message catcher")
}
return new vscode.Disposable(() => {
// Cleanup function if needed
Logger.log("Cline message catcher disposed")
})
}
/**
* Automatically responds to ask messages to continue task execution without user intervention
* @param webviewProvider The webview provider instance
* @param askType The type of ask message
* @param askText The text content of the ask message
*/
function autoRespondToAsk(webviewProvider: WebviewProvider, askType: ClineAsk, askText?: string): void {
if (!webviewProvider.controller) {
return
}
Logger.log(`Auto-responding to ask type: ${askType}`)
// Create a response message based on the ask type
const response: WebviewMessage = {
type: "askResponse",
askResponse: "yesButtonClicked", // Default to approving most actions
}
// Handle specific ask types differently if needed
switch (askType) {
case "followup":
// For follow-up questions, provide a generic response
response.askResponse = "messageResponse"
response.text = "I can't answer any questions right now, use your best judgment."
break
case "api_req_failed":
// Always retry API requests
response.askResponse = "yesButtonClicked" // "Retry" button
break
case "completion_result":
// Accept the completion
response.askResponse = "messageResponse"
response.text = "Task completed successfully."
break
case "mistake_limit_reached":
// Provide guidance to continue
response.askResponse = "messageResponse"
response.text = "Try breaking down the task into smaller steps."
break
case "auto_approval_max_req_reached":
// Reset the count to continue
response.askResponse = "yesButtonClicked" // "Reset and continue" button
break
case "resume_task":
case "resume_completed_task":
// Resume the task
response.askResponse = "messageResponse"
break
case "new_task":
// Decline creating a new task to keep the current task running
response.askResponse = "messageResponse"
response.text = "Continue with the current task."
break
case "plan_mode_respond":
// Respond to plan mode with a message to toggle to Act mode
response.askResponse = "messageResponse"
response.text = "PLAN_MODE_TOGGLE_RESPONSE" // Special marker to toggle to Act mode
// Automatically toggle to Act mode after responding
setTimeout(async () => {
try {
if (webviewProvider.controller) {
Logger.log("Auto-toggling to Act mode from Plan mode")
await webviewProvider.controller.togglePlanActModeWithChatSettings({ mode: "act" })
}
} catch (error) {
Logger.log(`Error toggling to Act mode: ${error}`)
}
}, 500) // Small delay to ensure the response is processed first
break
// For all other ask types (tool, command, browser_action_launch, use_mcp_server),
// we use the default "yesButtonClicked" to approve the action
}
// Send the response message
webviewProvider.controller.handleWebviewMessage(response)
Logger.log(`Auto-responded to ${askType} with ${response.askResponse}`)
}
/**
* Shuts down the test server if it exists
*/
export function shutdownTestServer() {
if (testServer) {
testServer.close()
Logger.log("Test server shut down")
testServer = undefined
}
// Dispose of the message catcher if it exists
if (messageCatcherDisposable) {
messageCatcherDisposable.dispose()
messageCatcherDisposable = undefined
}
}
+8 -4
View File
@@ -3,10 +3,12 @@ export interface AutoApprovalSettings {
enabled: boolean
// Individual action permissions
actions: {
readFiles: boolean // Read files and directories
editFiles: boolean // Edit files
executeSafeCommands: boolean // Execute safe commands
executeAllCommands: boolean // Execute all commands
readFiles: boolean // Read files and directories in the working directory
readFilesExternally?: boolean // Read files and directories outside of the working directory
editFiles: boolean // Edit files in the working directory
editFilesExternally?: boolean // Edit files outside of the working directory
executeSafeCommands?: boolean // Execute safe commands
executeAllCommands?: boolean // Execute all commands
useBrowser: boolean // Use browser
useMcp: boolean // Use MCP servers
}
@@ -19,7 +21,9 @@ export const DEFAULT_AUTO_APPROVAL_SETTINGS: AutoApprovalSettings = {
enabled: false,
actions: {
readFiles: false,
readFilesExternally: false,
editFiles: false,
editFilesExternally: false,
executeSafeCommands: false,
executeAllCommands: false,
useBrowser: false,
+11 -1
View File
@@ -6,7 +6,7 @@ import { AutoApprovalSettings } from "./AutoApprovalSettings"
import { BrowserSettings } from "./BrowserSettings"
import { ChatSettings } from "./ChatSettings"
import { HistoryItem } from "./HistoryItem"
import { McpServer, McpMarketplaceCatalog, McpMarketplaceItem, McpDownloadResponse } from "./mcp"
import { McpServer, McpMarketplaceCatalog, McpMarketplaceItem, McpDownloadResponse, McpViewTab } from "./mcp"
import { TelemetrySetting } from "./TelemetrySetting"
import type { BalanceResponse, UsageTransaction, PaymentTransaction } from "../shared/ClineAccount"
@@ -24,6 +24,7 @@ export interface ExtensionMessage {
| "partialMessage"
| "openRouterModels"
| "openAiModels"
| "requestyModels"
| "mcpServers"
| "relinquishControl"
| "vsCodeLmModels"
@@ -69,6 +70,7 @@ export interface ExtensionMessage {
partialMessage?: ClineMessage
openRouterModels?: Record<string, ModelInfo>
openAiModels?: string[]
requestyModels?: Record<string, ModelInfo>
mcpServers?: McpServer[]
customToken?: string
mcpMarketplaceCatalog?: McpMarketplaceCatalog
@@ -106,6 +108,7 @@ export interface ExtensionMessage {
serverName: string
error?: string
}
tab?: McpViewTab
}
export type Invoke = "sendMessage" | "primaryButtonClick" | "secondaryButtonClick"
@@ -151,6 +154,7 @@ export interface ClineMessage {
partial?: boolean
lastCheckpointHash?: string
isCheckpointCheckedOut?: boolean
isOperationOutsideWorkspace?: boolean
conversationHistoryIndex?: number
conversationHistoryDeletedRange?: [number, number] // for when conversation history is truncated for API requests
}
@@ -169,6 +173,7 @@ export type ClineAsk =
| "auto_approval_max_req_reached"
| "browser_action_launch"
| "use_mcp_server"
| "new_task"
export type ClineSay =
| "task"
@@ -211,6 +216,7 @@ export interface ClineSayTool {
content?: string
regex?: string
filePattern?: string
operationIsLocatedInWorkspace?: boolean
}
// must keep in sync with system prompt
@@ -256,6 +262,10 @@ export interface ClineAskQuestion {
selected?: string
}
export interface ClineAskNewTask {
context: string
}
export interface ClineApiReqInfo {
request?: string
tokensIn?: number
+6
View File
@@ -5,6 +5,7 @@ import { ChatSettings } from "./ChatSettings"
import { UserInfo } from "./UserInfo"
import { ChatContent } from "./ChatContent"
import { TelemetrySetting } from "./TelemetrySetting"
import { McpViewTab } from "./mcp"
export interface WebviewMessage {
type:
@@ -29,6 +30,7 @@ export interface WebviewMessage {
| "openMention"
| "cancelTask"
| "refreshOpenRouterModels"
| "refreshRequestyModels"
| "refreshOpenAiModels"
| "openMcpSettings"
| "restartMcpServer"
@@ -78,6 +80,7 @@ export interface WebviewMessage {
| "scrollToSettings"
| "getRelativePaths" // Handles single and multiple URI resolution
| "searchFiles"
| "toggleFavoriteModel"
// | "relaunchChromeDebugMode"
text?: string
uris?: string[] // Used for getRelativePaths
@@ -93,6 +96,7 @@ export interface WebviewMessage {
chatContent?: ChatContent
mcpId?: string
timeout?: number
tab?: McpViewTab
// For toggleToolAutoApprove
serverName?: string
serverUrl?: string
@@ -111,6 +115,8 @@ export interface WebviewMessage {
feedbackType?: TaskFeedbackType
mentionsRequestId?: string
query?: string
// For toggleFavoriteModel
modelId?: string
}
export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse"
+131 -9
View File
@@ -1,3 +1,5 @@
import type { LanguageModelChatSelector } from "../api/providers/types"
export type ApiProvider =
| "anthropic"
| "openrouter"
@@ -60,36 +62,46 @@ export interface ApiHandlerOptions {
deepSeekApiKey?: string
requestyApiKey?: string
requestyModelId?: string
requestyModelInfo?: ModelInfo
togetherApiKey?: string
togetherModelId?: string
qwenApiKey?: string
doubaoApiKey?: string
mistralApiKey?: string
azureApiVersion?: string
vsCodeLmModelSelector?: any
vsCodeLmModelSelector?: LanguageModelChatSelector
o3MiniReasoningEffort?: string
qwenApiLine?: string
asksageApiUrl?: string
asksageApiKey?: string
xaiApiKey?: string
thinkingBudgetTokens?: number
reasoningEffort?: string
sambanovaApiKey?: string
}
export type ApiConfiguration = ApiHandlerOptions & {
apiProvider?: ApiProvider
favoritedModelIds?: string[]
}
// Models
interface PriceTier {
tokenLimit: number // Upper limit (inclusive) of *input* tokens for this price. Use Infinity for the highest tier.
price: number // Price per million tokens for this tier.
}
export interface ModelInfo {
maxTokens?: number
contextWindow?: number
supportsImages?: boolean
supportsComputerUse?: boolean
supportsPromptCache: boolean // this value is hardcoded for now
inputPrice?: number
outputPrice?: number
inputPrice?: number // Keep for non-tiered input models
inputPriceTiers?: PriceTier[] // Add for tiered input pricing
outputPrice?: number // Keep for non-tiered output models
outputPriceTiers?: PriceTier[] // Add for tiered output pricing
cacheWritesPrice?: number
cacheReadsPrice?: number
description?: string
@@ -384,8 +396,16 @@ export const vertexModels = {
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 1.25,
outputPrice: 10,
// inputPrice: 1.25, // Removed
// outputPrice: 10, // Removed
inputPriceTiers: [
{ tokenLimit: 200000, price: 1.25 }, // Input price for <= 200k input tokens
{ tokenLimit: Infinity, price: 2.5 }, // Input price for > 200k input tokens
],
outputPriceTiers: [
{ tokenLimit: 200000, price: 10.0 }, // Output price for <= 200k input tokens
{ tokenLimit: Infinity, price: 15.0 }, // Output price for > 200k input tokens
],
},
"gemini-2.0-flash-thinking-exp-01-21": {
maxTokens: 65_536,
@@ -474,8 +494,14 @@ export const geminiModels = {
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 1.25,
outputPrice: 10,
inputPriceTiers: [
{ tokenLimit: 200000, price: 1.25 }, // Input price for <= 200k input tokens
{ tokenLimit: Infinity, price: 2.5 }, // Input price for > 200k input tokens
],
outputPriceTiers: [
{ tokenLimit: 200000, price: 10.0 }, // Output price for <= 200k input tokens
{ tokenLimit: Infinity, price: 15.0 }, // Output price for > 200k input tokens
],
},
"gemini-2.0-flash-001": {
maxTokens: 8192,
@@ -578,8 +604,32 @@ export const geminiModels = {
// OpenAI Native
// https://openai.com/api/pricing/
export type OpenAiNativeModelId = keyof typeof openAiNativeModels
export const openAiNativeDefaultModelId: OpenAiNativeModelId = "gpt-4o"
export const openAiNativeDefaultModelId: OpenAiNativeModelId = "gpt-4.1"
export const openAiNativeModels = {
"gpt-4.1": {
maxTokens: 32_768,
contextWindow: 1_047_576,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 2,
outputPrice: 8,
},
"gpt-4.1-mini": {
maxTokens: 32_768,
contextWindow: 1_047_576,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 0.4,
outputPrice: 1.6,
},
"gpt-4.1-nano": {
maxTokens: 32_768,
contextWindow: 1_047_576,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 0.1,
outputPrice: 0.4,
},
"o3-mini": {
maxTokens: 100_000,
contextWindow: 200_000,
@@ -1143,6 +1193,26 @@ export const doubaoModels = {
cacheWritesPrice: 0,
cacheReadsPrice: 0,
},
"deepseek-v3-250324": {
maxTokens: 12_288,
contextWindow: 128_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.55,
outputPrice: 2.19,
cacheWritesPrice: 0,
cacheReadsPrice: 0,
},
"deepseek-r1-250120": {
maxTokens: 32_768,
contextWindow: 64_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.27,
outputPrice: 1.09,
cacheWritesPrice: 0,
cacheReadsPrice: 0,
},
} as const satisfies Record<string, ModelInfo>
// Mistral
@@ -1298,8 +1368,44 @@ export const askSageModels = {
// X AI
// https://docs.x.ai/docs/api-reference
export type XAIModelId = keyof typeof xaiModels
export const xaiDefaultModelId: XAIModelId = "grok-2-latest"
export const xaiDefaultModelId: XAIModelId = "grok-3-beta"
export const xaiModels = {
"grok-3-beta": {
maxTokens: 8192,
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 3.0,
outputPrice: 15.0,
description: "X AI's Grok-3 beta model with 131K context window",
},
"grok-3-fast-beta": {
maxTokens: 8192,
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 5.0,
outputPrice: 25.0,
description: "X AI's Grok-3 fast beta model with 131K context window",
},
"grok-3-mini-beta": {
maxTokens: 8192,
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.3,
outputPrice: 0.5,
description: "X AI's Grok-3 mini beta model with 131K context window",
},
"grok-3-mini-fast-beta": {
maxTokens: 8192,
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0.6,
outputPrice: 4.0,
description: "X AI's Grok-3 mini fast beta model with 131K context window",
},
"grok-2-latest": {
maxTokens: 8192,
contextWindow: 131072,
@@ -1476,3 +1582,19 @@ export const sambanovaModels = {
outputPrice: 1.5,
},
} as const satisfies Record<string, ModelInfo>
// Requesty
// https://requesty.ai/models
export const requestyDefaultModelId = "anthropic/claude-3-7-sonnet-latest"
export const requestyDefaultModelInfo: ModelInfo = {
maxTokens: 8192,
contextWindow: 200_000,
supportsImages: true,
supportsComputerUse: false,
supportsPromptCache: true,
inputPrice: 3.0,
outputPrice: 15.0,
cacheWritesPrice: 3.75,
cacheReadsPrice: 0.3,
description: "Anthropic's most intelligent model. Highest level of intelligence and capability.",
}
+2
View File
@@ -105,3 +105,5 @@ export interface McpDownloadResponse {
llmsInstallationContent: string
requiresApiKey: boolean
}
export type McpViewTab = "marketplace" | "addRemote" | "installed"
+2 -2
View File
@@ -7,8 +7,8 @@ import { userInfo } from "os"
describe("Shell Detection Tests", () => {
let originalPlatform: string
let originalEnv: NodeJS.ProcessEnv
let originalGetConfig: any
let originalUserInfo: any
let originalGetConfig: typeof vscode.workspace.getConfiguration
let originalUserInfo: typeof userInfo
// Helper to mock VS Code configuration
function mockVsCodeConfig(platformKey: string, defaultProfileName: string | null, profiles: Record<string, any>) {
+49 -5
View File
@@ -2,15 +2,48 @@ import { ModelInfo } from "../shared/api"
function calculateApiCostInternal(
modelInfo: ModelInfo,
inputTokens: number,
inputTokens: number, // Note: For OpenAI-style, this is non-cached tokens. For Anthropic-style, this is total input tokens.
outputTokens: number,
cacheCreationInputTokens: number,
cacheReadInputTokens: number,
totalInputTokensForPricing?: number, // The *total* input tokens, used for tiered pricing lookup
): number {
// Determine effective input price
let effectiveInputPrice = modelInfo.inputPrice || 0
if (modelInfo.inputPriceTiers && modelInfo.inputPriceTiers.length > 0 && totalInputTokensForPricing !== undefined) {
// Ensure tiers are sorted by tokenLimit ascending before finding
const sortedInputTiers = [...modelInfo.inputPriceTiers].sort((a, b) => a.tokenLimit - b.tokenLimit)
// Find the first tier where the total input tokens are less than or equal to the limit
const tier = sortedInputTiers.find((t) => totalInputTokensForPricing! <= t.tokenLimit)
if (tier) {
effectiveInputPrice = tier.price
} else {
// Should ideally not happen if Infinity is used for the last tier, but fallback just in case
effectiveInputPrice = sortedInputTiers[sortedInputTiers.length - 1]?.price || 0
}
}
// Determine effective output price (based on total *input* tokens for pricing)
let effectiveOutputPrice = modelInfo.outputPrice || 0
if (modelInfo.outputPriceTiers && modelInfo.outputPriceTiers.length > 0 && totalInputTokensForPricing !== undefined) {
// Ensure tiers are sorted by tokenLimit ascending before finding
const sortedOutputTiers = [...modelInfo.outputPriceTiers].sort((a, b) => a.tokenLimit - b.tokenLimit)
const tier = sortedOutputTiers.find((t) => totalInputTokensForPricing! <= t.tokenLimit)
if (tier) {
effectiveOutputPrice = tier.price
} else {
// Should ideally not happen if Infinity is used for the last tier, but fallback just in case
effectiveOutputPrice = sortedOutputTiers[sortedOutputTiers.length - 1]?.price || 0
}
}
const cacheWritesCost = ((modelInfo.cacheWritesPrice || 0) / 1_000_000) * cacheCreationInputTokens
const cacheReadsCost = ((modelInfo.cacheReadsPrice || 0) / 1_000_000) * cacheReadInputTokens
const baseInputCost = ((modelInfo.inputPrice || 0) / 1_000_000) * inputTokens
const outputCost = ((modelInfo.outputPrice || 0) / 1_000_000) * outputTokens
// Use effectiveInputPrice for baseInputCost. Note: 'inputTokens' here is the potentially adjusted count (e.g., non-cached for OpenAI)
const baseInputCost = (effectiveInputPrice / 1_000_000) * inputTokens
// Use effectiveOutputPrice for outputCost
const outputCost = (effectiveOutputPrice / 1_000_000) * outputTokens
const totalCost = cacheWritesCost + cacheReadsCost + baseInputCost + outputCost
return totalCost
}
@@ -25,7 +58,15 @@ export function calculateApiCostAnthropic(
): number {
const cacheCreationInputTokensNum = cacheCreationInputTokens || 0
const cacheReadInputTokensNum = cacheReadInputTokens || 0
return calculateApiCostInternal(modelInfo, inputTokens, outputTokens, cacheCreationInputTokensNum, cacheReadInputTokensNum)
// Anthropic style doesn't need totalInputTokensForPricing as its inputTokens already represents the total
return calculateApiCostInternal(
modelInfo,
inputTokens,
outputTokens,
cacheCreationInputTokensNum,
cacheReadInputTokensNum,
undefined, // Pass undefined for totalInputTokensForPricing
)
}
// For OpenAI compliant usage, the input tokens count INCLUDES the cached tokens
@@ -38,12 +79,15 @@ export function calculateApiCostOpenAI(
): number {
const cacheCreationInputTokensNum = cacheCreationInputTokens || 0
const cacheReadInputTokensNum = cacheReadInputTokens || 0
// Calculate non-cached tokens for the internal function's 'inputTokens' parameter
const nonCachedInputTokens = Math.max(0, inputTokens - cacheCreationInputTokensNum - cacheReadInputTokensNum)
// Pass the original 'inputTokens' as 'totalInputTokensForPricing' for tier lookup
return calculateApiCostInternal(
modelInfo,
nonCachedInputTokens,
nonCachedInputTokens, // Pass the adjusted token count here
outputTokens,
cacheCreationInputTokensNum,
cacheReadInputTokensNum,
inputTokens, // Pass the original total input tokens for pricing tier lookup
)
}
+19
View File
@@ -74,3 +74,22 @@ export async function getFileSizeInKB(filePath: string): Promise<number> {
return 0
}
}
/**
* Recursively reads a directory and returns an array of absolute file paths.
*
* @param directoryPath - The path to the directory to read.
* @returns A promise that resolves to an array of absolute file paths.
* @throws Error if the directory cannot be read.
*/
export const readDirectory = async (directoryPath: string) => {
try {
const filePaths = await fs
.readdir(directoryPath, { withFileTypes: true, recursive: true })
.then((files) => files.filter((file) => file.isFile()))
.then((files) => files.map((file) => path.resolve(file.parentPath, file.name)))
return filePaths
} catch {
throw new Error(`Error reading directory at ${directoryPath}`)
}
}
+22
View File
@@ -1,6 +1,7 @@
import * as path from "path"
import os from "os"
import * as vscode from "vscode"
import { realpathSync } from "fs"
/*
The Node.js 'path' module resolves and normalizes paths differently depending on the platform:
@@ -110,3 +111,24 @@ export const getWorkspacePath = (defaultCwdPath = "") => {
}
return cwdPath
}
export const isLocatedInWorkspace = (pathToCheck: string = ""): boolean => {
const workspacePath = getWorkspacePath()
// Handle long paths in Windows
if (pathToCheck.startsWith("\\\\?\\") || workspacePath.startsWith("\\\\?\\")) {
return pathToCheck.startsWith(workspacePath)
}
const resolvedPath = path.resolve(workspacePath, pathToCheck)
// Using realpathSync to resolve any symbolic links
try {
const realWorkspacePath = realpathSync(workspacePath)
const realPath = realpathSync(resolvedPath)
return realPath.startsWith(realWorkspacePath)
} catch (error) {
console.error("Error resolving paths:", error)
return false
}
}

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